Merge branch 'open182' into open229

This commit is contained in:
Victor Woeltjen 2015-10-30 14:54:37 -07:00
commit 5706fa4567
41 changed files with 1295 additions and 281 deletions

View File

@ -6,6 +6,7 @@
"platform/commonUI/browse",
"platform/commonUI/edit",
"platform/commonUI/dialog",
"platform/commonUI/formats",
"platform/commonUI/general",
"platform/commonUI/inspect",
"platform/commonUI/mobile",

View File

@ -16,6 +16,23 @@
"implementation": "SinewaveLimitCapability.js"
}
],
"formats": [
{
"key": "example.delta",
"implementation": "SinewaveDeltaFormat.js"
}
],
"constants": [
{
"key": "TIME_CONDUCTOR_DOMAINS",
"value": [
{ "key": "time", "name": "Time" },
{ "key": "yesterday", "name": "Yesterday" },
{ "key": "delta", "name": "Delta", "format": "example.delta" }
],
"priority": -1
}
],
"types": [
{
"key": "generator",
@ -38,6 +55,11 @@
{
"key": "yesterday",
"name": "Yesterday"
},
{
"key": "delta",
"name": "Delta",
"format": "example.delta"
}
],
"ranges": [

View File

@ -0,0 +1,26 @@
/*****************************************************************************
* 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({
START_TIME: Date.now() - 24 * 60 * 60 * 1000 // Now minus a day.
});

View File

@ -0,0 +1,68 @@
/*****************************************************************************
* 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(
['./SinewaveConstants', 'moment'],
function (SinewaveConstants, moment) {
"use strict";
var START_TIME = SinewaveConstants.START_TIME,
FORMAT_REGEX = /^-?\d+:\d+:\d+$/,
SECOND = 1000,
MINUTE = SECOND * 60,
HOUR = MINUTE * 60;
function SinewaveDeltaFormat() {
}
function twoDigit(v) {
return v >= 10 ? String(v) : ('0' + v);
}
SinewaveDeltaFormat.prototype.format = function (value) {
var delta = Math.abs(value - START_TIME),
negative = value < START_TIME,
seconds = Math.floor(delta / SECOND) % 60,
minutes = Math.floor(delta / MINUTE) % 60,
hours = Math.floor(delta / HOUR);
return (negative ? "-" : "") +
[ hours, minutes, seconds ].map(twoDigit).join(":");
};
SinewaveDeltaFormat.prototype.validate = function (text) {
return FORMAT_REGEX.test(text);
};
SinewaveDeltaFormat.prototype.parse = function (text) {
var negative = text[0] === "-",
parts = text.replace("-", "").split(":");
return [ HOUR, MINUTE, SECOND ].map(function (sz, i) {
return parseInt(parts[i], 10) * sz;
}).reduce(function (a, b) {
return a + b;
}, 0) * (negative ? -1 : 1) + START_TIME;
};
return SinewaveDeltaFormat;
}
);

View File

@ -25,12 +25,12 @@
* Module defining SinewaveTelemetry. Created by vwoeltje on 11/12/14.
*/
define(
[],
function () {
['./SinewaveConstants'],
function (SinewaveConstants) {
"use strict";
var ONE_DAY = 60 * 60 * 24,
firstObservedTime = Math.floor(Date.now() / 1000) - ONE_DAY;
firstObservedTime = Math.floor(SinewaveConstants.START_TIME / 1000);
/**
*
@ -58,6 +58,9 @@ define(
};
generatorData.getDomainValue = function (i, domain) {
// delta uses the same numeric values as the default domain,
// so it's not checked for here, just formatted for display
// differently.
return (i + offset) * 1000 + firstTime * 1000 -
(domain === 'yesterday' ? ONE_DAY : 0);
};

View File

@ -0,0 +1,26 @@
{
"name": "Time services bundle",
"description": "Defines interfaces and provides default implementations for handling different time systems.",
"extensions": {
"components": [
{
"provides": "formatService",
"type": "provider",
"implementation": "FormatProvider.js",
"depends": [ "formats[]", "$log" ]
}
],
"formats": [
{
"key": "utc",
"implementation": "UTCTimeFormat.js"
}
],
"constants": [
{
"key": "DEFAULT_TIME_FORMAT",
"value": "utc"
}
]
}
}

View File

@ -0,0 +1,113 @@
/*****************************************************************************
* 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";
/**
* An object used to convert between numeric values and text values,
* typically used to display these values to the user and to convert
* user input to a numeric format, particularly for time formats.
* @interface {Format}
*/
/**
* Parse text (typically user input) to a numeric value.
* Behavior is undefined when the text cannot be parsed;
* `validate` should be called first if the text may be invalid.
* @method parse
* @memberof Format#
* @param {string} text the text to parse
* @returns {number} the parsed numeric value
*/
/**
* Determine whether or not some text (typically user input) can
* be parsed to a numeric value by this format.
* @method validate
* @memberof Format#
* @param {string} text the text to parse
* @returns {boolean} true if the text can be parsed
*/
/**
* Convert a numeric value to a text value for display using
* this format.
* @method format
* @memberof Format#
* @param {number} value the numeric value to format
* @returns {string} the text representation of the value
*/
/**
* Provides access to `Format` objects which can be used to
* convert values between human-readable text and numeric
* representations.
* @interface FormatService
*/
/**
* Look up a format by its symbolic identifier.
* @param {string} key the identifier for this format
* @returns {Format} the format, or `undefined` if no such format
* is known.
*/
/**
* Provides formats from the `formats` extension category.
* @constructor
* @implements {FormatService}
* @memberof platform/commonUI/formats
* @param {Array.<function(new : Format)>} format constructors,
* from the `formats` extension category.
*/
function FormatProvider(formats, $log) {
var formatMap = {};
function addToMap(Format) {
var key = Format.key;
if (key && !formatMap[key]) {
formatMap[key] = new Format();
}
}
formats.forEach(addToMap);
this.formatMap = formatMap;
this.$log = $log;
}
FormatProvider.prototype.getFormat = function (key) {
var format = this.formatMap[key];
if (!format) {
this.$log.warn("No format found for " + key);
}
return format;
};
return FormatProvider;
});

View File

@ -0,0 +1,63 @@
/*****************************************************************************
* Open MCT Web, Copyright (c) 2014-2015, United States Government
* as represented by the Administrator of the National Aeronautics and Space
* Administration. All rights reserved.
*
* Open MCT Web is licensed under the Apache License, Version 2.0 (the
* "License"); you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
* http://www.apache.org/licenses/LICENSE-2.0.
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
* WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
* License for the specific language governing permissions and limitations
* under the License.
*
* Open MCT Web includes source code licensed under additional open source
* licenses. See the Open Source Licenses file (LICENSES.md) included with
* this source code distribution or the Licensing information page available
* at runtime from the About dialog for additional information.
*****************************************************************************/
/*global define*/
define([
'moment'
], function (
moment
) {
"use strict";
var DATE_FORMAT = "YYYY-MM-DD HH:mm:ss",
DATE_FORMATS = [
DATE_FORMAT,
"YYYY-MM-DD HH:mm",
"YYYY-MM-DD"
];
/**
* Formatter for UTC timestamps. Interprets numeric values as
* milliseconds since the start of 1970.
*
* @implements {Format}
* @constructor
* @memberof platform/commonUI/formats
*/
function UTCTimeFormat() {
}
UTCTimeFormat.prototype.format = function (value) {
return moment.utc(value).format(DATE_FORMAT);
};
UTCTimeFormat.prototype.parse = function (text) {
return moment.utc(text, DATE_FORMATS).valueOf();
};
UTCTimeFormat.prototype.validate = function (text) {
return moment.utc(text, DATE_FORMATS).isValid();
};
return UTCTimeFormat;
});

View File

@ -0,0 +1,70 @@
/*****************************************************************************
* Open MCT Web, Copyright (c) 2014-2015, United States Government
* as represented by the Administrator of the National Aeronautics and Space
* Administration. All rights reserved.
*
* Open MCT Web is licensed under the Apache License, Version 2.0 (the
* "License"); you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
* http://www.apache.org/licenses/LICENSE-2.0.
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
* WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
* License for the specific language governing permissions and limitations
* under the License.
*
* Open MCT Web includes source code licensed under additional open source
* licenses. See the Open Source Licenses file (LICENSES.md) included with
* this source code distribution or the Licensing information page available
* at runtime from the About dialog for additional information.
*****************************************************************************/
/*global define,Promise,describe,it,expect,beforeEach,waitsFor,jasmine*/
define(
['../src/FormatProvider'],
function (FormatProvider) {
'use strict';
var KEYS = [ 'a', 'b', 'c' ];
describe("The FormatProvider", function () {
var mockFormats,
mockLog,
mockFormatInstances,
provider;
beforeEach(function () {
mockFormatInstances = KEYS.map(function (k) {
return jasmine.createSpyObj(
'format-' + k,
[ 'parse', 'validate', 'format' ]
);
});
mockLog = jasmine.createSpyObj('$log', ['error', 'warn']);
// Return constructors
mockFormats = KEYS.map(function (k, i) {
function MockFormat() { return mockFormatInstances[i]; }
MockFormat.key = k;
return MockFormat;
});
provider = new FormatProvider(mockFormats, mockLog);
});
it("looks up formats by key", function () {
KEYS.forEach(function (k, i) {
expect(provider.getFormat(k))
.toEqual(mockFormatInstances[i]);
});
});
it("warns about unknown formats", function () {
provider.getFormat('a'); // known format
expect(mockLog.warn).not.toHaveBeenCalled();
provider.getFormat('some-unknown-format');
expect(mockLog.warn).toHaveBeenCalledWith(jasmine.any(String));
});
});
}
);

View File

@ -0,0 +1,56 @@
/*****************************************************************************
* 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/UTCTimeFormat', 'moment'],
function (UTCTimeFormat, moment) {
'use strict';
describe("The UTCTimeFormat", function () {
var format;
beforeEach(function () {
format = new UTCTimeFormat();
});
it("formats UTC timestamps", function () {
var timestamp = 12345670000,
formatted = format.format(timestamp);
expect(formatted).toEqual(jasmine.any(String));
expect(moment.utc(formatted).valueOf()).toEqual(timestamp);
});
it("validates time inputs", function () {
expect(format.validate("1977-05-25 11:21:22")).toBe(true);
expect(format.validate("garbage text")).toBe(false);
});
it("parses valid input", function () {
var text = "1977-05-25 11:21:22",
parsed = format.parse(text);
expect(parsed).toEqual(jasmine.any(Number));
expect(parsed).toEqual(moment.utc(text).valueOf());
});
});
}
);

View File

@ -0,0 +1,4 @@
[
"FormatProvider",
"UTCTimeFormat"
]

View File

@ -53,13 +53,18 @@
{
"key": "TimeRangeController",
"implementation": "controllers/TimeRangeController.js",
"depends": [ "$scope", "now" ]
"depends": [ "$scope", "formatService", "DEFAULT_TIME_FORMAT", "now" ]
},
{
"key": "DateTimePickerController",
"implementation": "controllers/DateTimePickerController.js",
"depends": [ "$scope", "now" ]
},
{
"key": "DateTimeFieldController",
"implementation": "controllers/DateTimeFieldController.js",
"depends": [ "$scope", "formatService", "DEFAULT_TIME_FORMAT" ]
},
{
"key": "TreeNodeController",
"implementation": "controllers/TreeNodeController.js",
@ -242,6 +247,10 @@
{
"key": "datetime-picker",
"templateUrl": "templates/controls/datetime-picker.html"
},
{
"key": "datetime-field",
"templateUrl": "templates/controls/datetime-field.html"
}
],
"licenses": [

View File

@ -0,0 +1,20 @@
<span class="s-btn"
ng-controller="DateTimeFieldController">
<input type="text"
ng-model="textValue"
ng-class="{ error: textInvalid }">
</input>
<a class="ui-symbol icon icon-calendar"
ng-if="structure.format === 'utc' || !structure.format"
ng-click="pickerActive = !pickerActive">
</a>
<mct-popup ng-if="pickerActive">
<div mct-click-elsewhere="pickerActive = false">
<mct-control key="'datetime-picker'"
ng-model="ngModel"
field="field"
options="{ hours: true }">
</mct-control>
</div>
</mct-popup>
</span>

View File

@ -22,47 +22,24 @@
<div ng-controller="TimeRangeController">
<div class="l-time-range-inputs-holder">
<span class="l-time-range-inputs-elem ui-symbol type-icon">&#x43;</span>
<span class="l-time-range-input" ng-controller="ToggleController as t1">
<!--<span class="lbl">Start</span>-->
<span class="s-btn time-range-start">
<input type="text"
ng-model="boundsModel.start"
ng-class="{ error: !boundsModel.startValid }">
</input>
<a class="ui-symbol icon icon-calendar" ng-click="t1.toggle()"></a>
<mct-popup ng-if="t1.isActive()">
<div mct-click-elsewhere="t1.setState(false)">
<mct-control key="'datetime-picker'"
ng-model="ngModel.outer"
field="'start'"
options="{ hours: true }">
</mct-control>
</div>
</mct-popup>
</span>
<span class="l-time-range-input">
<mct-control key="'datetime-field'"
structure="{ format: parameters.format }"
ng-model="ngModel.outer"
field="'start'"
class="time-range-start">
</mct-control>
</span>
<span class="l-time-range-inputs-elem lbl">to</span>
<span class="l-time-range-input" ng-controller="ToggleController as t2">
<!--<span class="lbl">End</span>-->
<span class="s-btn l-time-range-input">
<input type="text"
ng-model="boundsModel.end"
ng-class="{ error: !boundsModel.endValid }">
</input>
<a class="ui-symbol icon icon-calendar" ng-click="t2.toggle()">
</a>
<mct-popup ng-if="t2.isActive()">
<div mct-click-elsewhere="t2.setState(false)">
<mct-control key="'datetime-picker'"
ng-model="ngModel.outer"
field="'end'"
options="{ hours: true }">
</mct-control>
</div>
</mct-popup>
</span>&nbsp;
<mct-control key="'datetime-field'"
structure="{ format: parameters.format }"
ng-model="ngModel.outer"
field="'end'"
class="time-range-end">
</mct-control>&nbsp;
</span>
</div>
@ -97,7 +74,7 @@
<div class="l-time-range-ticks-holder">
<div class="l-time-range-ticks">
<div
ng-repeat="tick in ticks"
ng-repeat="tick in ticks track by $index"
ng-style="{ left: $index * (100 / (ticks.length - 1)) + '%' }"
class="tick tick-x"
>

View File

@ -0,0 +1,85 @@
/*****************************************************************************
* 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(
[],
function () {
'use strict';
var UNRECOGNIZED_FORMAT_ERROR =
"Unrecognized format for date-time field.";
/**
* Controller to support the date-time entry field.
*
* Accepts a `format` property in the `structure` attribute
* which allows a date/time to be specified via its symbolic
* key (as will be used to look up said format from the
* `formatService`.)
*
* {@see FormatService}
* @constructor
* @memberof platform/commonUI/general
* @param $scope the Angular scope for this controller
* @param {FormatService} formatService the service to user to format
* domain values
* @param {string} defaultFormat the format to request when no
* format has been otherwise specified
*/
function DateTimeFieldController($scope, formatService, defaultFormat) {
var formatter = formatService.getFormat(defaultFormat);
function updateFromModel(value) {
// Only reformat if the value is different from user
// input (to avoid reformatting valid input while typing.)
if (!formatter.validate($scope.textValue) ||
formatter.parse($scope.textValue) !== value) {
$scope.textValue = formatter.format(value);
$scope.textInvalid = false;
}
}
function updateFromView(textValue) {
$scope.textInvalid = !formatter.validate(textValue);
if (!$scope.textInvalid) {
$scope.ngModel[$scope.field] =
formatter.parse(textValue);
}
}
function setFormat(format) {
formatter = formatService.getFormat(format || defaultFormat);
if (!formatter) {
throw new Error(UNRECOGNIZED_FORMAT_ERROR);
}
updateFromModel($scope.ngModel[$scope.field]);
}
$scope.$watch('structure.format', setFormat);
$scope.$watch('ngModel[field]', updateFromModel);
$scope.$watch('textValue', updateFromView);
}
return DateTimeFieldController;
}
);

View File

@ -26,33 +26,34 @@ define(
function (moment) {
"use strict";
var DATE_FORMAT = "YYYY-MM-DD HH:mm:ss",
TICK_SPACING_PX = 150;
var TICK_SPACING_PX = 150,
UNRECOGNIZED_FORMAT_ERROR =
"Unrecognized format for time range control.";
/**
* Controller used by the `time-controller` template.
* @memberof platform/commonUI/general
* @constructor
* @param $scope the Angular scope for this controller
* @param {FormatService} formatService the service to user to format
* domain values
* @param {string} defaultFormat the format to request when no
* format has been otherwise specified
* @param {Function} now a function to return current system time
*/
function TimeConductorController($scope, now) {
function TimeRangeController($scope, formatService, defaultFormat, now) {
var tickCount = 2,
innerMinimumSpan = 1000, // 1 second
outerMinimumSpan = 1000 * 60 * 60, // 1 hour
initialDragValue;
initialDragValue,
formatter = formatService.getFormat(defaultFormat);
function formatTimestamp(ts) {
return moment.utc(ts).format(DATE_FORMAT);
return formatter.format(ts);
}
function parseTimestamp(text) {
var m = moment.utc(text, DATE_FORMAT);
if (m.isValid()) {
return m.valueOf();
} else {
throw new Error("Could not parse " + text);
}
}
// From 0.0-1.0 to "0%"-"1%"
// From 0.0-1.0 to "0%"-"100%"
function toPercent(p) {
return (100 * p) + "%";
}
@ -101,41 +102,15 @@ define(
return { start: bounds.start, end: bounds.end };
}
function updateBoundsTextForProperty(ngModel, property) {
try {
if (!$scope.boundsModel[property] ||
parseTimestamp($scope.boundsModel[property]) !==
ngModel.outer[property]) {
$scope.boundsModel[property] =
formatTimestamp(ngModel.outer[property]);
}
} catch (e) {
// User-entered text is invalid, so leave it be
// until they fix it.
}
}
function updateBoundsText(ngModel) {
updateBoundsTextForProperty(ngModel, 'start');
updateBoundsTextForProperty(ngModel, '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
updateBoundsText(ngModel);
// Then various updates for the inner span
updateViewForInnerSpanFromModel(ngModel);
// Stick it back is scope (in case we just set defaults)
$scope.ngModel = ngModel;
updateViewForInnerSpanFromModel(ngModel);
updateTicks();
}
@ -155,7 +130,8 @@ define(
}
function toMillis(pixels) {
var span = $scope.ngModel.outer.end - $scope.ngModel.outer.start;
var span =
$scope.ngModel.outer.end - $scope.ngModel.outer.start;
return (pixels / $scope.spanWidth) * span;
}
@ -243,36 +219,15 @@ define(
updateTicks();
}
function updateStartFromText(value) {
try {
updateOuterStart(parseTimestamp(value));
updateBoundsTextForProperty($scope.ngModel, 'end');
$scope.boundsModel.startValid = true;
} catch (e) {
$scope.boundsModel.startValid = false;
return;
function updateFormat(key) {
formatter = formatService.getFormat(key || defaultFormat);
if (!formatter) {
throw new Error(UNRECOGNIZED_FORMAT_ERROR);
}
}
function updateEndFromText(value) {
try {
updateOuterEnd(parseTimestamp(value));
updateBoundsTextForProperty($scope.ngModel, 'start');
$scope.boundsModel.endValid = true;
} catch (e) {
$scope.boundsModel.endValid = false;
return;
}
}
function updateStartFromPicker(value) {
updateOuterStart(value);
updateBoundsText($scope.ngModel);
}
function updateEndFromPicker(value) {
updateOuterEnd(value);
updateBoundsText($scope.ngModel);
updateViewForInnerSpanFromModel($scope.ngModel);
updateTicks();
}
$scope.startLeftDrag = startLeftDrag;
@ -282,21 +237,18 @@ define(
$scope.rightDrag = rightDrag;
$scope.middleDrag = middleDrag;
$scope.state = false;
$scope.ticks = [];
$scope.boundsModel = {};
// Initialize scope to defaults
updateViewFromModel($scope.ngModel);
$scope.$watchCollection("ngModel", updateViewFromModel);
$scope.$watch("spanWidth", updateSpanWidth);
$scope.$watch("ngModel.outer.start", updateStartFromPicker);
$scope.$watch("ngModel.outer.end", updateEndFromPicker);
$scope.$watch("boundsModel.start", updateStartFromText);
$scope.$watch("boundsModel.end", updateEndFromText);
$scope.$watch("ngModel.outer.start", updateOuterStart);
$scope.$watch("ngModel.outer.end", updateOuterEnd);
$scope.$watch("parameters.format", updateFormat);
}
return TimeConductorController;
return TimeRangeController;
}
);

View File

@ -0,0 +1,183 @@
/*****************************************************************************
* 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/DateTimeFieldController", "moment"],
function (DateTimeFieldController, moment) {
'use strict';
var TEST_FORMAT = "YYYY-MM-DD HH:mm:ss";
describe("The DateTimeFieldController", function () {
var mockScope,
mockFormatService,
mockFormat,
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', ['$watch']);
mockFormatService =
jasmine.createSpyObj('formatService', ['getFormat']);
mockFormat = jasmine.createSpyObj('format', [
'parse',
'validate',
'format'
]);
mockFormatService.getFormat.andReturn(mockFormat);
mockFormat.validate.andCallFake(function (text) {
return moment.utc(text, TEST_FORMAT).isValid();
});
mockFormat.parse.andCallFake(function (text) {
return moment.utc(text, TEST_FORMAT).valueOf();
});
mockFormat.format.andCallFake(function (value) {
return moment.utc(value).format(TEST_FORMAT);
});
mockScope.ngModel = { testField: 12321 };
mockScope.field = "testField";
mockScope.structure = { format: "someFormat" };
controller = new DateTimeFieldController(
mockScope,
mockFormatService
);
});
it("updates models from user-entered text", function () {
var newText = "1977-05-25 17:30:00";
mockScope.textValue = newText;
fireWatch("textValue", newText);
expect(mockScope.ngModel.testField)
.toEqual(mockFormat.parse(newText));
expect(mockScope.textInvalid).toBeFalsy();
});
it("updates text from model values", function () {
var testTime = mockFormat.parse("1977-05-25 17:30:00");
mockScope.ngModel.testField = testTime;
fireWatch("ngModel[field]", testTime);
expect(mockScope.textValue).toEqual("1977-05-25 17:30:00");
});
describe("when user input is invalid", function () {
var newText, oldValue;
beforeEach(function () {
newText = "Not a date";
oldValue = mockScope.ngModel.testField;
mockScope.textValue = newText;
fireWatch("textValue", newText);
});
it("displays error state", function () {
expect(mockScope.textInvalid).toBeTruthy();
});
it("does not modify model state", function () {
expect(mockScope.ngModel.testField).toEqual(oldValue);
});
it("does not modify user input", function () {
expect(mockScope.textValue).toEqual(newText);
});
});
it("does not modify valid but irregular user input", function () {
// Don't want the controller "fixing" bad or
// irregularly-formatted input out from under
// the user's fingertips.
var newText = "2015-3-3 01:02:04",
oldValue = mockScope.ngModel.testField;
mockFormat.validate.andReturn(true);
mockFormat.parse.andReturn(42);
mockScope.textValue = newText;
fireWatch("textValue", newText);
expect(mockScope.textValue).toEqual(newText);
expect(mockScope.ngModel.testField).toEqual(42);
expect(mockScope.ngModel.testField).not.toEqual(oldValue);
});
it("obtains a format from the format service", function () {
fireWatch('structure.format', mockScope.structure.format);
expect(mockFormatService.getFormat)
.toHaveBeenCalledWith(mockScope.structure.format);
});
it("throws an error for unknown formats", function () {
mockFormatService.getFormat.andReturn(undefined);
expect(function () {
fireWatch("structure.format", "some-format");
}).toThrow();
});
describe("using the obtained format", function () {
var testValue = 1234321,
testText = "some text";
beforeEach(function () {
mockFormat.validate.andReturn(true);
mockFormat.parse.andReturn(testValue);
mockFormat.format.andReturn(testText);
});
it("parses user input", function () {
var newText = "some other new text";
mockScope.textValue = newText;
fireWatch("textValue", newText);
expect(mockFormat.parse).toHaveBeenCalledWith(newText);
expect(mockScope.ngModel.testField).toEqual(testValue);
});
it("validates user input", function () {
var newText = "some other new text";
mockScope.textValue = newText;
fireWatch("textValue", newText);
expect(mockFormat.validate).toHaveBeenCalledWith(newText);
});
it("formats model data for display", function () {
var newValue = 42;
mockScope.ngModel.testField = newValue;
fireWatch("ngModel[field]", newValue);
expect(mockFormat.format).toHaveBeenCalledWith(newValue);
expect(mockScope.textValue).toEqual(testText);
});
});
});
}
);

View File

@ -33,7 +33,10 @@ define(
describe("The TimeRangeController", function () {
var mockScope,
mockFormatService,
testDefaultFormat,
mockNow,
mockFormat,
controller;
function fireWatch(expr, value) {
@ -57,8 +60,30 @@ define(
"$scope",
[ "$apply", "$watch", "$watchCollection" ]
);
mockFormatService = jasmine.createSpyObj(
"formatService",
[ "getFormat" ]
);
testDefaultFormat = 'utc';
mockFormat = jasmine.createSpyObj(
"format",
[ "validate", "format", "parse" ]
);
mockFormatService.getFormat.andReturn(mockFormat);
mockFormat.format.andCallFake(function (value) {
return moment.utc(value).format("YYYY-MM-DD HH:mm:ss");
});
mockNow = jasmine.createSpy('now');
controller = new TimeRangeController(mockScope, mockNow);
controller = new TimeRangeController(
mockScope,
mockFormatService,
testDefaultFormat,
mockNow
);
});
it("watches the model that was passed in", function () {
@ -167,70 +192,22 @@ define(
.toBeGreaterThan(mockScope.ngModel.inner.start);
});
describe("by typing", function () {
it("updates models", function () {
var newStart = "1977-05-25 17:30:00",
newEnd = "2015-12-18 03:30:00";
mockScope.boundsModel.start = newStart;
fireWatch("boundsModel.start", newStart);
expect(mockScope.ngModel.outer.start)
.toEqual(moment.utc(newStart).valueOf());
expect(mockScope.boundsModel.startValid)
.toBeTruthy();
mockScope.boundsModel.end = newEnd;
fireWatch("boundsModel.end", newEnd);
expect(mockScope.ngModel.outer.end)
.toEqual(moment.utc(newEnd).valueOf());
expect(mockScope.boundsModel.endValid)
.toBeTruthy();
});
it("displays error state", function () {
var newStart = "Not a date",
newEnd = "Definitely not a date",
oldStart = mockScope.ngModel.outer.start,
oldEnd = mockScope.ngModel.outer.end;
mockScope.boundsModel.start = newStart;
fireWatch("boundsModel.start", newStart);
expect(mockScope.ngModel.outer.start)
.toEqual(oldStart);
expect(mockScope.boundsModel.startValid)
.toBeFalsy();
mockScope.boundsModel.end = newEnd;
fireWatch("boundsModel.end", newEnd);
expect(mockScope.ngModel.outer.end)
.toEqual(oldEnd);
expect(mockScope.boundsModel.endValid)
.toBeFalsy();
});
it("does not modify user input", function () {
// Don't want the controller "fixing" bad or
// irregularly-formatted input out from under
// the user's fingertips.
var newStart = "Not a date",
newEnd = "2015-3-3 01:02:04",
oldStart = mockScope.ngModel.outer.start,
oldEnd = mockScope.ngModel.outer.end;
mockScope.boundsModel.start = newStart;
fireWatch("boundsModel.start", newStart);
expect(mockScope.boundsModel.start)
.toEqual(newStart);
mockScope.boundsModel.end = newEnd;
fireWatch("boundsModel.end", newEnd);
expect(mockScope.boundsModel.end)
.toEqual(newEnd);
});
});
});
it("watches for changes in format selection", function () {
expect(mockFormatService.getFormat)
.not.toHaveBeenCalledWith('test-format');
fireWatch("parameters.format", 'test-format');
expect(mockFormatService.getFormat)
.toHaveBeenCalledWith('test-format');
});
it("throws an error for unknown formats", function () {
mockFormatService.getFormat.andReturn(undefined);
expect(function () {
fireWatch("parameters.format", "some-format");
}).toThrow();
});
});
}

View File

@ -3,6 +3,7 @@
"controllers/BottomBarController",
"controllers/ClickAwayController",
"controllers/ContextMenuController",
"controllers/DateTimeFieldController",
"controllers/DateTimePickerController",
"controllers/GetterSetterController",
"controllers/SelectorController",

View File

@ -36,9 +36,9 @@
{
"key": "TIME_CONDUCTOR_DOMAINS",
"value": [
{ "key": "time", "name": "Time" },
{ "key": "yesterday", "name": "Yesterday" }
{ "key": "time", "name": "UTC", "format": "utc" }
],
"priority": "fallback",
"comment": "Placeholder; to be replaced by inspection of available domains."
}
]

View File

@ -1,4 +1,5 @@
<mct-include key="'time-controller'"
parameters='parameters'
ng-model='ngModel.conductor'>
</mct-include>
<mct-control key="'select'"

View File

@ -27,7 +27,10 @@ define(
"use strict";
var TEMPLATE = [
"<mct-include key=\"'time-conductor'\" ng-model='ngModel' class='l-time-controller'>",
"<mct-include key=\"'time-conductor'\" ",
"ng-model='ngModel' ",
"parameters='parameters' ",
"class='l-time-controller'>",
"</mct-include>"
].join(''),
THROTTLE_MS = 200,
@ -74,11 +77,11 @@ define(
broadcastBounds;
// Combine start/end times into a single object
function bounds(start, end) {
function bounds() {
return {
start: conductor.displayStart(),
end: conductor.displayEnd(),
domain: conductor.domain()
domain: conductor.domain().key
};
}
@ -97,12 +100,10 @@ define(
}
function updateDomain(value) {
conductor.domain(value);
repScope.$broadcast('telemetry:display:bounds', bounds(
conductor.displayStart(),
conductor.displayEnd(),
conductor.domain()
));
var newDomain = conductor.domain(value);
conductorScope.parameters.format =
newDomain && newDomain.format;
repScope.$broadcast('telemetry:display:bounds', bounds());
}
// telemetry domain metadata -> option for a select control
@ -130,7 +131,8 @@ define(
{ outer: bounds(), inner: bounds() };
conductorScope.ngModel.options =
conductor.domainOptions().map(makeOption);
conductorScope.ngModel.domain = conductor.domain();
conductorScope.ngModel.domain = conductor.domain().key;
conductorScope.parameters = {};
conductorScope
.$watch('ngModel.conductor.inner.start', updateConductorInner);

View File

@ -51,7 +51,7 @@ define(
request = request || {};
request.start = start;
request.end = end;
request.domain = domain;
request.domain = domain.key;
return request;
}

View File

@ -43,7 +43,7 @@ define(
function TimeConductor(start, end, domains) {
this.range = { start: start, end: end };
this.domains = domains;
this.activeDomain = domains[0].key;
this.activeDomain = domains[0];
}
/**
@ -73,7 +73,7 @@ define(
/**
* Get available domain options which can be used to bound time
* selection.
* @returns {TelemetryDomain[]} available domains
* @returns {TelemetryDomainMetadata[]} available domains
*/
TimeConductor.prototype.domainOptions = function () {
return this.domains;
@ -82,19 +82,21 @@ define(
/**
* Get or set (if called with an argument) the active domain.
* @param {string} [key] the key identifying the domain choice
* @returns {TelemetryDomain} the active telemetry domain
* @returns {TelemetryDomainMetadata} the active telemetry domain
*/
TimeConductor.prototype.domain = function (key) {
function matchesKey(domain) {
return domain.key === key;
}
var i;
if (arguments.length > 0) {
if (!this.domains.some(matchesKey)) {
throw new Error("Unknown domain " + key);
for (i = 0; i < this.domains.length; i += 1) {
if (this.domains[i].key === key) {
return (this.activeDomain = this.domains[i]);
}
}
this.activeDomain = key;
throw new Error("Unknown domain " + key);
}
return this.activeDomain;
};

View File

@ -129,7 +129,7 @@ define(
it("exposes conductor state in scope", function () {
mockConductor.displayStart.andReturn(1977);
mockConductor.displayEnd.andReturn(1984);
mockConductor.domain.andReturn('d');
mockConductor.domain.andReturn({ key: 'd' });
representer.represent(testViews[0], {});
expect(mockNewScope.ngModel.conductor).toEqual({
@ -219,7 +219,7 @@ define(
representer.represent(testViews[0], null);
expect(mockNewScope.ngModel.domain)
.toEqual(mockConductor.domain());
.toEqual(mockConductor.domain().key);
});
it("exposes domain options in scope", function () {

View File

@ -77,7 +77,7 @@ define(
mockConductor.displayStart.andReturn(42);
mockConductor.displayEnd.andReturn(1977);
mockConductor.domain.andReturn("testDomain");
mockConductor.domain.andReturn({ key: "testDomain" });
decorator = new ConductorTelemetryDecorator(
mockConductorService,
@ -104,7 +104,7 @@ define(
});
it("with domain selection", function () {
expect(request.domain).toEqual(mockConductor.domain());
expect(request.domain).toEqual(mockConductor.domain().key);
});
});
@ -127,7 +127,7 @@ define(
});
it("with domain selection", function () {
expect(request.domain).toEqual(mockConductor.domain());
expect(request.domain).toEqual(mockConductor.domain().key);
});
});

View File

@ -59,12 +59,12 @@ define(
});
it("exposes the current domain choice", function () {
expect(conductor.domain()).toEqual(testDomains[0].key);
expect(conductor.domain()).toEqual(testDomains[0]);
});
it("allows the domain choice to be changed", function () {
conductor.domain(testDomains[1].key);
expect(conductor.domain()).toEqual(testDomains[1].key);
expect(conductor.domain()).toEqual(testDomains[1]);
});
it("throws an error on attempts to set an invalid domain", function () {

View File

@ -31,10 +31,19 @@ define(
"./elements/PlotPalette",
"./elements/PlotAxis",
"./elements/PlotLimitTracker",
"./elements/PlotTelemetryFormatter",
"./modes/PlotModeOptions",
"./SubPlotFactory"
],
function (PlotUpdater, PlotPalette, PlotAxis, PlotLimitTracker, PlotModeOptions, SubPlotFactory) {
function (
PlotUpdater,
PlotPalette,
PlotAxis,
PlotLimitTracker,
PlotTelemetryFormatter,
PlotModeOptions,
SubPlotFactory
) {
"use strict";
var AXIS_DEFAULTS = [
@ -62,7 +71,10 @@ define(
PLOT_FIXED_DURATION
) {
var self = this,
subPlotFactory = new SubPlotFactory(telemetryFormatter),
plotTelemetryFormatter =
new PlotTelemetryFormatter(telemetryFormatter),
subPlotFactory =
new SubPlotFactory(plotTelemetryFormatter),
cachedObjects = [],
updater,
lastBounds,
@ -71,10 +83,9 @@ define(
// Populate the scope with axis information (specifically, options
// available for each axis.)
function setupAxes(metadatas) {
$scope.axes = [
new PlotAxis("domain", metadatas, AXIS_DEFAULTS[0]),
new PlotAxis("range", metadatas, AXIS_DEFAULTS[1])
];
$scope.axes.forEach(function (axis) {
axis.updateMetadata(metadatas);
});
}
// Trigger an update of a specific subplot;
@ -125,37 +136,49 @@ define(
}
}
function getUpdater() {
if (!updater) {
recreateUpdater();
}
return updater;
}
// Handle new telemetry data in this plot
function updateValues() {
self.pending = false;
if (handle) {
setupModes(handle.getTelemetryObjects());
}
if (updater) {
updater.update();
setupAxes(handle.getMetadata());
getUpdater().update();
self.modeOptions.getModeHandler().plotTelemetry(updater);
}
if (self.limitTracker) {
self.limitTracker.update();
self.update();
}
self.update();
}
// Display new historical data as it becomes available
function addHistoricalData(domainObject, series) {
self.pending = false;
updater.addHistorical(domainObject, series);
getUpdater().addHistorical(domainObject, series);
self.modeOptions.getModeHandler().plotTelemetry(updater);
self.update();
}
// Issue a new request for historical telemetry
function requestTelemetry() {
if (handle && updater) {
if (handle) {
handle.request({}, addHistoricalData);
}
}
// Requery for data entirely
function replot() {
if (handle) {
updater = undefined;
requestTelemetry();
}
}
// Create a new subscription; telemetrySubscriber gets
// to do the meaningful work here.
function subscribe(domainObject) {
@ -167,12 +190,7 @@ define(
updateValues,
true // Lossless
);
if (handle) {
setupModes(handle.getTelemetryObjects());
setupAxes(handle.getMetadata());
recreateUpdater();
requestTelemetry();
}
replot();
}
// Release the current subscription (called when scope is destroyed)
@ -185,12 +203,22 @@ define(
// Respond to a display bounds change (requery for data)
function changeDisplayBounds(event, bounds) {
var domainAxis = $scope.axes[0];
domainAxis.chooseOption(bounds.domain);
plotTelemetryFormatter
.setDomainFormat(domainAxis.active.format);
self.pending = true;
releaseSubscription();
subscribe($scope.domainObject);
setBasePanZoom(bounds);
}
function updateDomainFormat(format) {
plotTelemetryFormatter.setDomainFormat(format);
}
this.modeOptions = new PlotModeOptions([], subPlotFactory);
this.updateValues = updateValues;
@ -202,6 +230,13 @@ define(
self.pending = true;
// Initialize axes; will get repopulated when telemetry
// metadata becomes available.
$scope.axes = [
new PlotAxis("domains", [], AXIS_DEFAULTS[0]),
new PlotAxis("ranges", [], AXIS_DEFAULTS[1])
];
// Subscribe to telemetry when a domain object becomes available
$scope.$watch('domainObject', subscribe);

View File

@ -121,9 +121,9 @@ define(
// Utility, for map/forEach loops. Index 0 is domain,
// index 1 is range.
function formatValue(v, i) {
return (i ?
formatter.formatRangeValue :
formatter.formatDomainValue)(v);
return i ?
formatter.formatRangeValue(v) :
formatter.formatDomainValue(v);
}
this.hoverCoordinates = this.mousePosition &&

View File

@ -46,21 +46,9 @@ define(
*
*/
function PlotAxis(axisType, metadatas, defaultValue) {
var keys = {},
options = [];
// Look through all metadata objects and assemble a list
// of all possible domain or range options
function buildOptionsForMetadata(m) {
(m[axisType] || []).forEach(function (option) {
if (!keys[option.key]) {
keys[option.key] = true;
options.push(option);
}
});
}
(metadatas || []).forEach(buildOptionsForMetadata);
this.axisType = axisType;
this.defaultValue = defaultValue;
this.optionKeys = {};
/**
* The currently chosen option for this axis. An
@ -68,7 +56,7 @@ define(
* directly form the plot template.
* @memberof platform/features/plot.PlotAxis#
*/
this.active = options[0] || defaultValue;
this.active = defaultValue;
/**
* The set of options applicable for this axis;
@ -77,9 +65,71 @@ define(
* human-readable names respectively)
* @memberof platform/features/plot.PlotAxis#
*/
this.options = options;
this.options = [];
// Initialize options from metadata objects
this.updateMetadata(metadatas);
}
/**
* Update axis options to reflect current metadata.
* @param {TelemetryMetadata[]} metadata objects describing
* applicable telemetry
*/
PlotAxis.prototype.updateMetadata = function (metadatas) {
var axisType = this.axisType,
optionKeys = this.optionKeys,
newOptions = {},
toAdd = [];
function isValid(option) {
return option && optionKeys[option.key];
}
metadatas.forEach(function (m) {
(m[axisType] || []).forEach(function (option) {
var key = option.key;
if (!optionKeys[key] && !newOptions[key]) {
toAdd.push(option);
}
newOptions[key] = true;
});
});
optionKeys = this.optionKeys = newOptions;
// General approach here is to avoid changing object
// instances unless something has really changed, since
// Angular is watching; don't want to trigger extra digests.
if (!this.options.every(isValid)) {
this.options = this.options.filter(isValid);
}
if (toAdd.length > 0) {
this.options = this.options.concat(toAdd);
}
if (!isValid(this.active)) {
this.active = this.options[0] || this.defaultValue;
}
};
/**
* Change the domain/range selection for this axis. If the
* provided `key` is not recognized as an option, no change
* will occur.
* @param {string} key the identifier for the domain/range
*/
PlotAxis.prototype.chooseOption = function (key) {
var self = this;
this.options.forEach(function (option) {
if (option.key === key) {
self.active = option;
}
});
};
return PlotAxis;
}

View File

@ -0,0 +1,72 @@
/*****************************************************************************
* 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';
/**
* Wraps a `TelemetryFormatter` to provide formats for domain and
* range values; provides a single place to track domain/range
* formats within a plot, allowing other plot elements to simply
* request that values be formatted.
* @constructor
* @memberof platform/features/plot
* @implements {platform/telemetry.TelemetryFormatter}
* @param {TelemetryFormatter} telemetryFormatter the formatter
* to wrap.
*/
function PlotTelemetryFormatter(telemetryFormatter) {
this.telemetryFormatter = telemetryFormatter;
}
/**
* Specify the format to use for domain values.
* @param {string} key the format's identifier
*/
PlotTelemetryFormatter.prototype.setDomainFormat = function (key) {
this.domainFormat = key;
};
/**
* Specify the format to use for range values.
* @param {string} key the format's identifier
*/
PlotTelemetryFormatter.prototype.setRangeFormat = function (key) {
this.rangeFormat = key;
};
PlotTelemetryFormatter.prototype.formatDomainValue = function (value) {
return this.telemetryFormatter
.formatDomainValue(value, this.domainFormat);
};
PlotTelemetryFormatter.prototype.formatRangeValue = function (value) {
return this.telemetryFormatter
.formatRangeValue(value, this.rangeFormat);
};
return PlotTelemetryFormatter;
}
);

View File

@ -43,6 +43,14 @@ define(
this.formatter = formatter;
}
// For phantomjs compatibility, for headless testing
// (Function.prototype.bind unsupported)
function bind(fn, thisObj) {
return fn.bind ? fn.bind(thisObj) : function () {
return fn.apply(thisObj, arguments);
};
}
// Generate ticks; interpolate from start up to
// start + span in count steps, using the provided
// formatter to represent each value.
@ -72,7 +80,7 @@ define(
panZoom.origin[0],
panZoom.dimensions[0],
count,
this.formatter.formatDomainValue
bind(this.formatter.formatDomainValue, this.formatter)
);
};
@ -87,7 +95,7 @@ define(
panZoom.origin[1],
panZoom.dimensions[1],
count,
this.formatter.formatRangeValue
bind(this.formatter.formatRangeValue, this.formatter)
);
};

View File

@ -169,8 +169,9 @@ define(
mockDomainObject
]);
// Make an object available
// Make an object available; invoke handler's callback
mockScope.$watch.mostRecentCall.args[1](mockDomainObject);
mockHandler.handle.mostRecentCall.args[1]();
expect(controller.getModeOptions().length).toEqual(1);
@ -181,8 +182,9 @@ define(
mockDomainObject
]);
// Make an object available
// Make an object available; invoke handler's callback
mockScope.$watch.mostRecentCall.args[1](mockDomainObject);
mockHandler.handle.mostRecentCall.args[1]();
expect(controller.getModeOptions().length).toEqual(2);
});

View File

@ -30,7 +30,12 @@ define(
"use strict";
describe("A plot axis", function () {
var testMetadatas = [
var testMetadatas,
testDefault,
axis;
beforeEach(function () {
testMetadatas = [
{
tests: [
{ key: "t0", name: "T0" },
@ -52,13 +57,14 @@ define(
{ key: "t6", name: "T6" }
]
}
],
testDefault = { key: "test", name: "Test" },
controller = new PlotAxis("tests", testMetadatas, testDefault);
];
testDefault = { key: "test", name: "Test" };
axis = new PlotAxis("tests", testMetadatas, testDefault);
});
it("pulls out a list of domain or range options", function () {
// Should have filtered out duplicates, etc
expect(controller.options).toEqual([
expect(axis.options).toEqual([
{ key: "t0", name: "T0" },
{ key: "t1", name: "T1" },
{ key: "t2", name: "T2" },
@ -70,7 +76,7 @@ define(
});
it("chooses the first option as a default", function () {
expect(controller.active).toEqual({ key: "t0", name: "T0" });
expect(axis.active).toEqual({ key: "t0", name: "T0" });
});
it("falls back to a provided default if no options are present", function () {
@ -78,6 +84,26 @@ define(
.toEqual(testDefault);
});
it("allows options to be chosen by key", function () {
axis.chooseOption("t3");
expect(axis.active).toEqual({ key: "t3", name: "T3" });
});
it("reflects changes to applicable metadata", function () {
axis.updateMetadata([ testMetadatas[1] ]);
expect(axis.options).toEqual([
{ key: "t0", name: "T0" },
{ key: "t2", name: "T2" }
]);
});
it("returns the same array instance for unchanged metadata", function () {
// ...to avoid triggering extra digest cycles.
var oldInstance = axis.options;
axis.updateMetadata(testMetadatas);
expect(axis.options).toBe(oldInstance);
});
});
}
);
);

View File

@ -0,0 +1,70 @@
/*****************************************************************************
* Open MCT Web, Copyright (c) 2014-2015, United States Government
* as represented by the Administrator of the National Aeronautics and Space
* Administration. All rights reserved.
*
* Open MCT Web is licensed under the Apache License, Version 2.0 (the
* "License"); you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
* http://www.apache.org/licenses/LICENSE-2.0.
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
* WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
* License for the specific language governing permissions and limitations
* under the License.
*
* Open MCT Web includes source code licensed under additional open source
* licenses. See the Open Source Licenses file (LICENSES.md) included with
* this source code distribution or the Licensing information page available
* at runtime from the About dialog for additional information.
*****************************************************************************/
/*global define,Promise,describe,it,expect,beforeEach,waitsFor,jasmine*/
define(
["../../src/elements/PlotTelemetryFormatter"],
function (PlotTelemetryFormatter) {
'use strict';
describe("The PlotTelemetryFormatter", function () {
var mockFormatter,
formatter;
beforeEach(function () {
mockFormatter = jasmine.createSpyObj(
'telemetryFormatter',
['formatDomainValue', 'formatRangeValue']
);
formatter = new PlotTelemetryFormatter(mockFormatter);
});
describe("using domain & range format keys", function () {
var rangeFormat = "someRangeFormat",
domainFormat = "someDomainFormat";
beforeEach(function () {
formatter.setRangeFormat(rangeFormat);
formatter.setDomainFormat(domainFormat);
});
it("includes format in formatDomainValue calls", function () {
mockFormatter.formatDomainValue.andReturn("formatted!");
expect(formatter.formatDomainValue(12321))
.toEqual("formatted!");
expect(mockFormatter.formatDomainValue)
.toHaveBeenCalledWith(12321, domainFormat);
});
it("includes format in formatRangeValue calls", function () {
mockFormatter.formatRangeValue.andReturn("formatted!");
expect(formatter.formatRangeValue(12321))
.toEqual("formatted!");
expect(mockFormatter.formatRangeValue)
.toHaveBeenCalledWith(12321, rangeFormat);
});
});
});
}
);

View File

@ -14,6 +14,7 @@
"elements/PlotPosition",
"elements/PlotPreparer",
"elements/PlotSeriesWindow",
"elements/PlotTelemetryFormatter",
"elements/PlotTickGenerator",
"elements/PlotUpdater",
"modes/PlotModeOptions",

View File

@ -54,7 +54,8 @@ define(
DomainColumn.prototype.getValue = function (domainObject, datum) {
return {
text: this.telemetryFormatter.formatDomainValue(
datum[this.domainMetadata.key]
datum[this.domainMetadata.key],
this.domainMetadata.format
)
};
};

View File

@ -37,7 +37,8 @@
"services": [
{
"key": "telemetryFormatter",
"implementation": "TelemetryFormatter.js"
"implementation": "TelemetryFormatter.js",
"depends": [ "formatService", "DEFAULT_TIME_FORMAT", "$log" ]
},
{
"key": "telemetrySubscriber",
@ -63,4 +64,4 @@
}
]
}
}
}

View File

@ -36,6 +36,64 @@ define(
getRangeValue: ZERO
};
/**
* Provides metadata about telemetry associated with a
* given domain object.
*
* @typedef TelemetryMetadata
* @property {string} source the machine-readable identifier for
* the source of telemetry data for this object; used by
* {@link TelemetryService} implementations to determine
* whether or not they provide data for this object.
* @property {string} key the machine-readable identifier for
* telemetry data associated with this specific object,
* within that `source`.
* @property {TelemetryDomainMetadata[]} domains supported domain
* options for telemetry data associated with this object,
* to use in interpreting a {@link TelemetrySeries}
* @property {TelemetryRangeMetadata[]} ranges supported range
* options for telemetry data associated with this object,
* to use in interpreting a {@link TelemetrySeries}
*/
/**
* Provides metadata about range options within a telemetry series.
* Range options describe distinct properties within any given datum
* of a telemetry series; for instance, a telemetry series containing
* both raw and uncalibrated values may provide separate ranges for
* each.
*
* @typedef TelemetryRangeMetadata
* @property {string} key machine-readable identifier for this range
* @property {string} name human-readable name for this range
* @property {string} [units] human-readable units for this range
* @property {string} [format] data format for this range; usually,
* one of `number`, or `string`. If `undefined`,
* should presume to be a `number`. Custom formats
* may be indicated here.
*/
/**
* Provides metadata about domain options within a telemetry series.
* Domain options describe distinct properties within any given datum
* of a telemtry series; for instance, a telemetry series containing
* both spacecraft event time and earth received times may provide
* separate domains for each.
*
* Domains are typically used to represent timestamps in a telemetry
* series, but more generally may express any property which will
* have unique values for each datum in a series. It is this property
* which makes domains distinct from ranges, as it makes these values
* appropriate and meaningful for use to sort and bound a series.
*
* @typedef TelemetryDomainMetadata
* @property {string} key machine-readable identifier for this range
* @property {string} name human-readable name for this range
* @property {string} [system] machine-readable identifier for the
* time/date system associated with this domain;
* used by {@link DateService}
*/
/**
* A telemetry capability provides a means of requesting telemetry
* for a specific object, and for unwrapping the response (to get

View File

@ -22,14 +22,13 @@
/*global define,moment*/
define(
['moment'],
function (moment) {
[],
function () {
"use strict";
// Date format to use for domain values; in particular,
// use day-of-year instead of month/day
var DATE_FORMAT = "YYYY-DDD HH:mm:ss",
VALUE_FORMAT_DIGITS = 3;
var VALUE_FORMAT_DIGITS = 3;
/**
* The TelemetryFormatter is responsible for formatting (as text
@ -37,22 +36,33 @@ define(
* the range (usually value) of a data series.
* @memberof platform/telemetry
* @constructor
* @param {FormatService} formatService the service to user to format
* domain values
* @param {string} defaultFormatKey the format to request when no
* format has been otherwise specified
* @param $log Angular's `$log`, to log warnings
*/
function TelemetryFormatter() {
function TelemetryFormatter(formatService, defaultFormatKey, $log) {
this.formatService = formatService;
this.defaultFormat = formatService.getFormat(defaultFormatKey);
this.$log = $log;
}
/**
* Format a domain value.
* @param {number} v the domain value; a timestamp
* @param {number} v the domain value; usually, a timestamp
* in milliseconds since start of 1970
* @param {string} [key] the key which identifies the
* domain; if unspecified or unknown, this will
* be treated as a standard timestamp.
* @param {string} [key] a key which identifies the format
* to use
* @returns {string} a textual representation of the
* data and time, suitable for display.
*/
TelemetryFormatter.prototype.formatDomainValue = function (v, key) {
return isNaN(v) ? "" : moment.utc(v).format(DATE_FORMAT);
var formatter = (key === undefined) ?
this.defaultFormat :
this.formatService.getFormat(key);
return isNaN(v) ? "" : formatter ? formatter.format(v) : String(v);
};
/**

View File

@ -27,16 +27,35 @@ define(
"use strict";
describe("The telemetry formatter", function () {
var formatter;
var mockFormatService,
mockFormat,
formatter;
beforeEach(function () {
formatter = new TelemetryFormatter();
mockFormatService =
jasmine.createSpyObj("formatService", ["getFormat"]);
mockFormat = jasmine.createSpyObj("format", [
"validate",
"parse",
"format"
]);
mockFormatService.getFormat.andReturn(mockFormat);
formatter = new TelemetryFormatter(mockFormatService);
});
it("formats domains using YYYY-DDD style", function () {
expect(formatter.formatDomainValue(402513731000)).toEqual(
"1982-276 17:22:11"
);
it("formats domains using the formatService", function () {
var testValue = 12321, testResult = "some result";
mockFormat.format.andReturn(testResult);
expect(formatter.formatDomainValue(testValue))
.toEqual(testResult);
expect(mockFormat.format).toHaveBeenCalledWith(testValue);
});
it("passes format keys to the formatService", function () {
formatter.formatDomainValue(12321, "someKey");
expect(mockFormatService.getFormat)
.toHaveBeenCalledWith("someKey");
});
it("formats ranges as values", function () {
@ -44,4 +63,4 @@ define(
});
});
}
);
);