From 65043d0ff325c7e5a3d9d0bb374e90739182cccd Mon Sep 17 00:00:00 2001 From: Pete Richards Date: Wed, 12 Oct 2016 13:47:56 -0700 Subject: [PATCH] squash merge open933 into integration-1089, resolve merge conflicts. --- bower.json | 1 + example/localTimeSystem/bundle.js | 48 ++ example/localTimeSystem/src/LADTickSource.js | 43 ++ .../localTimeSystem/src/LocalTimeFormat.js | 112 +++++ .../localTimeSystem/src/LocalTimeSystem.js | 79 ++++ openmct.js | 9 +- .../browse/res/templates/browse-object.html | 3 +- .../res/templates/browse/object-header.html | 3 +- .../edit/res/templates/edit-object.html | 5 + platform/commonUI/formats/bundle.js | 17 + .../commonUI/formats/src/DurationFormat.js | 62 +++ .../commonUI/formats/src/FormatProvider.js | 4 + .../commonUI/formats/src/UTCTimeFormat.js | 64 ++- .../commonUI/formats/src/UTCTimeFormatSpec.js | 62 +++ .../general/res/sass/_animations.scss | 91 ++++ .../general/res/sass/_archetypes.scss | 3 + .../commonUI/general/res/sass/_constants.scss | 2 +- .../general/res/sass/_data-status.scss | 1 - .../commonUI/general/res/sass/_effects.scss | 10 +- .../commonUI/general/res/sass/_global.scss | 2 +- .../commonUI/general/res/sass/_icons.scss | 18 +- platform/commonUI/general/res/sass/_main.scss | 2 +- .../commonUI/general/res/sass/_mixins.scss | 17 - .../general/res/sass/controls/_controls.scss | 63 ++- .../general/res/sass/controls/_menus.scss | 73 +-- .../general/res/sass/controls/_messages.scss | 26 ++ .../res/sass/controls/_time-controller.scss | 266 ----------- .../general/res/sass/features/_imagery.scss | 2 +- .../res/sass/helpers/_wait-spinner.scss | 9 - .../res/sass/user-environ/_layout.scss | 4 +- .../templates/controls/datetime-field.html | 2 + .../themes/espresso/res/sass/_constants.scss | 1 + .../themes/snow/res/sass/_constants.scss | 1 + .../conductor-v2/compatibility/bundle.js | 66 +++ .../compatibility/src/ConductorRepresenter.js | 94 ++++ .../compatibility/src/ConductorService.js | 72 +++ .../src/ConductorTelemetryDecorator.js | 87 ++++ .../features/conductor-v2/conductor/bundle.js | 134 ++++++ .../conductor/res/sass/_constants.scss | 3 + .../res/sass/_time-conductor-base.scss | 431 ++++++++++++++++++ .../res/sass/time-conductor-espresso.scss | 39 ++ .../res/sass/time-conductor-snow.scss | 39 ++ .../templates/mode-selector/mode-menu.html | 45 ++ .../mode-selector/mode-selector.html | 34 ++ .../res/templates/time-conductor.html | 108 +++++ .../conductor/src/TimeConductor.js | 179 ++++++++ .../conductor/src/TimeConductorSpec.js | 110 +++++ .../conductor/src/timeSystems/LocalClock.js | 89 ++++ .../src/timeSystems/LocalClockSpec.js | 50 ++ .../conductor/src/timeSystems/TickSource.js | 47 ++ .../conductor/src/timeSystems/TimeSystem.js | 93 ++++ .../conductor/src/ui/MctConductorAxis.js | 146 ++++++ .../conductor/src/ui/MctConductorAxisSpec.js | 146 ++++++ .../conductor/src/ui/NumberFormat.js | 53 +++ .../conductor/src/ui/NumberFormatSpec.js | 49 ++ .../src/ui/TimeConductorController.js | 243 ++++++++++ .../src/ui/TimeConductorControllerSpec.js | 335 ++++++++++++++ .../conductor/src/ui/TimeConductorMode.js | 201 ++++++++ .../conductor/src/ui/TimeConductorModeSpec.js | 210 +++++++++ .../src/ui/TimeConductorValidation.js | 69 +++ .../src/ui/TimeConductorValidationSpec.js | 73 +++ .../src/ui/TimeConductorViewService.js | 202 ++++++++ .../src/ui/TimeConductorViewServiceSpec.js | 185 ++++++++ .../conductor-v2/utcTimeSystem/bundle.js | 40 ++ .../utcTimeSystem/src/UTCTimeSystem.js | 78 ++++ .../utcTimeSystem/src/UTCTimeSystemSpec.js | 46 ++ platform/features/conductor/bundle.js | 5 + .../conductor/res/sass/time-conductor.scss | 300 ++++++++++++ .../features/layout/res/templates/layout.html | 9 +- platform/features/plot/src/PlotController.js | 24 +- .../features/plot/test/PlotControllerSpec.js | 37 +- .../controllers/HistoricalTableController.js | 22 + .../controllers/TelemetryTableController.js | 5 - .../res/templates/controls/textfield.html | 1 + platform/forms/src/MCTControl.js | 3 + test-main.js | 6 +- 76 files changed, 4955 insertions(+), 358 deletions(-) create mode 100644 example/localTimeSystem/bundle.js create mode 100644 example/localTimeSystem/src/LADTickSource.js create mode 100644 example/localTimeSystem/src/LocalTimeFormat.js create mode 100644 example/localTimeSystem/src/LocalTimeSystem.js create mode 100644 platform/commonUI/formats/src/DurationFormat.js create mode 100644 platform/commonUI/formats/src/UTCTimeFormatSpec.js create mode 100644 platform/commonUI/general/res/sass/_animations.scss delete mode 100644 platform/commonUI/general/res/sass/controls/_time-controller.scss create mode 100644 platform/features/conductor-v2/compatibility/bundle.js create mode 100644 platform/features/conductor-v2/compatibility/src/ConductorRepresenter.js create mode 100644 platform/features/conductor-v2/compatibility/src/ConductorService.js create mode 100644 platform/features/conductor-v2/compatibility/src/ConductorTelemetryDecorator.js create mode 100644 platform/features/conductor-v2/conductor/bundle.js create mode 100644 platform/features/conductor-v2/conductor/res/sass/_constants.scss create mode 100644 platform/features/conductor-v2/conductor/res/sass/_time-conductor-base.scss create mode 100644 platform/features/conductor-v2/conductor/res/sass/time-conductor-espresso.scss create mode 100644 platform/features/conductor-v2/conductor/res/sass/time-conductor-snow.scss create mode 100644 platform/features/conductor-v2/conductor/res/templates/mode-selector/mode-menu.html create mode 100644 platform/features/conductor-v2/conductor/res/templates/mode-selector/mode-selector.html create mode 100644 platform/features/conductor-v2/conductor/res/templates/time-conductor.html create mode 100644 platform/features/conductor-v2/conductor/src/TimeConductor.js create mode 100644 platform/features/conductor-v2/conductor/src/TimeConductorSpec.js create mode 100644 platform/features/conductor-v2/conductor/src/timeSystems/LocalClock.js create mode 100644 platform/features/conductor-v2/conductor/src/timeSystems/LocalClockSpec.js create mode 100644 platform/features/conductor-v2/conductor/src/timeSystems/TickSource.js create mode 100644 platform/features/conductor-v2/conductor/src/timeSystems/TimeSystem.js create mode 100644 platform/features/conductor-v2/conductor/src/ui/MctConductorAxis.js create mode 100644 platform/features/conductor-v2/conductor/src/ui/MctConductorAxisSpec.js create mode 100644 platform/features/conductor-v2/conductor/src/ui/NumberFormat.js create mode 100644 platform/features/conductor-v2/conductor/src/ui/NumberFormatSpec.js create mode 100644 platform/features/conductor-v2/conductor/src/ui/TimeConductorController.js create mode 100644 platform/features/conductor-v2/conductor/src/ui/TimeConductorControllerSpec.js create mode 100644 platform/features/conductor-v2/conductor/src/ui/TimeConductorMode.js create mode 100644 platform/features/conductor-v2/conductor/src/ui/TimeConductorModeSpec.js create mode 100644 platform/features/conductor-v2/conductor/src/ui/TimeConductorValidation.js create mode 100644 platform/features/conductor-v2/conductor/src/ui/TimeConductorValidationSpec.js create mode 100644 platform/features/conductor-v2/conductor/src/ui/TimeConductorViewService.js create mode 100644 platform/features/conductor-v2/conductor/src/ui/TimeConductorViewServiceSpec.js create mode 100644 platform/features/conductor-v2/utcTimeSystem/bundle.js create mode 100644 platform/features/conductor-v2/utcTimeSystem/src/UTCTimeSystem.js create mode 100644 platform/features/conductor-v2/utcTimeSystem/src/UTCTimeSystemSpec.js create mode 100644 platform/features/conductor/res/sass/time-conductor.scss diff --git a/bower.json b/bower.json index ed3dbaf899..161ee04186 100644 --- a/bower.json +++ b/bower.json @@ -22,6 +22,7 @@ "eventemitter3": "^1.2.0", "lodash": "3.10.1", "almond": "~0.3.2", + "d3": "~4.1.0", "html2canvas": "^0.4.1" } } diff --git a/example/localTimeSystem/bundle.js b/example/localTimeSystem/bundle.js new file mode 100644 index 0000000000..1d1ff2cf39 --- /dev/null +++ b/example/localTimeSystem/bundle.js @@ -0,0 +1,48 @@ +/***************************************************************************** + * 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. + *****************************************************************************/ + +define([ + "./src/LocalTimeSystem", + "./src/LocalTimeFormat", + 'legacyRegistry' +], function ( + LocalTimeSystem, + LocalTimeFormat, + legacyRegistry +) { + legacyRegistry.register("example/localTimeSystem", { + "extensions": { + "formats": [ + { + "key": "local-format", + "implementation": LocalTimeFormat + } + ], + "timeSystems": [ + { + "implementation": LocalTimeSystem, + "depends": ["$timeout"] + } + ] + } + }); +}); diff --git a/example/localTimeSystem/src/LADTickSource.js b/example/localTimeSystem/src/LADTickSource.js new file mode 100644 index 0000000000..35fff32f54 --- /dev/null +++ b/example/localTimeSystem/src/LADTickSource.js @@ -0,0 +1,43 @@ +/***************************************************************************** + * 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. + *****************************************************************************/ + +define(['../../../platform/features/conductor-v2/conductor/src/timeSystems/LocalClock'], function (LocalClock) { + /** + * @implements TickSource + * @constructor + */ + function LADTickSource ($timeout, period) { + LocalClock.call(this, $timeout, period); + + this.metadata = { + key: 'test-lad', + mode: 'lad', + cssclass: 'icon-clock', + label: 'Latest Available Data', + name: 'Latest available data', + description: 'Monitor real-time streaming data as it comes in. The Time Conductor and displays will automatically advance themselves based on a UTC clock.' + }; + } + LADTickSource.prototype = Object.create(LocalClock.prototype); + + return LADTickSource; +}); diff --git a/example/localTimeSystem/src/LocalTimeFormat.js b/example/localTimeSystem/src/LocalTimeFormat.js new file mode 100644 index 0000000000..a9b26ed4ef --- /dev/null +++ b/example/localTimeSystem/src/LocalTimeFormat.js @@ -0,0 +1,112 @@ +/***************************************************************************** + * 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. + *****************************************************************************/ + +define([ + 'moment' +], function ( + moment +) { + + var DATE_FORMAT = "YYYY-MM-DD h:mm:ss.SSS a", + DATE_FORMATS = [ + DATE_FORMAT, + "YYYY-MM-DD h:mm:ss a", + "YYYY-MM-DD h:mm a", + "YYYY-MM-DD" + ]; + + /** + * @typedef Scale + * @property {number} min the minimum scale value, in ms + * @property {number} max the maximum scale value, in ms + */ + + /** + * Formatter for UTC timestamps. Interprets numeric values as + * milliseconds since the start of 1970. + * + * @implements {Format} + * @constructor + * @memberof platform/commonUI/formats + */ + function LocalTimeFormat() { + } + + /** + * Returns an appropriate time format based on the provided value and + * the threshold required. + * @private + */ + function getScaledFormat (d) { + var m = moment.utc(d); + /** + * Uses logic from d3 Time-Scales, v3 of the API. See + * https://github.com/d3/d3-3.x-api-reference/blob/master/Time-Scales.md + * + * Licensed + */ + return [ + [".SSS", function(m) { return m.milliseconds(); }], + [":ss", function(m) { return m.seconds(); }], + ["hh:mma", function(m) { return m.minutes(); }], + ["hha", function(m) { return m.hours(); }], + ["ddd DD", function(m) { + return m.days() && + m.date() != 1; + }], + ["MMM DD", function(m) { return m.date() != 1; }], + ["MMMM", function(m) { + return m.month(); + }], + ["YYYY", function() { return true; }] + ].filter(function (row){ + return row[1](m); + })[0][0]; + }; + + /** + * + * @param value + * @param {Scale} [scale] Optionally provides context to the + * format request, allowing for scale-appropriate formatting. + * @returns {string} the formatted date + */ + LocalTimeFormat.prototype.format = function (value, scale) { + if (scale !== undefined){ + var scaledFormat = getScaledFormat(value, scale); + if (scaledFormat) { + return moment.utc(value).format(scaledFormat); + } + } + return moment(value).format(DATE_FORMAT); + }; + + LocalTimeFormat.prototype.parse = function (text) { + return moment(text, DATE_FORMATS).valueOf(); + }; + + LocalTimeFormat.prototype.validate = function (text) { + return moment(text, DATE_FORMATS).isValid(); + }; + + return LocalTimeFormat; +}); diff --git a/example/localTimeSystem/src/LocalTimeSystem.js b/example/localTimeSystem/src/LocalTimeSystem.js new file mode 100644 index 0000000000..95485b7e69 --- /dev/null +++ b/example/localTimeSystem/src/LocalTimeSystem.js @@ -0,0 +1,79 @@ +/***************************************************************************** + * 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. + *****************************************************************************/ + +define([ + '../../../platform/features/conductor-v2/conductor/src/timeSystems/TimeSystem', + '../../../platform/features/conductor-v2/conductor/src/timeSystems/LocalClock', + './LADTickSource' +], function (TimeSystem, LocalClock, LADTickSource) { + var THIRTY_MINUTES = 30 * 60 * 1000, + DEFAULT_PERIOD = 1000; + + /** + * This time system supports UTC dates and provides a ticking clock source. + * @implements TimeSystem + * @constructor + */ + function LocalTimeSystem ($timeout) { + TimeSystem.call(this); + + /** + * Some metadata, which will be used to identify the time system in + * the UI + * @type {{key: string, name: string, glyph: string}} + */ + this.metadata = { + 'key': 'local', + 'name': 'Local', + 'glyph': '\u0043' + }; + + this.fmts = ['local-format']; + this.sources = [new LocalClock($timeout, DEFAULT_PERIOD), new LADTickSource($timeout, DEFAULT_PERIOD)]; + } + + LocalTimeSystem.prototype = Object.create(TimeSystem.prototype); + + LocalTimeSystem.prototype.formats = function () { + return this.fmts; + }; + + LocalTimeSystem.prototype.deltaFormat = function () { + return 'duration'; + }; + + LocalTimeSystem.prototype.tickSources = function () { + return this.sources; + }; + + LocalTimeSystem.prototype.defaults = function (key) { + var now = Math.ceil(Date.now() / 1000) * 1000; + return { + key: 'local-default', + name: 'Local 12 hour time system defaults', + deltas: {start: THIRTY_MINUTES, end: 0}, + bounds: {start: now - THIRTY_MINUTES, end: now} + }; + }; + + return LocalTimeSystem; +}); diff --git a/openmct.js b/openmct.js index 864bf243cd..f1bb593860 100644 --- a/openmct.js +++ b/openmct.js @@ -37,7 +37,8 @@ requirejs.config({ "text": "bower_components/text/text", "uuid": "bower_components/node-uuid/uuid", "zepto": "bower_components/zepto/zepto.min", - "lodash": "bower_components/lodash/lodash" + "lodash": "bower_components/lodash/lodash", + "d3": "bower_components/d3/d3.min" }, "shim": { "angular": { @@ -52,6 +53,9 @@ requirejs.config({ "html2canvas": { "exports": "html2canvas" }, + "EventEmitter": { + "exports": "EventEmitter" + }, "moment-duration-format": { "deps": ["moment"] }, @@ -63,6 +67,9 @@ requirejs.config({ }, "lodash": { "exports": "lodash" + }, + "d3": { + "exports": "d3" } } }); diff --git a/platform/commonUI/browse/res/templates/browse-object.html b/platform/commonUI/browse/res/templates/browse-object.html index eb32a15648..f431003e47 100644 --- a/platform/commonUI/browse/res/templates/browse-object.html +++ b/platform/commonUI/browse/res/templates/browse-object.html @@ -43,7 +43,7 @@ -
+
@@ -59,4 +59,5 @@
+
diff --git a/platform/commonUI/browse/res/templates/browse/object-header.html b/platform/commonUI/browse/res/templates/browse/object-header.html index 48a64796da..62391caac5 100644 --- a/platform/commonUI/browse/res/templates/browse/object-header.html +++ b/platform/commonUI/browse/res/templates/browse/object-header.html @@ -22,7 +22,8 @@ {{parameters.mode}} - {{model.name}} + {{model.name}} +
+ + + diff --git a/platform/commonUI/formats/bundle.js b/platform/commonUI/formats/bundle.js index 040c7f57fb..c685151149 100644 --- a/platform/commonUI/formats/bundle.js +++ b/platform/commonUI/formats/bundle.js @@ -23,10 +23,12 @@ define([ "./src/FormatProvider", "./src/UTCTimeFormat", + "./src/DurationFormat", 'legacyRegistry' ], function ( FormatProvider, UTCTimeFormat, + DurationFormat, legacyRegistry ) { @@ -48,6 +50,10 @@ define([ { "key": "utc", "implementation": UTCTimeFormat + }, + { + "key": "duration", + "implementation": DurationFormat } ], "constants": [ @@ -55,6 +61,17 @@ define([ "key": "DEFAULT_TIME_FORMAT", "value": "utc" } + ], + "licenses": [ + { + "name": "d3", + "version": "3.0.0", + "description": "Incorporates modified code from d3 Time Scales", + "author": "Mike Bostock", + "copyright": "Copyright 2010-2016 Mike Bostock. " + + "All rights reserved.", + "link": "https://github.com/d3/d3/blob/master/LICENSE" + } ] } }); diff --git a/platform/commonUI/formats/src/DurationFormat.js b/platform/commonUI/formats/src/DurationFormat.js new file mode 100644 index 0000000000..12ce3b3c6b --- /dev/null +++ b/platform/commonUI/formats/src/DurationFormat.js @@ -0,0 +1,62 @@ +/***************************************************************************** + * 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. + *****************************************************************************/ + +define([ + 'moment' +], function ( + moment +) { + + var DATE_FORMAT = "HH:mm:ss", + DATE_FORMATS = [ + DATE_FORMAT + ]; + + + /** + * Formatter for duration. Uses moment to produce a date from a given + * value, but output is formatted to display only time. Can be used for + * specifying a time duration. For specifying duration, it's best to + * specify a date of January 1, 1970, as the ms offset will equal the + * duration represented by the time. + * + * @implements {Format} + * @constructor + * @memberof platform/commonUI/formats + */ + function DurationFormat() { + } + + DurationFormat.prototype.format = function (value) { + return moment.utc(value).format(DATE_FORMAT); + }; + + DurationFormat.prototype.parse = function (text) { + return moment.duration(text).asMilliseconds(); + }; + + DurationFormat.prototype.validate = function (text) { + return moment.utc(text, DATE_FORMATS).isValid(); + }; + + return DurationFormat; +}); diff --git a/platform/commonUI/formats/src/FormatProvider.js b/platform/commonUI/formats/src/FormatProvider.js index f8f126001d..4700260fd8 100644 --- a/platform/commonUI/formats/src/FormatProvider.js +++ b/platform/commonUI/formats/src/FormatProvider.js @@ -58,6 +58,10 @@ define([ * @method format * @memberof Format# * @param {number} value the numeric value to format + * @param {number} [threshold] Optionally provides context to the + * format request, allowing for scale-appropriate formatting. This value + * should be the minimum unit to be represented by this format, in ms. For + * example, to display seconds, a threshold of 1 * 1000 should be provided. * @returns {string} the text representation of the value */ diff --git a/platform/commonUI/formats/src/UTCTimeFormat.js b/platform/commonUI/formats/src/UTCTimeFormat.js index 8c4277f9b8..3faab7a620 100644 --- a/platform/commonUI/formats/src/UTCTimeFormat.js +++ b/platform/commonUI/formats/src/UTCTimeFormat.js @@ -34,6 +34,11 @@ define([ "YYYY-MM-DD" ]; + /** + * @typedef Scale + * @property {number} min the minimum scale value, in ms + * @property {number} max the maximum scale value, in ms + */ /** * Formatter for UTC timestamps. Interprets numeric values as @@ -46,7 +51,64 @@ define([ function UTCTimeFormat() { } - UTCTimeFormat.prototype.format = function (value) { + /** + * Returns an appropriate time format based on the provided value and + * the threshold required. + * @private + */ + function getScaledFormat(d) { + var momentified = moment.utc(d); + /** + * Uses logic from d3 Time-Scales, v3 of the API. See + * https://github.com/d3/d3-3.x-api-reference/blob/master/Time-Scales.md + * + * Licensed + */ + return [ + [".SSS", function (m) { + return m.milliseconds(); + }], + [":ss", function (m) { + return m.seconds(); + }], + ["HH:mm", function (m) { + return m.minutes(); + }], + ["HH", function (m) { + return m.hours(); + }], + ["ddd DD", function (m) { + return m.days() && + m.date() !== 1; + }], + ["MMM DD", function (m) { + return m.date() !== 1; + }], + ["MMMM", function (m) { + return m.month(); + }], + ["YYYY", function () { + return true; + }] + ].filter(function (row) { + return row[1](momentified); + })[0][0]; + } + + /** + * + * @param value + * @param {Scale} [scale] Optionally provides context to the + * format request, allowing for scale-appropriate formatting. + * @returns {string} the formatted date + */ + UTCTimeFormat.prototype.format = function (value, scale) { + if (scale !== undefined) { + var scaledFormat = getScaledFormat(value, scale); + if (scaledFormat) { + return moment.utc(value).format(scaledFormat); + } + } return moment.utc(value).format(DATE_FORMAT) + "Z"; }; diff --git a/platform/commonUI/formats/src/UTCTimeFormatSpec.js b/platform/commonUI/formats/src/UTCTimeFormatSpec.js new file mode 100644 index 0000000000..c4111709a3 --- /dev/null +++ b/platform/commonUI/formats/src/UTCTimeFormatSpec.js @@ -0,0 +1,62 @@ +/***************************************************************************** + * 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. + *****************************************************************************/ + +define([ + "./UTCTimeFormat", + "moment" +], function ( + UTCTimeFormat, + moment +) { + describe("The UTCTimeFormat class", function () { + var format; + var scale; + + beforeEach(function () { + format = new UTCTimeFormat(); + scale = {min: 0, max: 0}; + }); + + it("Provides an appropriately scaled time format based on the input" + + " time", function () { + var TWO_HUNDRED_MS = 200; + var THREE_SECONDS = 3000; + var FIVE_MINUTES = 5 * 60 * 1000; + var ONE_HOUR_TWENTY_MINS = (1 * 60 * 60 * 1000) + (20 * 60 * 1000); + var TEN_HOURS = (10 * 60 * 60 * 1000); + + var JUNE_THIRD = moment.utc("2016-06-03", "YYYY-MM-DD"); + var APRIL = moment.utc("2016-04", "YYYY-MM"); + var TWENTY_SIXTEEN = moment.utc("2016", "YYYY"); + + expect(format.format(TWO_HUNDRED_MS, scale)).toBe(".200"); + expect(format.format(THREE_SECONDS, scale)).toBe(":03"); + expect(format.format(FIVE_MINUTES, scale)).toBe("00:05"); + expect(format.format(ONE_HOUR_TWENTY_MINS, scale)).toBe("01:20"); + expect(format.format(TEN_HOURS, scale)).toBe("10"); + + expect(format.format(JUNE_THIRD, scale)).toBe("Fri 03"); + expect(format.format(APRIL, scale)).toBe("April"); + expect(format.format(TWENTY_SIXTEEN, scale)).toBe("2016"); + }); + }); +}); diff --git a/platform/commonUI/general/res/sass/_animations.scss b/platform/commonUI/general/res/sass/_animations.scss new file mode 100644 index 0000000000..fb1c0f1bfb --- /dev/null +++ b/platform/commonUI/general/res/sass/_animations.scss @@ -0,0 +1,91 @@ +@include keyframes(rotation) { + 100% { @include transform(rotate(360deg)); } +} + +@include keyframes(rotation-centered) { + 0% { @include transform(translate(-50%, -50%) rotate(0deg)); } + 100% { @include transform(translate(-50%, -50%) rotate(360deg)); } +} + +@include keyframes(clock-hands) { + 0% { @include transform(translate(-50%, -50%) rotate(0deg)); } + 100% { @include transform(translate(-50%, -50%) rotate(360deg)); } +} + +@include keyframes(clock-hands-sticky) { + 0% { + @include transform(translate(-50%, -50%) rotate(0deg)); + } + 7% { + @include transform(translate(-50%, -50%) rotate(0deg)); + } + 8% { + @include transform(translate(-50%, -50%) rotate(30deg)); + } + 15% { + @include transform(translate(-50%, -50%) rotate(30deg)); + } + 16% { + @include transform(translate(-50%, -50%) rotate(60deg)); + } + 24% { + @include transform(translate(-50%, -50%) rotate(60deg)); + } + 25% { + @include transform(translate(-50%, -50%) rotate(90deg)); + } + 32% { + @include transform(translate(-50%, -50%) rotate(90deg)); + } + 33% { + @include transform(translate(-50%, -50%) rotate(120deg)); + } + 40% { + @include transform(translate(-50%, -50%) rotate(120deg)); + } + 41% { + @include transform(translate(-50%, -50%) rotate(150deg)); + } + 49% { + @include transform(translate(-50%, -50%) rotate(150deg)); + } + 50% { + @include transform(translate(-50%, -50%) rotate(180deg)); + } + 57% { + @include transform(translate(-50%, -50%) rotate(180deg)); + } + 58% { + @include transform(translate(-50%, -50%) rotate(210deg)); + } + 65% { + @include transform(translate(-50%, -50%) rotate(210deg)); + } + 66% { + @include transform(translate(-50%, -50%) rotate(240deg)); + } + 74% { + @include transform(translate(-50%, -50%) rotate(240deg)); + } + 75% { + @include transform(translate(-50%, -50%) rotate(270deg)); + } + 82% { + @include transform(translate(-50%, -50%) rotate(270deg)); + } + 83% { + @include transform(translate(-50%, -50%) rotate(300deg)); + } + 90% { + @include transform(translate(-50%, -50%) rotate(300deg)); + } + 91% { + @include transform(translate(-50%, -50%) rotate(330deg)); + } + 99% { + @include transform(translate(-50%, -50%) rotate(330deg)); + } + 100% { + @include transform(translate(-50%, -50%) rotate(360deg)); + } +} \ No newline at end of file diff --git a/platform/commonUI/general/res/sass/_archetypes.scss b/platform/commonUI/general/res/sass/_archetypes.scss index d51944e2f4..206f126fde 100644 --- a/platform/commonUI/general/res/sass/_archetypes.scss +++ b/platform/commonUI/general/res/sass/_archetypes.scss @@ -108,6 +108,9 @@ &.grows { @include flex(1 1 auto); } + &.contents-align-right { + text-align: right; + } } .flex-container { // Apply to wrapping elements, mct-includes, etc. diff --git a/platform/commonUI/general/res/sass/_constants.scss b/platform/commonUI/general/res/sass/_constants.scss index 1e609cc038..efa87c6a08 100644 --- a/platform/commonUI/general/res/sass/_constants.scss +++ b/platform/commonUI/general/res/sass/_constants.scss @@ -49,7 +49,6 @@ $uePaneMiniTabFontSize: 8px; $uePaneMiniTabCollapsedW: 18px; $ueEditLeftPaneW: 75%; $treeSearchInputBarH: 25px; -$ueTimeControlH: (33px, 18px, 20px); /*************** Panes */ $ueBrowseLeftPaneTreeMinW: 150px; $ueBrowseLeftPaneTreeMaxW: 35%; @@ -112,6 +111,7 @@ $bubbleMaxW: 300px; $reqSymbolW: 15px; $reqSymbolM: $interiorMargin * 2; $reqSymbolFontSize: 0.75em; +$inputTextP: 3px 5px; /*************** Wait Spinner Defaults */ $waitSpinnerD: 32px; $waitSpinnerTreeD: 20px; diff --git a/platform/commonUI/general/res/sass/_data-status.scss b/platform/commonUI/general/res/sass/_data-status.scss index 24a15db794..d058885cf1 100644 --- a/platform/commonUI/general/res/sass/_data-status.scss +++ b/platform/commonUI/general/res/sass/_data-status.scss @@ -4,4 +4,3 @@ @include s-stale(); } } - diff --git a/platform/commonUI/general/res/sass/_effects.scss b/platform/commonUI/general/res/sass/_effects.scss index 22a5443294..acdda74b55 100644 --- a/platform/commonUI/general/res/sass/_effects.scss +++ b/platform/commonUI/general/res/sass/_effects.scss @@ -39,20 +39,20 @@ @include pulse($animName: pulse-subtle, $dur: 500ms, $opacity0: 0.7); } -@mixin animTo($animName, $propName, $propValStart, $propValEnd, $dur: 500ms, $delay: 0) { +@mixin animTo($animName, $propName, $propValStart, $propValEnd, $dur: 500ms, $delay: 0, $dir: normal, $count: 1) { @include keyframes($animName) { from { #{propName}: $propValStart; } to { #{$propName}: $propValEnd; } } - @include animToParams($animName, $dur: 500ms, $delay: 0) + @include animToParams($animName, $dur: $dur, $delay: $delay, $dir: $dir, $count: $count) } -@mixin animToParams($animName, $dur: 500ms, $delay: 0) { +@mixin animToParams($animName, $dur: 500ms, $delay: 0, $dir: normal, $count: 1) { @include animation-name($animName); @include animation-duration($dur); @include animation-delay($delay); @include animation-fill-mode(both); - @include animation-direction(normal); - @include animation-iteration-count(1); + @include animation-direction($dir); + @include animation-iteration-count($count); @include animation-timing-function(ease-in-out); } \ No newline at end of file diff --git a/platform/commonUI/general/res/sass/_global.scss b/platform/commonUI/general/res/sass/_global.scss index 9d3627f3aa..964d609c4f 100644 --- a/platform/commonUI/general/res/sass/_global.scss +++ b/platform/commonUI/general/res/sass/_global.scss @@ -82,7 +82,7 @@ input, textarea { input[type="text"], input[type="search"] { vertical-align: baseline; - padding: 3px 5px; + padding: $inputTextP; } h1, h2, h3 { diff --git a/platform/commonUI/general/res/sass/_icons.scss b/platform/commonUI/general/res/sass/_icons.scss index a8990d4fe8..d15ec1d362 100644 --- a/platform/commonUI/general/res/sass/_icons.scss +++ b/platform/commonUI/general/res/sass/_icons.scss @@ -30,6 +30,7 @@ .ui-symbol { font-family: 'symbolsfont'; + -webkit-font-smoothing: antialiased; } .ui-symbol.icon { @@ -70,10 +71,21 @@ line-height: inherit; position: relative; &.l-icon-link { - .t-item-icon-glyph { + &:after { + color: $colorIconLink; + content: $glyph-icon-link; + height: auto; width: auto; + position: absolute; + left: 0; top: 0; right: 0; bottom: 20%; + @include transform-origin(bottom left); + @include transform(scale(0.3)); + z-index: 2; + } + +/* .t-item-icon-glyph { &:after { color: $colorIconLink; - content: $glyph-icon-link; + content: '\e921'; //$glyph-icon-link; height: auto; width: auto; position: absolute; left: 0; top: 0; right: 0; bottom: 20%; @@ -81,6 +93,6 @@ @include transform(scale(0.3)); z-index: 2; } - } + }*/ } } diff --git a/platform/commonUI/general/res/sass/_main.scss b/platform/commonUI/general/res/sass/_main.scss index e092f1474a..3ecb2304ab 100644 --- a/platform/commonUI/general/res/sass/_main.scss +++ b/platform/commonUI/general/res/sass/_main.scss @@ -22,6 +22,7 @@ @import "effects"; @import "global"; @import "glyphs"; +@import "animations"; @import "archetypes"; @import "about"; @import "text"; @@ -41,7 +42,6 @@ @import "controls/lists"; @import "controls/menus"; @import "controls/messages"; -@import "controls/time-controller"; @import "mobile/controls/menus"; /********************************* FORMS */ diff --git a/platform/commonUI/general/res/sass/_mixins.scss b/platform/commonUI/general/res/sass/_mixins.scss index 318bfff495..91720348b9 100644 --- a/platform/commonUI/general/res/sass/_mixins.scss +++ b/platform/commonUI/general/res/sass/_mixins.scss @@ -185,21 +185,15 @@ } @mixin sliderTrack($bg: $scrollbarTrackColorBg) { - //$b: 1px solid lighten($bg, 30%); border-radius: 2px; box-sizing: border-box; @include boxIncised(0.7); background-color: $bg; - //border-bottom: $b; - //border-right: $b; } @mixin controlGrippy($b, $direction: horizontal, $w: 1px, $style: dotted) { - //&:before { - //@include trans-prop-nice("border-color", 25ms); content: ''; display: block; - //height: auto; pointer-events: none; position: absolute; z-index: 2; @@ -274,16 +268,6 @@ text-shadow: rgba(black, $sVal) 0 3px 7px; } -@function pullForward($c, $p: 20%) { - // For dark interfaces, lighter things come forward - @return lighten($c, $p); -} - -@function pushBack($c, $p: 20%) { - // For dark interfaces, darker things move back - @return darken($c, $p); -} - @function percentToDecimal($p) { @return $p / 100%; } @@ -304,7 +288,6 @@ border-radius: $controlCr; box-sizing: border-box; color: $fg; - //display: inline-block; } @mixin btnBase($bg: $colorBtnBg, $bgHov: $colorBtnBgHov, $fg: $colorBtnFg, $fgHov: $colorBtnFgHov, $ic: $colorBtnIcon, $icHov: $colorBtnIconHov) { diff --git a/platform/commonUI/general/res/sass/controls/_controls.scss b/platform/commonUI/general/res/sass/controls/_controls.scss index d1935d8ce0..ad32ea0515 100644 --- a/platform/commonUI/general/res/sass/controls/_controls.scss +++ b/platform/commonUI/general/res/sass/controls/_controls.scss @@ -296,8 +296,6 @@ input[type="search"] { .title-label { color: $colorObjHdrTxt; @include ellipsize(); - @include webkitProp(flex, '0 1 auto'); - padding-right: 0.35em; // For context arrow. Done with em's so pad is relative to the scale of the text. } .context-available-w { @@ -308,6 +306,10 @@ input[type="search"] { font-size: 0.7em; @include flex(0 0 1); } + + .t-object-alert { + display: none; + } } /******************************************************** PROGRESS BAR */ @@ -441,6 +443,63 @@ input[type="search"] { } } +@mixin sliderKnob() { + $h: 16px; + cursor: pointer; + width: floor($h/1.75); + height: $h; + margin-top: 1 + floor($h/2) * -1; + @include btnSubtle(pullForward($colorBtnBg, 10%)); + //border-radius: 50% !important; +} + +@mixin sliderKnobRound() { + $h: 12px; + cursor: pointer; + width: $h; + height: $h; + margin-top: 1 + floor($h/2) * -1; + @include btnSubtle(pullForward($colorBtnBg, 10%)); + border-radius: 50% !important; +} + +input[type="range"] { + // HTML5 range inputs + + -webkit-appearance: none; /* Hides the slider so that custom slider can be made */ + background: transparent; /* Otherwise white in Chrome */ + &:focus { + outline: none; /* Removes the blue border. */ + } + + // Thumb + &::-webkit-slider-thumb { + -webkit-appearance: none; + @include sliderKnobRound(); + } + &::-moz-range-thumb { + border: none; + @include sliderKnobRound(); + } + &::-ms-thumb { + border: none; + @include sliderKnobRound(); + } + + // Track + &::-webkit-slider-runnable-track { + width: 100%; + height: 3px; + @include sliderTrack(); + } + + &::-moz-range-track { + width: 100%; + height: 3px; + @include sliderTrack(); + } +} + /******************************************************** DATETIME PICKER */ .l-datetime-picker { $r1H: 15px; diff --git a/platform/commonUI/general/res/sass/controls/_menus.scss b/platform/commonUI/general/res/sass/controls/_menus.scss index 61e87b5b2b..def9c3d3bb 100644 --- a/platform/commonUI/general/res/sass/controls/_menus.scss +++ b/platform/commonUI/general/res/sass/controls/_menus.scss @@ -178,7 +178,7 @@ } .pane { box-sizing: border-box; - &.left { + &.menu-items { border-right: 1px solid pullForward($colorMenuBg, 10%); left: 0; padding-right: $interiorMargin; @@ -194,38 +194,53 @@ } } } - &.right { + &.menu-item-description { left: auto; right: 0; padding: $interiorMargin * 5; width: $prw; + .desc-area { + &.icon { + color: $colorCreateMenuLgIcon; + font-size: 8em; + margin-bottom: $interiorMargin * 3; + position: relative; + text-align: center; + } + &.title { + color: $colorCreateMenuText; + font-size: 1.2em; + margin-bottom: $interiorMargin * 2; + } + &.description { + color: pushBack($colorCreateMenuText, 20%); + font-size: 0.8em; + line-height: 1.5em; + } + } } } - .menu-item-description { - .desc-area { - &.icon { - $h: 150px; - color: $colorCreateMenuLgIcon; - position: relative; - font-size: 8em; - left: 0; - height: $h; - line-height: $h; - margin-bottom: $interiorMargin * 5; - text-align: center; - } - &.title { - color: $colorCreateMenuText; - font-size: 1.2em; - margin-bottom: 0.5em; - } - &.description { - color: $colorCreateMenuText; - font-size: 0.8em; - line-height: 1.5em; - } - } - } + + &.mini { + width: 400px; + height: 300px; + .pane { + &.menu-items { + font-size: 0.8em; + } + &.menu-item-description { + padding: $interiorMargin * 3; + .desc-area { + &.icon { + font-size: 4em; + } + &.title { + font-size: 1em; + } + } + } + } + } } .context-menu { font-size: 0.80rem; @@ -262,3 +277,7 @@ right: 0; width: auto; } + +.menus-up .menu { + bottom: $btnStdH; top: auto; +} diff --git a/platform/commonUI/general/res/sass/controls/_messages.scss b/platform/commonUI/general/res/sass/controls/_messages.scss index 2604d498bd..485202c252 100644 --- a/platform/commonUI/general/res/sass/controls/_messages.scss +++ b/platform/commonUI/general/res/sass/controls/_messages.scss @@ -345,3 +345,29 @@ body.desktop .t-message-single { body.desktop .t-message-list { .message-contents .l-message { margin-right: $interiorMarginLg; } } + +// Alert elements in views +.s-unsynced { + $c: $colorPausedBg; + border: 1px solid $c; + @include animTo($animName: pulsePaused, $propName: border-color, $propValStart: rgba($c, 0.8), $propValEnd: rgba($c, 0.5), $dur: $animPausedPulseDur, $dir: alternate, $count: infinite); +} + +.s-status-timeconductor-unsynced { + // Plot areas + .gl-plot .gl-plot-display-area { + @extend .s-unsynced; + } + + // Object headers + .object-header { + .t-object-alert { + display: inline; + &.t-alert-unsynced { + @extend .icon-alert-triangle; + color: $colorPausedBg; + } + } + } +} + diff --git a/platform/commonUI/general/res/sass/controls/_time-controller.scss b/platform/commonUI/general/res/sass/controls/_time-controller.scss deleted file mode 100644 index ba2cf5b3ee..0000000000 --- a/platform/commonUI/general/res/sass/controls/_time-controller.scss +++ /dev/null @@ -1,266 +0,0 @@ -@mixin toiLineHovEffects() { - &:before, - &:after { - background-color: $timeControllerToiLineColorHov; - } -} - -.l-time-controller { - $minW: 500px; - $knobHOffset: 0px; - $knobM: ($sliderKnobW + $knobHOffset) * -1; - $rangeValPad: $interiorMargin; - $rangeValOffset: $sliderKnobW + $interiorMargin; - $timeRangeSliderLROffset: 150px + ($sliderKnobW * 2); - $r1H: nth($ueTimeControlH,1); // Not currently used - $r2H: nth($ueTimeControlH,2); - $r3H: nth($ueTimeControlH,3); - - min-width: $minW; - font-size: 0.8rem; - - .l-time-range-inputs-holder, - .l-time-range-slider-holder, - .l-time-range-ticks-holder - { - box-sizing: border-box; - position: relative; - &:not(:first-child) { - margin-top: $interiorMargin; - } - } - .l-time-range-slider, - .l-time-range-ticks { - @include absPosDefault(0, visible); - left: $timeRangeSliderLROffset; right: $timeRangeSliderLROffset; - } - - .l-time-range-inputs-holder { - border-top: 1px solid $colorInteriorBorder; - padding-top: $interiorMargin; - &.l-flex-row, - .l-flex-row { - @include align-items(center); - .flex-elem { - height: auto; - line-height: normal; - } - } - .type-icon { - font-size: 120%; - vertical-align: middle; - } - .l-time-range-input-w, - .l-time-range-inputs-elem { - margin-right: $interiorMargin; - .lbl { - color: $colorPlotLabelFg; - } - .ui-symbol.icon { - font-size: 11px; - } - } - .l-time-range-input-w { - // Wraps a datetime text input field - position: relative; - input[type="text"] { - width: 200px; - &.picker-icon { - padding-right: 20px; - } - } - .icon-calendar { - position: absolute; - right: 5px; - top: 5px; - } - } - } - - .l-time-range-slider-holder { - height: $r2H; - .range-holder { - box-shadow: none; - background: none; - border: none; - .range { - .toi-line { - $myC: $timeControllerToiLineColor; - $myW: 8px; - @include transform(translateX(50%)); - position: absolute; - top: 0; right: 0; bottom: 0px; left: auto; - width: $myW; - height: auto; - z-index: 2; - &:before { - // Vert line - background-color: $myC; - position: absolute; - content: ""; - top: 0; right: auto; bottom: -10px; left: floor($myW/2) - 1; - width: 1px; - } - } - &:hover .toi-line { - @include toiLineHovEffects; - } - } - } - &:not(:active) { - .knob, - .range { - @include transition-property(left, right); - @include transition-duration(500ms); - @include transition-timing-function(ease-in-out); - } - } - } - - .l-time-range-ticks-holder { - height: $r3H; - .l-time-range-ticks { - border-top: 1px solid $colorTick; - .tick { - background-color: $colorTick; - border:none; - height: 5px; - width: 1px; - margin-left: -1px; - position: absolute; - &:first-child { - margin-left: 0; - } - .l-time-range-tick-label { - @include webkitProp(transform, translateX(-50%)); - color: $colorPlotLabelFg; - display: inline-block; - font-size: 0.7rem; - position: absolute; - top: 5px; - white-space: nowrap; - z-index: 2; - } - } - } - } - - .knob { - z-index: 2; - &:before { - $mTB: 2px; - $grippyW: 3px; - $mLR: ($sliderKnobW - $grippyW)/2; - @include bgStripes($c: pullForward($sliderColorKnob, 20%), $a: 1, $bgsize: 4px, $angle: 0deg); - content: ''; - display: block; - position: absolute; - top: $mTB; right: $mLR; bottom: $mTB; left: $mLR; - } - .range-value { - @include trans-prop-nice-fade(.25s); - font-size: 0.7rem; - position: absolute; - height: $r2H; - line-height: $r2H; - white-space: nowrap; - z-index: 1; - } - &:hover { - .range-value { - color: $sliderColorKnobHov; - } - } - &.knob-l { - margin-left: $knobM; - .range-value { - text-align: right; - right: $rangeValOffset; - } - } - &.knob-r { - margin-right: $knobM; - .range-value { - left: $rangeValOffset; - } - &:hover + .range-holder .range .toi-line { - @include toiLineHovEffects; - } - } - } - - .l-time-domain-selector { - position: absolute; - right: 0px; - top: $interiorMargin; - } - -} - -.s-time-range-val { - border-radius: $controlCr; - background-color: $colorInputBg; - padding: 1px 1px 0 $interiorMargin; -} - -/******************************************************************** MOBILE */ - -@include phoneandtablet { - .l-time-controller { - min-width: 0; - .l-time-range-slider-holder, - .l-time-range-ticks-holder { - display: none; - } - } -} - -@include phone { - .l-time-controller { - .l-time-range-inputs-holder { - &.l-flex-row, - .l-flex-row { - @include align-items(flex-start); - } - .l-time-range-inputs-elem { - &.type-icon { - margin-top: 3px; - } - } - .t-inputs-w { - @include flex-direction(column); - .l-time-range-input-w:not(:first-child) { - &:not(:first-child) { - margin-top: $interiorMargin; - } - margin-right: 0; - } - .l-time-range-inputs-elem { - &.lbl { display: none; } - } - } - } - } -} - -@include phonePortrait { - .l-time-controller { - .l-time-range-inputs-holder { - .t-inputs-w { - @include flex(1 1 auto); - padding-top: 25px; // Make room for the ever lovin' Time Domain Selector - .flex-elem { - @include flex(1 1 auto); - width: 100%; - } - input[type="text"] { - width: 100%; - } - } - } - } - .l-time-domain-selector { - right: auto; - left: 20px; - } -} diff --git a/platform/commonUI/general/res/sass/features/_imagery.scss b/platform/commonUI/general/res/sass/features/_imagery.scss index cd04568e02..e33970b0e9 100644 --- a/platform/commonUI/general/res/sass/features/_imagery.scss +++ b/platform/commonUI/general/res/sass/features/_imagery.scss @@ -74,7 +74,7 @@ .s-image-main { border: 1px solid transparent; &.paused { - border-color: $colorPausedBg; + @extend .s-unsynced; } } diff --git a/platform/commonUI/general/res/sass/helpers/_wait-spinner.scss b/platform/commonUI/general/res/sass/helpers/_wait-spinner.scss index aecf42e02f..93c4fa093f 100644 --- a/platform/commonUI/general/res/sass/helpers/_wait-spinner.scss +++ b/platform/commonUI/general/res/sass/helpers/_wait-spinner.scss @@ -19,15 +19,6 @@ * this source code distribution or the Licensing information page available * at runtime from the About dialog for additional information. *****************************************************************************/ -@include keyframes(rotation) { - 100% { @include transform(rotate(360deg)); } -} - -@include keyframes(rotation-centered) { - 0% { @include transform(translate(-50%, -50%) rotate(0deg)); } - 100% { @include transform(translate(-50%, -50%) rotate(360deg)); } -} - @mixin spinner($b: 5px, $c: $colorKey) { @include transform-origin(center); @include animation-name(rotation-centered); diff --git a/platform/commonUI/general/res/sass/user-environ/_layout.scss b/platform/commonUI/general/res/sass/user-environ/_layout.scss index 9740171ff0..d9ef67bb0a 100644 --- a/platform/commonUI/general/res/sass/user-environ/_layout.scss +++ b/platform/commonUI/general/res/sass/user-environ/_layout.scss @@ -128,7 +128,7 @@ line-height: $ueTopBarH; } - .primary-pane { + .t-object.primary-pane { // Need to lift up this pane to ensure that 'collapsed' panes don't block user interactions z-index: 4; } @@ -212,6 +212,8 @@ body.desktop .pane .mini-tab-icon.toggle-pane { .holder-object { top: $bodyMargin; bottom: $interiorMargin; + // Clip element that have min-widths + overflow: hidden; } .holder-inspector { top: $bodyMargin; diff --git a/platform/commonUI/general/res/templates/controls/datetime-field.html b/platform/commonUI/general/res/templates/controls/datetime-field.html index 1cb5e76f6d..684ac0b684 100644 --- a/platform/commonUI/general/res/templates/controls/datetime-field.html +++ b/platform/commonUI/general/res/templates/controls/datetime-field.html @@ -23,6 +23,8 @@ .l-row-elem { + // First order row elements + box-sizing: border-box; + width: 100%; + position: relative; + } + + .mode-selector .s-menu-button, + .time-delta { + &:before { + @extend .ui-symbol; + } + } + + .time-delta { + &:before { + color: $colorTimeCondKeyBg; + } + } + + .l-time-conductor-inputs-holder, + .l-time-conductor-inputs-and-ticks, + .l-time-conductor-zoom-w { + font-size: 0.8rem; + } + + .l-time-conductor-inputs-holder { + $ticksBlockerFadeW: 50px; + $iconCalendarW: 16px; + $wBgColor: $colorBodyBg; + + height: $r1H; + position: absolute; + top: 0; + right: 0; + bottom: 0; + left: 0; + z-index: 1; + .l-time-range-w { + // Wraps a datetime text input field + height: 100%; + position: absolute; + .title { + display: inline-block; + margin-right: $interiorMarginSm; + } + &.start-w { + @include background-image(linear-gradient(270deg, transparent, $wBgColor $ticksBlockerFadeW)); + padding-right: $ticksBlockerFadeW; + .title:before { + content: 'Start'; + } + } + &.end-w { + @include background-image(linear-gradient(90deg, transparent, $wBgColor $ticksBlockerFadeW)); + padding-left: $ticksBlockerFadeW; + right: 0; + .title:before { + content: 'End'; + } + } + input[type="text"] { + @include trans-prop-nice(padding, 250ms); + } + .time-range-input input[type="text"] { + width: $timeCondInputTimeSysDefW; + } + .hrs-min-input input[type="text"] { + width: $timeCondInputDeltaDefW; + } + .icon-calendar { + margin-top: 4px; + } + } + } + + .l-time-conductor-inputs-and-ticks { + $c: $colorTimeCondTicks; //$colorTick; + height: $r1H; + mct-conductor-axis { + display: block; + position: relative; + width: 100%; + } + .l-axis-holder { + height: $r1H; + position: relative; + width: 100%; + svg { + text-rendering: geometricPrecision; + width: 100%; + height: 100%; + > g { + font-size: 0.9em; + } + path { + // Line beneath ticks + display: none; + } + line { + // Tick marks + stroke: $c; + } + text { + // Tick labels + fill: $c; + } + } + } + } + .l-data-visualization { + background: $colorTimeCondDataVisBg; + height: $r2H; + } + + .l-time-conductor-controls { + align-items: center; + margin-top: $interiorMargin; + .l-time-conductor-zoom-w { + @include justify-content(flex-end); + .time-conductor-zoom { + display: none; // TEMP per request from Andrew 8/1/16 + height: $r3H; + min-width: 100px; + width: 20%; + } + .time-conductor-zoom-current-range { + display: none; // TEMP per request from Andrew 8/1/16 + color: $colorTick; + } + } + } + + // Real-time, latest modes + &.realtime-mode, + &.lad-mode { + .time-conductor-icon { + &:before { color: $colorTimeCondKeyBg; } + div[class*="hand"] { + @include animation-name(clock-hands); + &:before { + background: $colorTimeCondKeyBg; + } + } + } + + .l-time-conductor-inputs-holder { + .l-time-range-input-w { + input[type="text"]:not(.error) { + background: transparent; + box-shadow: none; + border-radius: 0; + padding-left: 0; + padding-right: 0; + &:hover, + &:focus { + @include nice-input(); + padding: $inputTextP; + } + } + .icon-calendar { + display: none; + } + &.start-date { + display: none; + } + &.end-date { + pointer-events: none; + input[type="text"] { + color: pullForward($colorTimeCondKeyBg, 5%); + margin-right: $interiorMargin; + tab-index: -1; + } + } + } + } + + .l-data-visualization { + background: $colorTimeCondDataVisRtBg !important + } + + .mode-selector .s-menu-button { + $fg: $colorTimeCondKeyFg; + @include btnSubtle($bg: $colorTimeCondKeyBg, $bgHov: pullForward($colorTimeCondKeyBg, $ltGamma), $fg: $colorTimeCondKeyFg); + &:before { color: $fg !important; }; + color: $fg !important; + } + } + + // Fixed mode + &.fixed-mode { + $i: $glyph-icon-calendar; + .time-conductor-icon div[class*="hand"] { + &.hand-little { + @include transform(rotate(120deg)); + } + } + .mode-selector .s-menu-button:before { + content: $i; + } + } + + // Realtime mode + &.realtime-mode { + $i: $glyph-icon-clock; + .time-conductor-icon div[class*="hand"] { + @include animation-name(clock-hands); + } + .time-delta:before { + content: $i; + } + .l-time-conductor-inputs-holder .l-time-range-w.end-w .title:before { + content: 'Now'; + } + .mode-selector .s-menu-button:before { + content: $i; + } + } + + // LAD mode + &.lad-mode { + $i: $glyph-icon-database; + .time-conductor-icon div[class*="hand"] { + @include animation-name(clock-hands-sticky); + &.hand-big { + @include animation-duration(5s); + } + &.hand-little { + @include animation-duration(60s); + } + } + .time-delta:before { + content: $i; + } + .l-time-conductor-inputs-holder .l-time-range-w.end-w .title:before { + content: 'LAD'; + } + .mode-selector .s-menu-button:before { + content: $i; + } + } +} + +/******************************************************************** MOBILE */ + +@include phoneandtablet { + .l-time-conductor-holder { min-width: 0 !important; } + .super-menu.mini { + width: 200px; + height: 100px; + .pane.menu-item-description { + display: none; + } + } +} + +@include phone { + .l-time-conductor { + min-width: 0; + .l-time-conductor-inputs-and-ticks { + .l-time-conductor-inputs-holder { + .l-time-range-w { + background-image: none !important; + } + } + mct-conductor-axis { + display: none; + } + } + } +} + +@include phonePortrait { + .l-time-conductor { + .l-data-visualization, + .l-time-conductor-zoom-w, + .time-delta { + display: none; + } + + .l-time-conductor-inputs-and-ticks { + height: auto !important; + .l-time-conductor-inputs-holder { + position: relative; + height: auto !important; + + .l-time-range-w { + background-image: none !important; + display: block; + height: auto !important; + padding: 0 !important; + position: relative; + text-align: left; + &:not(:first-child) { + margin-top: $interiorMargin; + } + } + } + } + + // Fixed mode + &.fixed-mode { + .l-time-conductor-inputs-and-ticks { + .l-time-range-w { + .title { + width: 30px; + } + } + } + } + + // Real-time, latest modes + &.realtime-mode, + &.lad-mode { + .l-time-conductor-inputs-and-ticks { + .l-time-range-w { + &.start-w { + display: none; + } + &.end-w { + margin-top: 0; + .end-date input[type="text"] { + margin: 0; + text-align: left; + } + } + } + } + } + } +} diff --git a/platform/features/conductor-v2/conductor/res/sass/time-conductor-espresso.scss b/platform/features/conductor-v2/conductor/res/sass/time-conductor-espresso.scss new file mode 100644 index 0000000000..a47f4c07a7 --- /dev/null +++ b/platform/features/conductor-v2/conductor/res/sass/time-conductor-espresso.scss @@ -0,0 +1,39 @@ +/***************************************************************************** + * Open MCT, Copyright (c) 2014-2015, United States Government + * as represented by the Administrator of the National Aeronautics and Space + * Administration. All rights reserved. + * + * Open MCT 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 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. + *****************************************************************************/ +@import "bourbon"; +@import "../../../../../commonUI/general/res/sass/constants"; +@import "../../../../../commonUI/general/res/sass/mixins"; +@import "../../../../../commonUI/general/res/sass/mobile/constants"; +@import "../../../../../commonUI/general/res/sass/mobile/mixins"; +@import "../../../../../commonUI/themes/espresso/res/sass/constants"; +@import "../../../../../commonUI/themes/espresso/res/sass/mixins"; +@import "../../../../../commonUI/general/res/sass/glyphs"; +@import "../../../../../commonUI/general/res/sass/icons"; +@import "constants"; + +// Thematic constants +$colorTimeCondTicks: pullForward($colorBodyBg, 30%); +$colorTimeCondKeyBg: #4e70dc; +$colorTimeCondKeyFg: #fff; +$colorTimeCondDataVisBg: pullForward($colorBodyBg, 10%); +$colorTimeCondDataVisRtBg: pushBack($colorTimeCondKeyBg, 10%); +@import "time-conductor-base"; \ No newline at end of file diff --git a/platform/features/conductor-v2/conductor/res/sass/time-conductor-snow.scss b/platform/features/conductor-v2/conductor/res/sass/time-conductor-snow.scss new file mode 100644 index 0000000000..626ceb0e51 --- /dev/null +++ b/platform/features/conductor-v2/conductor/res/sass/time-conductor-snow.scss @@ -0,0 +1,39 @@ +/***************************************************************************** + * Open MCT, Copyright (c) 2014-2015, United States Government + * as represented by the Administrator of the National Aeronautics and Space + * Administration. All rights reserved. + * + * Open MCT 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 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. + *****************************************************************************/ +@import "bourbon"; +@import "../../../../../commonUI/general/res/sass/constants"; +@import "../../../../../commonUI/general/res/sass/mixins"; +@import "../../../../../commonUI/general/res/sass/mobile/constants"; +@import "../../../../../commonUI/general/res/sass/mobile/mixins"; +@import "../../../../../commonUI/themes/snow/res/sass/constants"; +@import "../../../../../commonUI/themes/snow/res/sass/mixins"; +@import "../../../../../commonUI/general/res/sass/glyphs"; +@import "../../../../../commonUI/general/res/sass/icons"; +@import "constants"; + +// Thematic constants +$colorTimeCondTicks: pullForward($colorBodyBg, 30%); +$colorTimeCondKeyBg: #6178dc; +$colorTimeCondKeyFg: #fff; +$colorTimeCondDataVisBg: pullForward($colorBodyBg, 10%); +$colorTimeCondDataVisRtBg: pushBack($colorTimeCondKeyBg, 30%); +@import "time-conductor-base"; \ No newline at end of file diff --git a/platform/features/conductor-v2/conductor/res/templates/mode-selector/mode-menu.html b/platform/features/conductor-v2/conductor/res/templates/mode-selector/mode-menu.html new file mode 100644 index 0000000000..990c6de482 --- /dev/null +++ b/platform/features/conductor-v2/conductor/res/templates/mode-selector/mode-menu.html @@ -0,0 +1,45 @@ + +
+ + +
diff --git a/platform/features/conductor-v2/conductor/res/templates/mode-selector/mode-selector.html b/platform/features/conductor-v2/conductor/res/templates/mode-selector/mode-selector.html new file mode 100644 index 0000000000..1f346f2bef --- /dev/null +++ b/platform/features/conductor-v2/conductor/res/templates/mode-selector/mode-selector.html @@ -0,0 +1,34 @@ + + +
+ {{ngModel.options[ngModel.selectedKey] + .label}} +
+ +
\ No newline at end of file diff --git a/platform/features/conductor-v2/conductor/res/templates/time-conductor.html b/platform/features/conductor-v2/conductor/res/templates/time-conductor.html new file mode 100644 index 0000000000..19c86b2b74 --- /dev/null +++ b/platform/features/conductor-v2/conductor/res/templates/time-conductor.html @@ -0,0 +1,108 @@ + +
+ +
+
+
+
+ +
+ +
+
+ + + + + + + + - + + + + + + + + + + + + + + + + + + + +
+ +
+ + +
+ + +
+ + + + + +
+ + +
+
+ +
+
diff --git a/platform/features/conductor-v2/conductor/src/TimeConductor.js b/platform/features/conductor-v2/conductor/src/TimeConductor.js new file mode 100644 index 0000000000..2c2194b5a0 --- /dev/null +++ b/platform/features/conductor-v2/conductor/src/TimeConductor.js @@ -0,0 +1,179 @@ +/***************************************************************************** + * 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. + *****************************************************************************/ + +define(['EventEmitter'], function (EventEmitter) { + + /** + * The public API for setting and querying time conductor state. The + * time conductor is the means by which the temporal bounds of a view + * are controlled. Time-sensitive views will typically respond to + * changes to bounds or other properties of the time conductor and + * update the data displayed based on the time conductor state. + * + * The TimeConductor extends the EventEmitter class. A number of events are + * fired when properties of the time conductor change, which are + * documented below. + * @constructor + */ + function TimeConductor() { + EventEmitter.call(this); + + //The Time System + this.system = undefined; + //The Time Of Interest + this.toi = undefined; + + this.boundsVal = { + start: undefined, + end: undefined + }; + + //Default to fixed mode + this.followMode = false; + } + + TimeConductor.prototype = Object.create(EventEmitter.prototype); + + /** + * Validate the given bounds. This can be used for pre-validation of + * bounds, for example by views validating user inputs. + * @param bounds The start and end time of the conductor. + * @returns {string | true} A validation error, or true if valid + */ + TimeConductor.prototype.validateBounds = function (bounds) { + if ((bounds.start === undefined) || + (bounds.end === undefined) || + isNaN(bounds.start) || + isNaN(bounds.end) + ) { + return "Start and end must be specified as integer values"; + } else if (bounds.start > bounds.end) { + return "Specified start date exceeds end bound"; + } + return true; + }; + + /** + * Get or set the follow mode of the time conductor. In follow mode the + * time conductor ticks, regularly updating the bounds from a timing + * source appropriate to the selected time system and mode of the time + * conductor. + * @fires TimeConductor#follow + * @param {boolean} followMode + * @returns {boolean} + */ + TimeConductor.prototype.follow = function (followMode) { + if (arguments.length > 0) { + this.followMode = followMode; + /** + * @event TimeConductor#follow The TimeConductor has toggled + * into or out of follow mode. + * @property {boolean} followMode true if follow mode is + * enabled, otherwise false. + */ + this.emit('follow', this.followMode); + } + return this.followMode; + }; + + /** + * @typedef {Object} TimeConductorBounds + * @property {number} start The start time displayed by the time conductor in ms since epoch. Epoch determined by current time system + * @property {number} end The end time displayed by the time conductor in ms since epoch. + */ + /** + * Get or set the start and end time of the time conductor. Basic validation + * of bounds is performed. + * + * @param {TimeConductorBounds} newBounds + * @throws {Error} Validation error + * @fires TimeConductor#bounds + * @returns {TimeConductorBounds} + */ + TimeConductor.prototype.bounds = function (newBounds) { + if (arguments.length > 0) { + var validationResult = this.validateBounds(newBounds); + if (validationResult !== true) { + throw new Error(validationResult); + } + //Create a copy to avoid direct mutation of conductor bounds + this.boundsVal = JSON.parse(JSON.stringify(newBounds)); + /** + * @event TimeConductor#bounds The start time, end time, or + * both have been updated + * @property {TimeConductorBounds} bounds + */ + this.emit('bounds', this.boundsVal); + } + //Return a copy to prevent direct mutation of time conductor bounds. + return JSON.parse(JSON.stringify(this.boundsVal)); + }; + + /** + * Get or set the time system of the TimeConductor. Time systems determine + * units, epoch, and other aspects of time representation. When changing + * the time system in use, new valid bounds must also be provided. + * @param {TimeSystem} newTimeSystem + * @param {TimeConductorBounds} bounds + * @fires TimeConductor#timeSystem + * @returns {TimeSystem} The currently applied time system + */ + TimeConductor.prototype.timeSystem = function (newTimeSystem, bounds) { + if (arguments.length >= 2) { + this.system = newTimeSystem; + /** + * @event TimeConductor#timeSystem The time system used by the time + * conductor has changed. A change in Time System will always be + * followed by a bounds event specifying new query bounds + * @property {TimeSystem} The value of the currently applied + * Time System + * */ + this.emit('timeSystem', this.system); + this.bounds(bounds); + } else if (arguments.length === 1) { + throw new Error('Must set bounds when changing time system'); + } + return this.system; + }; + + /** + * Get or set the Time of Interest. The Time of Interest is the temporal + * focus of the current view. It can be manipulated by the user from the + * time conductor or from other views. + * @fires TimeConductor#timeOfInterest + * @param newTOI + * @returns {number} the current time of interest + */ + TimeConductor.prototype.timeOfInterest = function (newTOI) { + if (arguments.length > 0) { + this.toi = newTOI; + /** + * @event TimeConductor#timeOfInterest The Time of Interest has moved. + * @property {number} Current time of interest + */ + this.emit('timeOfInterest', this.toi); + } + return this.toi; + }; + + return TimeConductor; +}); diff --git a/platform/features/conductor-v2/conductor/src/TimeConductorSpec.js b/platform/features/conductor-v2/conductor/src/TimeConductorSpec.js new file mode 100644 index 0000000000..7701f5e9b4 --- /dev/null +++ b/platform/features/conductor-v2/conductor/src/TimeConductorSpec.js @@ -0,0 +1,110 @@ +/***************************************************************************** + * 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. + *****************************************************************************/ + +define(['./TimeConductor'], function (TimeConductor) { + describe("The Time Conductor", function () { + var tc, + timeSystem, + bounds, + eventListener, + toi, + follow; + + beforeEach(function () { + tc = new TimeConductor(); + timeSystem = {}; + bounds = {start: 0, end: 0}; + eventListener = jasmine.createSpy("eventListener"); + toi = 111; + follow = true; + }); + + it("Supports setting and querying of time of interest and and follow mode", function () { + expect(tc.timeOfInterest()).not.toBe(toi); + tc.timeOfInterest(toi); + expect(tc.timeOfInterest()).toBe(toi); + + expect(tc.follow()).not.toBe(follow); + tc.follow(follow); + expect(tc.follow()).toBe(follow); + }); + + it("Allows setting of valid bounds", function () { + bounds = {start: 0, end: 1}; + expect(tc.bounds()).not.toEqual(bounds); + expect(tc.bounds.bind(tc, bounds)).not.toThrow(); + expect(tc.bounds()).toEqual(bounds); + }); + + it("Disallows setting of invalid bounds", function () { + bounds = {start: 1, end: 0}; + expect(tc.bounds()).not.toEqual(bounds); + expect(tc.bounds.bind(tc, bounds)).toThrow(); + expect(tc.bounds()).not.toEqual(bounds); + + bounds = {start: 1}; + expect(tc.bounds()).not.toEqual(bounds); + expect(tc.bounds.bind(tc, bounds)).toThrow(); + expect(tc.bounds()).not.toEqual(bounds); + }); + + it("Allows setting of time system with bounds", function () { + expect(tc.timeSystem()).not.toBe(timeSystem); + expect(tc.timeSystem.bind(tc, timeSystem, bounds)).not.toThrow(); + expect(tc.timeSystem()).toBe(timeSystem); + }); + + it("Disallows setting of time system without bounds", function () { + expect(tc.timeSystem()).not.toBe(timeSystem); + expect(tc.timeSystem.bind(tc, timeSystem)).toThrow(); + expect(tc.timeSystem()).not.toBe(timeSystem); + }); + + it("Emits an event when time system changes", function () { + expect(eventListener).not.toHaveBeenCalled(); + tc.on("timeSystem", eventListener); + tc.timeSystem(timeSystem, bounds); + expect(eventListener).toHaveBeenCalledWith(timeSystem); + }); + + it("Emits an event when time of interest changes", function () { + expect(eventListener).not.toHaveBeenCalled(); + tc.on("timeOfInterest", eventListener); + tc.timeOfInterest(toi); + expect(eventListener).toHaveBeenCalledWith(toi); + }); + + it("Emits an event when bounds change", function () { + expect(eventListener).not.toHaveBeenCalled(); + tc.on("bounds", eventListener); + tc.bounds(bounds); + expect(eventListener).toHaveBeenCalledWith(bounds); + }); + + it("Emits an event when follow mode changes", function () { + expect(eventListener).not.toHaveBeenCalled(); + tc.on("follow", eventListener); + tc.follow(follow); + expect(eventListener).toHaveBeenCalledWith(follow); + }); + }); +}); diff --git a/platform/features/conductor-v2/conductor/src/timeSystems/LocalClock.js b/platform/features/conductor-v2/conductor/src/timeSystems/LocalClock.js new file mode 100644 index 0000000000..09ddd35612 --- /dev/null +++ b/platform/features/conductor-v2/conductor/src/timeSystems/LocalClock.js @@ -0,0 +1,89 @@ +/***************************************************************************** + * 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. + *****************************************************************************/ + +define(['./TickSource'], function (TickSource) { + /** + * @implements TickSource + * @constructor + */ + function LocalClock($timeout, period) { + TickSource.call(this); + + this.metadata = { + key: 'local', + mode: 'realtime', + cssclass: 'icon-clock', + label: 'Real-time', + name: 'Real-time Mode', + description: 'Monitor real-time streaming data as it comes in. The Time Conductor and displays will automatically advance themselves based on a UTC clock.' + }; + + this.period = period; + this.$timeout = $timeout; + this.timeoutHandle = undefined; + } + + LocalClock.prototype = Object.create(TickSource.prototype); + + LocalClock.prototype.start = function () { + this.timeoutHandle = this.$timeout(this.tick.bind(this), this.period); + }; + + LocalClock.prototype.stop = function () { + if (this.timeoutHandle) { + this.$timeout.cancel(this.timeoutHandle); + } + }; + + LocalClock.prototype.tick = function () { + var now = Date.now(); + this.listeners.forEach(function (listener) { + listener(now); + }); + this.timeoutHandle = this.$timeout(this.tick.bind(this), this.period); + }; + + /** + * Register a listener for the local clock. When it ticks, the local + * clock will provide the current local system time + * + * @param listener + * @returns {function} a function for deregistering the provided listener + */ + LocalClock.prototype.listen = function (listener) { + var listeners = this.listeners; + listeners.push(listener); + + if (listeners.length === 1) { + this.start(); + } + + return function () { + listeners.splice(listeners.indexOf(listener)); + if (listeners.length === 0) { + this.stop(); + } + }.bind(this); + }; + + return LocalClock; +}); diff --git a/platform/features/conductor-v2/conductor/src/timeSystems/LocalClockSpec.js b/platform/features/conductor-v2/conductor/src/timeSystems/LocalClockSpec.js new file mode 100644 index 0000000000..b34b625f43 --- /dev/null +++ b/platform/features/conductor-v2/conductor/src/timeSystems/LocalClockSpec.js @@ -0,0 +1,50 @@ +/***************************************************************************** + * 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. + *****************************************************************************/ + +define(["./LocalClock"], function (LocalClock) { + describe("The LocalClock class", function () { + var clock, + mockTimeout, + timeoutHandle = {}; + + beforeEach(function () { + mockTimeout = jasmine.createSpy("timeout"); + mockTimeout.andReturn(timeoutHandle); + mockTimeout.cancel = jasmine.createSpy("cancel"); + + clock = new LocalClock(mockTimeout, 0); + clock.start(); + }); + + it("calls listeners on tick with current time", function () { + var mockListener = jasmine.createSpy("listener"); + clock.listen(mockListener); + clock.tick(); + expect(mockListener).toHaveBeenCalledWith(jasmine.any(Number)); + }); + + it("stops ticking when stop is called", function () { + clock.stop(); + expect(mockTimeout.cancel).toHaveBeenCalledWith(timeoutHandle); + }); + }); +}); diff --git a/platform/features/conductor-v2/conductor/src/timeSystems/TickSource.js b/platform/features/conductor-v2/conductor/src/timeSystems/TickSource.js new file mode 100644 index 0000000000..8cb9fd308d --- /dev/null +++ b/platform/features/conductor-v2/conductor/src/timeSystems/TickSource.js @@ -0,0 +1,47 @@ +/***************************************************************************** + * 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. + *****************************************************************************/ + +define([], function () { + /** + * A tick source is an event generator such as a timing signal, or + * indicator of data availability, which can be used to advance the Time + * Conductor. Usage is simple, a listener registers a callback which is + * invoked when this source 'ticks'. + * + * @interface + * @constructor + */ + function TickSource() { + this.listeners = []; + } + + /** + * @param callback Function to be called when this tick source ticks. + * @returns an 'unlisten' function that will remove the callback from + * the registered listeners + */ + TickSource.prototype.listen = function (callback) { + throw new Error('Not implemented'); + }; + + return TickSource; +}); diff --git a/platform/features/conductor-v2/conductor/src/timeSystems/TimeSystem.js b/platform/features/conductor-v2/conductor/src/timeSystems/TimeSystem.js new file mode 100644 index 0000000000..652ea9ed0f --- /dev/null +++ b/platform/features/conductor-v2/conductor/src/timeSystems/TimeSystem.js @@ -0,0 +1,93 @@ +/***************************************************************************** + * 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. + *****************************************************************************/ + +define([], function () { + /** + * @interface + * @constructor + */ + function TimeSystem() { + /** + * @typedef TimeSystemMetadata + * @property {string} key + * @property {string} name + * @property {string} description + * + * @type {TimeSystemMetadata} + */ + this.metadata = undefined; + } + + /** + * Time formats are defined as extensions. Time systems that implement + * this interface should provide an array of format keys supported by them. + * + * @returns {string[]} An array of time format keys + */ + TimeSystem.prototype.formats = function () { + throw new Error('Not implemented'); + }; + + /** + * @typedef DeltaFormat + * @property {string} type the type of MctControl used to represent this + * field. Typically 'datetime-field' for UTC based dates, or 'textfield' + * otherwise + * @property {string} [format] An optional field specifying the + * Format to use for delta fields in this time system. + */ + /** + * Specifies a format for deltas in this time system. + * + * @returns {DeltaFormat} a delta format specifier + */ + TimeSystem.prototype.deltaFormat = function () { + throw new Error('Not implemented'); + }; + + /** + * Returns the tick sources supported by this time system. Tick sources + * are event generators that can be used to advance the time conductor + * @returns {TickSource[]} The tick sources supported by this time system. + */ + TimeSystem.prototype.tickSources = function () { + throw new Error('Not implemented'); + }; + + /** + * + * @returns {TimeSystemDefault[]} At least one set of default values for + * this time system. + */ + TimeSystem.prototype.defaults = function () { + throw new Error('Not implemented'); + }; + + /** + * @return {boolean} + */ + TimeSystem.prototype.isUTCBased = function () { + return true; + }; + + return TimeSystem; +}); diff --git a/platform/features/conductor-v2/conductor/src/ui/MctConductorAxis.js b/platform/features/conductor-v2/conductor/src/ui/MctConductorAxis.js new file mode 100644 index 0000000000..58cb60befc --- /dev/null +++ b/platform/features/conductor-v2/conductor/src/ui/MctConductorAxis.js @@ -0,0 +1,146 @@ +/***************************************************************************** + * 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. + *****************************************************************************/ + +define( + [ + "d3" + ], + function (d3) { + var PADDING = 1; + + /** + * The mct-conductor-axis renders a horizontal axis with regular + * labelled 'ticks'. It requires 'start' and 'end' integer values to + * be specified as attributes. + */ + function MCTConductorAxis(conductor, formatService) { + // Dependencies + this.d3 = d3; + this.conductor = conductor; + this.formatService = formatService; + + // Runtime properties (set by 'link' function) + this.target = undefined; + this.xScale = undefined; + this.xAxis = undefined; + this.axisElement = undefined; + + // Angular Directive interface + this.link = this.link.bind(this); + this.restrict = "E"; + this.template = + "
"; + this.priority = 1000; + + //Bind all class functions to 'this' + Object.keys(MCTConductorAxis.prototype).filter(function (key) { + return typeof MCTConductorAxis.prototype[key] === 'function'; + }).forEach(function (key) { + this[key] = this[key].bind(this); + }.bind(this)); + } + + MCTConductorAxis.prototype.setScale = function () { + var width = this.target.offsetWidth; + var timeSystem = this.conductor.timeSystem(); + var bounds = this.conductor.bounds(); + + if (timeSystem.isUTCBased()) { + this.xScale = this.xScale || this.d3.scaleUtc(); + this.xScale.domain([new Date(bounds.start), new Date(bounds.end)]); + } else { + this.xScale = this.xScale || this.d3.scaleLinear(); + this.xScale.domain([bounds.start, bounds.end]); + } + + this.xScale.range([PADDING, width - PADDING * 2]); + this.axisElement.call(this.xAxis); + }; + + MCTConductorAxis.prototype.changeTimeSystem = function (timeSystem) { + var key = timeSystem.formats()[0]; + if (key !== undefined) { + var format = this.formatService.getFormat(key); + var bounds = this.conductor.bounds(); + + if (timeSystem.isUTCBased()) { + this.xScale = this.d3.scaleUtc(); + } else { + this.xScale = this.d3.scaleLinear(); + } + + this.xAxis.scale(this.xScale); + //Define a custom format function + this.xAxis.tickFormat(function (tickValue) { + // Normalize date representations to numbers + if (tickValue instanceof Date) { + tickValue = tickValue.getTime(); + } + return format.format(tickValue, { + min: bounds.start, + max: bounds.end + }); + }); + this.axisElement.call(this.xAxis); + } + }; + + MCTConductorAxis.prototype.destroy = function () { + this.conductor.off('timeSystem', this.changeTimeSystem); + this.conductor.off('bounds', this.setScale); + }; + + MCTConductorAxis.prototype.link = function (scope, element) { + var conductor = this.conductor; + this.target = element[0].firstChild; + var height = this.target.offsetHeight; + var vis = this.d3.select(this.target) + .append('svg:svg') + .attr('width', '100%') + .attr('height', height); + + this.xAxis = this.d3.axisTop(); + + // draw x axis with labels and move to the bottom of the chart area + this.axisElement = vis.append("g") + .attr("transform", "translate(0," + (height - PADDING) + ")"); + + scope.resize = this.setScale; + + conductor.on('timeSystem', this.changeTimeSystem); + + //On conductor bounds changes, redraw ticks + conductor.on('bounds', this.setScale); + + scope.$on("$destroy", this.destroy); + + if (conductor.timeSystem() !== undefined) { + this.changeTimeSystem(conductor.timeSystem()); + this.setScale(); + } + }; + + return function (conductor, formatService) { + return new MCTConductorAxis(conductor, formatService); + }; + } +); diff --git a/platform/features/conductor-v2/conductor/src/ui/MctConductorAxisSpec.js b/platform/features/conductor-v2/conductor/src/ui/MctConductorAxisSpec.js new file mode 100644 index 0000000000..0fe3c4cf3f --- /dev/null +++ b/platform/features/conductor-v2/conductor/src/ui/MctConductorAxisSpec.js @@ -0,0 +1,146 @@ +/***************************************************************************** + * 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. + *****************************************************************************/ + +define(['./MctConductorAxis'], function (MctConductorAxis) { + describe("The MctConductorAxis directive", function () { + var directive, + mockConductor, + mockFormatService, + mockScope, + mockElement, + mockTarget, + mockBounds, + d3; + + beforeEach(function () { + mockScope = jasmine.createSpyObj("scope", [ + "$on" + ]); + + //Add some HTML elements + mockTarget = { + offsetWidth: 0, + offsetHeight: 0 + }; + mockElement = { + firstChild: mockTarget + }; + mockBounds = { + start: 100, + end: 200 + }; + mockConductor = jasmine.createSpyObj("conductor", [ + "timeSystem", + "bounds", + "on", + "off" + ]); + mockConductor.bounds.andReturn(mockBounds); + + mockFormatService = jasmine.createSpyObj("formatService", [ + "getFormat" + ]); + + var d3Functions = [ + "scale", + "scaleUtc", + "scaleLinear", + "select", + "append", + "attr", + "axisTop", + "call", + "tickFormat", + "domain", + "range" + ]; + d3 = jasmine.createSpyObj("d3", d3Functions); + d3Functions.forEach(function (func) { + d3[func].andReturn(d3); + }); + + directive = new MctConductorAxis(mockConductor, mockFormatService); + directive.d3 = d3; + directive.link(mockScope, [mockElement]); + }); + + it("listens for changes to time system and bounds", function () { + expect(mockConductor.on).toHaveBeenCalledWith("timeSystem", directive.changeTimeSystem); + expect(mockConductor.on).toHaveBeenCalledWith("bounds", directive.setScale); + }); + + it("on scope destruction, deregisters listeners", function () { + expect(mockScope.$on).toHaveBeenCalledWith("$destroy", directive.destroy); + directive.destroy(); + expect(mockConductor.off).toHaveBeenCalledWith("timeSystem", directive.changeTimeSystem); + expect(mockConductor.off).toHaveBeenCalledWith("bounds", directive.setScale); + }); + + describe("when the time system changes", function () { + var mockTimeSystem; + var mockFormat; + + beforeEach(function () { + mockTimeSystem = jasmine.createSpyObj("timeSystem", [ + "formats", + "isUTCBased" + ]); + mockFormat = jasmine.createSpyObj("format", [ + "format" + ]); + + mockTimeSystem.formats.andReturn(["mockFormat"]); + mockFormatService.getFormat.andReturn(mockFormat); + }); + + it("uses a UTC scale for UTC time systems", function () { + mockTimeSystem.isUTCBased.andReturn(true); + directive.changeTimeSystem(mockTimeSystem); + expect(d3.scaleUtc).toHaveBeenCalled(); + expect(d3.scaleLinear).not.toHaveBeenCalled(); + }); + + it("uses a linear scale for non-UTC time systems", function () { + mockTimeSystem.isUTCBased.andReturn(false); + directive.changeTimeSystem(mockTimeSystem); + expect(d3.scaleLinear).toHaveBeenCalled(); + expect(d3.scaleUtc).not.toHaveBeenCalled(); + }); + + it("sets axis domain to time conductor bounds", function () { + mockTimeSystem.isUTCBased.andReturn(false); + mockConductor.timeSystem.andReturn(mockTimeSystem); + + directive.setScale(); + expect(d3.domain).toHaveBeenCalledWith([mockBounds.start, mockBounds.end]); + }); + + it("uses the format specified by the time system to format tick" + + " labels", function () { + directive.changeTimeSystem(mockTimeSystem); + expect(d3.tickFormat).toHaveBeenCalled(); + d3.tickFormat.mostRecentCall.args[0](); + expect(mockFormat.format).toHaveBeenCalled(); + }); + }); + }); +}); diff --git a/platform/features/conductor-v2/conductor/src/ui/NumberFormat.js b/platform/features/conductor-v2/conductor/src/ui/NumberFormat.js new file mode 100644 index 0000000000..ac3f40cb7b --- /dev/null +++ b/platform/features/conductor-v2/conductor/src/ui/NumberFormat.js @@ -0,0 +1,53 @@ +/***************************************************************************** + * Open MCT, Copyright (c) 2014-2016, United States Government + * as represented by the Administrator of the National Aeronautics and Space + * Administration. All rights reserved. + * + * Open MCT 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 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. + *****************************************************************************/ + +define([], function () { + + /** + * Formatter for basic numbers. Provides basic support for non-UTC + * numbering systems + * + * @implements {Format} + * @constructor + * @memberof platform/commonUI/formats + */ + function NumberFormat() { + } + + NumberFormat.prototype.format = function (value) { + if (isNaN(value)) { + return ''; + } else { + return '' + value; + } + }; + + NumberFormat.prototype.parse = function (text) { + return parseFloat(text); + }; + + NumberFormat.prototype.validate = function (text) { + return !isNaN(text); + }; + + return NumberFormat; +}); diff --git a/platform/features/conductor-v2/conductor/src/ui/NumberFormatSpec.js b/platform/features/conductor-v2/conductor/src/ui/NumberFormatSpec.js new file mode 100644 index 0000000000..f56c6e67d3 --- /dev/null +++ b/platform/features/conductor-v2/conductor/src/ui/NumberFormatSpec.js @@ -0,0 +1,49 @@ +/***************************************************************************** + * 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. + *****************************************************************************/ + +define(['./NumberFormat'], function (NumberFormat) { + describe("The NumberFormat class", function () { + var format; + beforeEach(function () { + format = new NumberFormat(); + }); + + it("The format function takes a string and produces a number", function () { + var text = format.format(1); + expect(text).toBe("1"); + expect(typeof text).toBe("string"); + }); + + it("The parse function takes a string and produces a number", function () { + var number = format.parse("1"); + expect(number).toBe(1); + expect(typeof number).toBe("number"); + }); + + it("validates that the input is a number", function () { + expect(format.validate("1")).toBe(true); + expect(format.validate(1)).toBe(true); + expect(format.validate("1.1")).toBe(true); + expect(format.validate("abc")).toBe(false); + }); + }); +}); diff --git a/platform/features/conductor-v2/conductor/src/ui/TimeConductorController.js b/platform/features/conductor-v2/conductor/src/ui/TimeConductorController.js new file mode 100644 index 0000000000..83bc0558c6 --- /dev/null +++ b/platform/features/conductor-v2/conductor/src/ui/TimeConductorController.js @@ -0,0 +1,243 @@ +/***************************************************************************** + * 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. + *****************************************************************************/ + +define( + [ + './TimeConductorValidation' + ], + function (TimeConductorValidation) { + + function TimeConductorController($scope, $window, timeConductor, conductorViewService, timeSystems) { + + var self = this; + + //Bind all class functions to 'this' + Object.keys(TimeConductorController.prototype).filter(function (key) { + return typeof TimeConductorController.prototype[key] === 'function'; + }).forEach(function (key) { + self[key] = self[key].bind(self); + }); + + this.$scope = $scope; + this.$window = $window; + this.conductorViewService = conductorViewService; + this.conductor = timeConductor; + this.modes = conductorViewService.availableModes(); + this.validation = new TimeConductorValidation(this.conductor); + + // Construct the provided time system definitions + this.timeSystems = timeSystems.map(function (timeSystemConstructor) { + return timeSystemConstructor(); + }); + + //Set the initial state of the view based on current time conductor + this.initializeScope(); + + this.conductor.on('bounds', this.setFormFromBounds); + this.conductor.on('timeSystem', this.changeTimeSystem); + + // If no mode selected, select fixed as the default + if (!this.conductorViewService.mode()) { + this.setMode('fixed'); + } + } + + /** + * @private + */ + TimeConductorController.prototype.initializeScope = function () { + //Set time Conductor bounds in the form + this.$scope.boundsModel = this.conductor.bounds(); + + //If conductor has a time system selected already, populate the + //form from it + this.$scope.timeSystemModel = {}; + if (this.conductor.timeSystem()) { + this.setFormFromTimeSystem(this.conductor.timeSystem()); + } + + //Represents the various modes, and the currently selected mode + //in the view + this.$scope.modeModel = { + options: this.conductorViewService.availableModes() + }; + + var mode = this.conductorViewService.mode(); + if (mode) { + //If view already defines a mode (eg. controller is being + // initialized after navigation), then pre-populate form. + this.setFormFromMode(mode); + var deltas = this.conductorViewService.deltas(); + if (deltas) { + this.setFormFromDeltas(deltas); + } + + } + + this.setFormFromBounds(this.conductor.bounds()); + + // Watch scope for selection of mode or time system by user + this.$scope.$watch('modeModel.selectedKey', this.setMode); + + this.$scope.$on('$destroy', this.destroy); + }; + + TimeConductorController.prototype.destroy = function () { + this.conductor.off('bounds', this.setFormFromBounds); + this.conductor.off('timeSystem', this.changeTimeSystem); + }; + + /** + * Called when the bounds change in the time conductor. Synchronizes + * the bounds values in the time conductor with those in the form + * + * @private + */ + TimeConductorController.prototype.setFormFromBounds = function (bounds) { + this.$scope.boundsModel.start = bounds.start; + this.$scope.boundsModel.end = bounds.end; + if (!this.pendingUpdate) { + this.pendingUpdate = true; + this.$window.requestAnimationFrame(function () { + this.pendingUpdate = false; + this.$scope.$digest(); + }.bind(this)); + } + }; + + /** + * @private + */ + TimeConductorController.prototype.setFormFromMode = function (mode) { + this.$scope.modeModel.selectedKey = mode; + //Synchronize scope with time system on mode + this.$scope.timeSystemModel.options = + this.conductorViewService.availableTimeSystems() + .map(function (t) { + return t.metadata; + }); + }; + + /** + * @private + */ + TimeConductorController.prototype.setFormFromDeltas = function (deltas) { + this.$scope.boundsModel.startDelta = deltas.start; + this.$scope.boundsModel.endDelta = deltas.end; + }; + + /** + * @private + */ + TimeConductorController.prototype.setFormFromTimeSystem = function (timeSystem) { + this.$scope.timeSystemModel.selected = timeSystem; + this.$scope.timeSystemModel.format = timeSystem.formats()[0]; + this.$scope.timeSystemModel.deltaFormat = timeSystem.deltaFormat(); + }; + + + /** + * Called when form values are changed. Synchronizes the form with + * the time conductor + * @param formModel + */ + TimeConductorController.prototype.updateBoundsFromForm = function (boundsModel) { + this.conductor.bounds({ + start: boundsModel.start, + end: boundsModel.end + }); + }; + + /** + * Called when the delta values in the form change. Validates and + * sets the new deltas on the Mode. + * @param boundsModel + * @see TimeConductorMode + */ + TimeConductorController.prototype.updateDeltasFromForm = function (boundsFormModel) { + var deltas = { + start: boundsFormModel.startDelta, + end: boundsFormModel.endDelta + }; + if (this.validation.validateStartDelta(deltas.start) && this.validation.validateEndDelta(deltas.end)) { + //Sychronize deltas between form and mode + this.conductorViewService.deltas(deltas); + } + }; + + /** + * Change the selected Time Conductor mode. This will call destroy + * and initialization functions on the relevant modes, setting + * default values for bound and deltas in the form. + * + * @private + * @param newModeKey + * @param oldModeKey + */ + TimeConductorController.prototype.setMode = function (newModeKey, oldModeKey) { + if (newModeKey !== oldModeKey) { + this.conductorViewService.mode(newModeKey); + this.setFormFromMode(newModeKey); + } + }; + + /** + * Respond to time system selection from UI + * + * Allows time system to be changed by key. This supports selection + * from the menu. Resolves a TimeSystem object and then invokes + * TimeConductorController#setTimeSystem + * @param key + * @see TimeConductorController#setTimeSystem + */ + TimeConductorController.prototype.selectTimeSystemByKey = function (key) { + var selected = this.timeSystems.filter(function (timeSystem) { + return timeSystem.metadata.key === key; + })[0]; + this.conductor.timeSystem(selected, selected.defaults().bounds); + }; + + /** + * Handles time system change from time conductor + * + * Sets the selected time system. Will populate form with the default + * bounds and deltas defined in the selected time system. + * + * @private + * @param newTimeSystem + */ + TimeConductorController.prototype.changeTimeSystem = function (newTimeSystem) { + if (newTimeSystem && (newTimeSystem !== this.$scope.timeSystemModel.selected)) { + if (newTimeSystem.defaults()) { + var deltas = newTimeSystem.defaults().deltas || {start: 0, end: 0}; + var bounds = newTimeSystem.defaults().bounds || {start: 0, end: 0}; + + this.setFormFromDeltas(deltas); + this.setFormFromBounds(bounds); + } + this.setFormFromTimeSystem(newTimeSystem); + } + }; + + return TimeConductorController; + } +); diff --git a/platform/features/conductor-v2/conductor/src/ui/TimeConductorControllerSpec.js b/platform/features/conductor-v2/conductor/src/ui/TimeConductorControllerSpec.js new file mode 100644 index 0000000000..39759c60f5 --- /dev/null +++ b/platform/features/conductor-v2/conductor/src/ui/TimeConductorControllerSpec.js @@ -0,0 +1,335 @@ +/***************************************************************************** + * 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. + *****************************************************************************/ + +define(['./TimeConductorController'], function (TimeConductorController) { + describe("The time conductor controller", function () { + var mockScope; + var mockWindow; + var mockTimeConductor; + var mockConductorViewService; + var mockTimeSystems; + var controller; + + beforeEach(function () { + mockScope = jasmine.createSpyObj("$scope", [ + "$watch", + "$on" + ]); + mockWindow = jasmine.createSpyObj("$window", ["requestAnimationFrame"]); + mockTimeConductor = jasmine.createSpyObj( + "TimeConductor", + [ + "bounds", + "timeSystem", + "on", + "off" + ] + ); + mockTimeConductor.bounds.andReturn({start: undefined, end: undefined}); + + mockConductorViewService = jasmine.createSpyObj( + "ConductorViewService", + [ + "availableModes", + "mode", + "availableTimeSystems", + "deltas" + ] + ); + mockConductorViewService.availableModes.andReturn([]); + mockConductorViewService.availableTimeSystems.andReturn([]); + + mockTimeSystems = []; + }); + + function getListener(name) { + return mockTimeConductor.on.calls.filter(function (call) { + return call.args[0] === name; + })[0].args[1]; + } + + describe("", function () { + beforeEach(function () { + controller = new TimeConductorController( + mockScope, + mockWindow, + mockTimeConductor, + mockConductorViewService, + mockTimeSystems + ); + }); + + }); + + describe("when time conductor state changes", function () { + var mockFormat; + var mockDeltaFormat; + var defaultBounds; + var defaultDeltas; + var mockDefaults; + var timeSystem; + var tsListener; + + beforeEach(function () { + mockFormat = {}; + mockDeltaFormat = {}; + defaultBounds = { + start: 2, + end: 3 + }; + defaultDeltas = { + start: 10, + end: 20 + }; + mockDefaults = { + deltas: defaultDeltas, + bounds: defaultBounds + }; + timeSystem = { + formats: function () { + return [mockFormat]; + }, + deltaFormat: function () { + return mockDeltaFormat; + }, + defaults: function () { + return mockDefaults; + } + }; + + controller = new TimeConductorController( + mockScope, + mockWindow, + mockTimeConductor, + mockConductorViewService, + mockTimeSystems + ); + + tsListener = getListener("timeSystem"); + }); + + it("listens for changes to conductor state", function () { + expect(mockTimeConductor.on).toHaveBeenCalledWith("timeSystem", controller.changeTimeSystem); + expect(mockTimeConductor.on).toHaveBeenCalledWith("bounds", controller.setFormFromBounds); + }); + + it("deregisters conductor listens when scope is destroyed", function () { + expect(mockScope.$on).toHaveBeenCalledWith("$destroy", controller.destroy); + + controller.destroy(); + expect(mockTimeConductor.off).toHaveBeenCalledWith("timeSystem", controller.changeTimeSystem); + expect(mockTimeConductor.off).toHaveBeenCalledWith("bounds", controller.setFormFromBounds); + }); + + it("when time system changes, sets time system on scope", function () { + expect(tsListener).toBeDefined(); + tsListener(timeSystem); + + expect(mockScope.timeSystemModel).toBeDefined(); + expect(mockScope.timeSystemModel.selected).toBe(timeSystem); + expect(mockScope.timeSystemModel.format).toBe(mockFormat); + expect(mockScope.timeSystemModel.deltaFormat).toBe(mockDeltaFormat); + }); + + it("when time system changes, sets defaults on scope", function () { + expect(tsListener).toBeDefined(); + tsListener(timeSystem); + + expect(mockScope.boundsModel.start).toEqual(defaultBounds.start); + expect(mockScope.boundsModel.end).toEqual(defaultBounds.end); + + expect(mockScope.boundsModel.startDelta).toEqual(defaultDeltas.start); + expect(mockScope.boundsModel.endDelta).toEqual(defaultDeltas.end); + }); + + it("when bounds change, sets them on scope", function () { + var bounds = { + start: 1, + end: 2 + }; + + var boundsListener = getListener("bounds"); + expect(boundsListener).toBeDefined(); + boundsListener(bounds); + + expect(mockScope.boundsModel).toBeDefined(); + expect(mockScope.boundsModel.start).toEqual(bounds.start); + expect(mockScope.boundsModel.end).toEqual(bounds.end); + }); + }); + + describe("when user makes changes from UI", function () { + var mode = "realtime"; + var ts1Metadata; + var ts2Metadata; + var ts3Metadata; + var mockTimeSystemConstructors; + + beforeEach(function () { + mode = "realtime"; + ts1Metadata = { + 'key': 'ts1', + 'name': 'Time System One', + 'cssClass': 'cssClassOne' + }; + ts2Metadata = { + 'key': 'ts2', + 'name': 'Time System Two', + 'cssClass': 'cssClassTwo' + }; + ts3Metadata = { + 'key': 'ts3', + 'name': 'Time System Three', + 'cssClass': 'cssClassThree' + }; + mockTimeSystems = [ + { + metadata: ts1Metadata + }, + { + metadata: ts2Metadata + }, + { + metadata: ts3Metadata + } + ]; + + //Wrap in mock constructors + mockTimeSystemConstructors = mockTimeSystems.map(function (mockTimeSystem) { + return function () { + return mockTimeSystem; + }; + }); + }); + + it("sets the mode on scope", function () { + controller = new TimeConductorController( + mockScope, + mockWindow, + mockTimeConductor, + mockConductorViewService, + mockTimeSystemConstructors + ); + + mockConductorViewService.availableTimeSystems.andReturn(mockTimeSystems); + controller.setMode(mode); + + expect(mockScope.modeModel.selectedKey).toEqual(mode); + }); + + it("sets available time systems on scope when mode changes", function () { + controller = new TimeConductorController( + mockScope, + mockWindow, + mockTimeConductor, + mockConductorViewService, + mockTimeSystemConstructors + ); + + mockConductorViewService.availableTimeSystems.andReturn(mockTimeSystems); + controller.setMode(mode); + + expect(mockScope.timeSystemModel.options.length).toEqual(3); + expect(mockScope.timeSystemModel.options[0]).toEqual(ts1Metadata); + expect(mockScope.timeSystemModel.options[1]).toEqual(ts2Metadata); + expect(mockScope.timeSystemModel.options[2]).toEqual(ts3Metadata); + }); + + it("sets bounds on the time conductor", function () { + var formModel = { + start: 1, + end: 10 + }; + + + controller = new TimeConductorController( + mockScope, + mockWindow, + mockTimeConductor, + mockConductorViewService, + mockTimeSystemConstructors + ); + + controller.updateBoundsFromForm(formModel); + expect(mockTimeConductor.bounds).toHaveBeenCalledWith(formModel); + }); + + it("applies deltas when they change in form", function () { + var deltas = { + start: 1000, + end: 2000 + }; + var formModel = { + startDelta: deltas.start, + endDelta: deltas.end + }; + + controller = new TimeConductorController( + mockScope, + mockWindow, + mockTimeConductor, + mockConductorViewService, + mockTimeSystemConstructors + ); + + controller.updateDeltasFromForm(formModel); + expect(mockConductorViewService.deltas).toHaveBeenCalledWith(deltas); + }); + + it("sets the time system on the time conductor", function () { + var defaultBounds = { + start: 5, + end: 6 + }; + var timeSystem = { + metadata: { + key: 'testTimeSystem' + }, + defaults: function () { + return { + bounds: defaultBounds + }; + } + }; + + mockTimeSystems = [ + // Wrap as constructor function + function () { + return timeSystem; + } + ]; + + controller = new TimeConductorController( + mockScope, + mockWindow, + mockTimeConductor, + mockConductorViewService, + mockTimeSystems + ); + + controller.selectTimeSystemByKey('testTimeSystem'); + expect(mockTimeConductor.timeSystem).toHaveBeenCalledWith(timeSystem, defaultBounds); + }); + }); + + }); +}); diff --git a/platform/features/conductor-v2/conductor/src/ui/TimeConductorMode.js b/platform/features/conductor-v2/conductor/src/ui/TimeConductorMode.js new file mode 100644 index 0000000000..1f9d3656af --- /dev/null +++ b/platform/features/conductor-v2/conductor/src/ui/TimeConductorMode.js @@ -0,0 +1,201 @@ +/***************************************************************************** + * 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. + *****************************************************************************/ + +define( + [], + function () { + + /** + * Supports mode-specific time conductor behavior. + * + * @constructor + * @param {TimeConductorMetadata} metadata + */ + function TimeConductorMode(metadata, conductor, timeSystems) { + this.conductor = conductor; + + this.mdata = metadata; + this.dlts = undefined; + this.source = undefined; + this.sourceUnlisten = undefined; + this.systems = timeSystems; + this.availableSources = undefined; + this.changeTimeSystem = this.changeTimeSystem.bind(this); + this.tick = this.tick.bind(this); + + //Set the time system initially + if (conductor.timeSystem()) { + this.changeTimeSystem(conductor.timeSystem()); + } + + //Listen for subsequent changes to time system + conductor.on('timeSystem', this.changeTimeSystem); + + if (metadata.key === 'fixed') { + //Fixed automatically supports all time systems + this.availableSystems = timeSystems; + } else { + this.availableSystems = timeSystems.filter(function (timeSystem) { + //Only include time systems that have tick sources that + // support the current mode + return timeSystem.tickSources().some(function (tickSource) { + return metadata.key === tickSource.metadata.mode; + }); + }); + } + } + + /** + * Get or set the currently selected time system + * @param timeSystem + * @returns {TimeSystem} the currently selected time system + */ + TimeConductorMode.prototype.changeTimeSystem = function (timeSystem) { + // On time system change, apply default deltas + var defaults = timeSystem.defaults() || { + bounds: { + start: 0, + end: 0 + }, + deltas: { + start: 0, + end: 0 + } + }; + + this.conductor.bounds(defaults.bounds); + this.deltas(defaults.deltas); + + // Tick sources are mode-specific, so restrict tick sources to only those supported by the current mode. + var key = this.mdata.key; + var tickSources = timeSystem.tickSources(); + if (tickSources) { + this.availableSources = tickSources.filter(function (source) { + return source.metadata.mode === key; + }); + } + + // Set an appropriate tick source from the new time system + this.tickSource(this.availableTickSources(timeSystem)[0]); + }; + + /** + * @returns {ModeMetadata} + */ + TimeConductorMode.prototype.metadata = function () { + return this.mdata; + }; + + TimeConductorMode.prototype.availableTimeSystems = function () { + return this.availableSystems; + }; + + /** + * Tick sources are mode-specific. This returns a filtered list of the tick sources available in the currently selected mode + * @param timeSystem + * @returns {Array.} + */ + TimeConductorMode.prototype.availableTickSources = function (timeSystem) { + return this.availableSources; + }; + + /** + * Get or set tick source. Setting tick source will also start + * listening to it and unlisten from any existing tick source + * @param tickSource + * @returns {TickSource} + */ + TimeConductorMode.prototype.tickSource = function (tickSource) { + if (arguments.length > 0) { + if (this.sourceUnlisten) { + this.sourceUnlisten(); + } + this.source = tickSource; + if (tickSource) { + this.sourceUnlisten = tickSource.listen(this.tick); + //Now following a tick source + this.conductor.follow(true); + } else { + this.conductor.follow(false); + } + } + return this.source; + }; + + TimeConductorMode.prototype.destroy = function () { + this.conductor.off('timeSystem', this.changeTimeSystem); + + if (this.sourceUnlisten) { + this.sourceUnlisten(); + } + }; + + /** + * @private + * @param {number} time some value that is valid in the current TimeSystem + */ + TimeConductorMode.prototype.tick = function (time) { + var deltas = this.deltas(); + var startTime = time; + var endTime = time; + + if (deltas) { + startTime = time - deltas.start; + endTime = time + deltas.end; + } + this.conductor.bounds({ + start: startTime, + end: endTime + }); + }; + + /** + * Get or set the current value for the deltas used by this time system. + * On change, the new deltas will be used to calculate and set the + * bounds on the time conductor. + * @param deltas + * @returns {TimeSystemDeltas} + */ + TimeConductorMode.prototype.deltas = function (deltas) { + if (arguments.length !== 0) { + var oldEnd = this.conductor.bounds().end; + + if (this.dlts && this.dlts.end !== undefined) { + //Calculate the previous raw end value (without delta) + oldEnd = oldEnd - this.dlts.end; + } + + this.dlts = deltas; + + var newBounds = { + start: oldEnd - this.dlts.start, + end: oldEnd + this.dlts.end + }; + + this.conductor.bounds(newBounds); + } + return this.dlts; + }; + + return TimeConductorMode; + } +); diff --git a/platform/features/conductor-v2/conductor/src/ui/TimeConductorModeSpec.js b/platform/features/conductor-v2/conductor/src/ui/TimeConductorModeSpec.js new file mode 100644 index 0000000000..6ea5ce79c2 --- /dev/null +++ b/platform/features/conductor-v2/conductor/src/ui/TimeConductorModeSpec.js @@ -0,0 +1,210 @@ +/***************************************************************************** + * 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. + *****************************************************************************/ + +define(['./TimeConductorMode'], function (TimeConductorMode) { + describe("The Time Conductor Mode", function () { + var mockTimeConductor, + fixedModeMetaData, + mockTimeSystems, + fixedTimeSystem, + + realtimeModeMetaData, + realtimeTimeSystem, + mockTickSource, + + mockBounds, + mode; + + beforeEach(function () { + fixedModeMetaData = { + key: "fixed" + }; + realtimeModeMetaData = { + key: "realtime" + }; + mockBounds = { + start: 0, + end: 1 + }; + + fixedTimeSystem = jasmine.createSpyObj("timeSystem", [ + "defaults", + "tickSources" + ]); + fixedTimeSystem.tickSources.andReturn([]); + + mockTickSource = jasmine.createSpyObj("tickSource", [ + "listen" + ]); + mockTickSource.metadata = { + mode: "realtime" + }; + realtimeTimeSystem = jasmine.createSpyObj("realtimeTimeSystem", [ + "defaults", + "tickSources" + ]); + realtimeTimeSystem.tickSources.andReturn([mockTickSource]); + + //Do not return any time systems initially for a default + // construction configuration that works without any additional work + mockTimeSystems = []; + + mockTimeConductor = jasmine.createSpyObj("timeConductor", [ + "bounds", + "timeSystem", + "on", + "off", + "follow" + ]); + mockTimeConductor.bounds.andReturn(mockBounds); + }); + + it("Reacts to changes in conductor time system", function () { + mode = new TimeConductorMode(fixedModeMetaData, mockTimeConductor, mockTimeSystems); + expect(mockTimeConductor.on).toHaveBeenCalledWith("timeSystem", mode.changeTimeSystem); + }); + + it("Stops listening to time system changes on destroy", function () { + mode = new TimeConductorMode(fixedModeMetaData, mockTimeConductor, mockTimeSystems); + mode.destroy(); + expect(mockTimeConductor.off).toHaveBeenCalledWith("timeSystem", mode.changeTimeSystem); + }); + + it("Filters available time systems to those with tick sources that" + + " support this mode", function () { + mockTimeSystems = [fixedTimeSystem, realtimeTimeSystem]; + mode = new TimeConductorMode(realtimeModeMetaData, mockTimeConductor, mockTimeSystems); + + var availableTimeSystems = mode.availableTimeSystems(); + expect(availableTimeSystems.length).toBe(1); + expect(availableTimeSystems.indexOf(fixedTimeSystem)).toBe(-1); + expect(availableTimeSystems.indexOf(realtimeTimeSystem)).toBe(0); + }); + + describe("Changing the time system", function () { + var defaults; + + beforeEach(function () { + defaults = { + bounds: { + start: 1, + end: 2 + }, + deltas: { + start: 3, + end: 4 + } + }; + + fixedTimeSystem.defaults.andReturn(defaults); + + }); + it ("sets defaults from new time system", function () { + mode = new TimeConductorMode(fixedModeMetaData, mockTimeConductor, mockTimeSystems); + spyOn(mode, "deltas"); + mode.deltas.andCallThrough(); + + mode.changeTimeSystem(fixedTimeSystem); + expect(mockTimeConductor.bounds).toHaveBeenCalledWith(defaults.bounds); + expect(mode.deltas).toHaveBeenCalledWith(defaults.deltas); + }); + it ("If a tick source is available, sets the tick source", function () { + mode = new TimeConductorMode(realtimeModeMetaData, mockTimeConductor, mockTimeSystems); + mode.changeTimeSystem(realtimeTimeSystem); + + var currentTickSource = mode.tickSource(); + expect(currentTickSource).toBe(mockTickSource); + }); + }); + + describe("Setting a tick source", function () { + var mockUnlistener; + + beforeEach(function () { + mockUnlistener = jasmine.createSpy("unlistener"); + mockTickSource.listen.andReturn(mockUnlistener); + + mode = new TimeConductorMode(realtimeModeMetaData, mockTimeConductor, mockTimeSystems); + mode.tickSource(mockTickSource); + }); + + it ("Unlistens from old tick source", function () { + mode.tickSource(mockTickSource); + expect(mockUnlistener).toHaveBeenCalled(); + }); + + it ("Listens to new tick source", function () { + expect(mockTickSource.listen).toHaveBeenCalledWith(mode.tick); + }); + + it ("Sets 'follow' state on time conductor", function () { + expect(mockTimeConductor.follow).toHaveBeenCalledWith(true); + }); + + it ("on destroy, unlistens from tick source", function () { + mode.destroy(); + expect(mockUnlistener).toHaveBeenCalled(); + }); + }); + + describe("setting deltas", function () { + beforeEach(function () { + mode = new TimeConductorMode(realtimeModeMetaData, mockTimeConductor, mockTimeSystems); + }); + it ("sets the bounds on the time conductor based on new delta" + + " values", function () { + var deltas = { + start: 20, + end: 10 + }; + + mode.deltas(deltas); + + expect(mockTimeConductor.bounds).toHaveBeenCalledWith({ + start: mockBounds.end - deltas.start, + end: mockBounds.end + deltas.end + }); + }); + }); + + describe("ticking", function () { + beforeEach(function () { + mode = new TimeConductorMode(realtimeModeMetaData, mockTimeConductor, mockTimeSystems); + }); + it ("sets bounds based on current delta values", function () { + var deltas = { + start: 20, + end: 10 + }; + var time = 100; + + mode.deltas(deltas); + mode.tick(time); + + expect(mockTimeConductor.bounds).toHaveBeenCalledWith({ + start: time - deltas.start, + end: time + deltas.end + }); + }); + }); + }); +}); diff --git a/platform/features/conductor-v2/conductor/src/ui/TimeConductorValidation.js b/platform/features/conductor-v2/conductor/src/ui/TimeConductorValidation.js new file mode 100644 index 0000000000..3778c26a21 --- /dev/null +++ b/platform/features/conductor-v2/conductor/src/ui/TimeConductorValidation.js @@ -0,0 +1,69 @@ +/***************************************************************************** + * 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. + *****************************************************************************/ + +define( + [], + function () { + + /** + * Form validation for the TimeConductorController. + * @param conductor + * @constructor + */ + function TimeConductorValidation(conductor) { + var self = this; + this.conductor = conductor; + + /* + * Bind all class functions to 'this' + */ + Object.keys(TimeConductorValidation.prototype).filter(function (key) { + return typeof TimeConductorValidation.prototype[key] === 'function'; + }).forEach(function (key) { + self[key] = self[key].bind(self); + }); + } + + /** + * Validation methods below are invoked directly from controls in the TimeConductor form + */ + TimeConductorValidation.prototype.validateStart = function (start) { + var bounds = this.conductor.bounds(); + return this.conductor.validateBounds({start: start, end: bounds.end}) === true; + }; + + TimeConductorValidation.prototype.validateEnd = function (end) { + var bounds = this.conductor.bounds(); + return this.conductor.validateBounds({start: bounds.start, end: end}) === true; + }; + + TimeConductorValidation.prototype.validateStartDelta = function (startDelta) { + return !isNaN(startDelta) && startDelta > 0; + }; + + TimeConductorValidation.prototype.validateEndDelta = function (endDelta) { + return !isNaN(endDelta) && endDelta >= 0; + }; + + return TimeConductorValidation; + } +); diff --git a/platform/features/conductor-v2/conductor/src/ui/TimeConductorValidationSpec.js b/platform/features/conductor-v2/conductor/src/ui/TimeConductorValidationSpec.js new file mode 100644 index 0000000000..833ba30993 --- /dev/null +++ b/platform/features/conductor-v2/conductor/src/ui/TimeConductorValidationSpec.js @@ -0,0 +1,73 @@ +/***************************************************************************** + * 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. + *****************************************************************************/ + +define(['./TimeConductorValidation'], function (TimeConductorValidation) { + describe("The Time Conductor Validation class", function () { + var timeConductorValidation, + mockTimeConductor; + + beforeEach(function () { + mockTimeConductor = jasmine.createSpyObj("timeConductor", [ + "validateBounds", + "bounds" + ]); + timeConductorValidation = new TimeConductorValidation(mockTimeConductor); + }); + + describe("Validates start and end values using Time Conductor", function () { + beforeEach(function () { + var mockBounds = { + start: 10, + end: 20 + }; + + mockTimeConductor.bounds.andReturn(mockBounds); + + }); + it("Validates start values using Time Conductor", function () { + var startValue = 30; + timeConductorValidation.validateStart(startValue); + expect(mockTimeConductor.validateBounds).toHaveBeenCalled(); + }); + it("Validates end values using Time Conductor", function () { + var endValue = 40; + timeConductorValidation.validateEnd(endValue); + expect(mockTimeConductor.validateBounds).toHaveBeenCalled(); + }); + }); + + it("Validates that start delta is valid number > 0", function () { + expect(timeConductorValidation.validateStartDelta(-1)).toBe(false); + expect(timeConductorValidation.validateStartDelta("abc")).toBe(false); + expect(timeConductorValidation.validateStartDelta("1")).toBe(true); + expect(timeConductorValidation.validateStartDelta(1)).toBe(true); + }); + + it("Validates that end delta is valid number >= 0", function () { + expect(timeConductorValidation.validateEndDelta(-1)).toBe(false); + expect(timeConductorValidation.validateEndDelta("abc")).toBe(false); + expect(timeConductorValidation.validateEndDelta("1")).toBe(true); + expect(timeConductorValidation.validateEndDelta(0)).toBe(true); + expect(timeConductorValidation.validateEndDelta(1)).toBe(true); + }); + }); +}); diff --git a/platform/features/conductor-v2/conductor/src/ui/TimeConductorViewService.js b/platform/features/conductor-v2/conductor/src/ui/TimeConductorViewService.js new file mode 100644 index 0000000000..8cbc349520 --- /dev/null +++ b/platform/features/conductor-v2/conductor/src/ui/TimeConductorViewService.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. + *****************************************************************************/ + +define( + [ + './TimeConductorMode' + ], + function (TimeConductorMode) { + + /** + * A class representing the state of the time conductor view. This + * exposes details of the UI that are not represented on the + * TimeConductor API itself such as modes and deltas. + * + * @param conductor + * @param timeSystems + * @constructor + */ + function TimeConductorViewService(conductor, timeSystems) { + this.systems = timeSystems.map(function (timeSystemConstructor) { + return timeSystemConstructor(); + }); + + this.conductor = conductor; + this.currentMode = undefined; + + /** + * @typedef {object} ModeMetadata + * @property {string} key A unique identifying key for this mode + * @property {string} cssClass The css class for the glyph + * representing this mode + * @property {string} label A short label for this mode + * @property {string} name A longer name for the mode + * @property {string} description A description of the mode + */ + this.availModes = { + 'fixed': { + key: 'fixed', + cssclass: 'icon-calendar', + label: 'Fixed', + name: 'Fixed Timespan Mode', + description: 'Query and explore data that falls between two fixed datetimes.' + } + }; + + function hasTickSource(sourceType, timeSystem) { + return timeSystem.tickSources().some(function (tickSource) { + return tickSource.metadata.mode === sourceType; + }); + } + + var timeSystemsForMode = function (sourceType) { + return this.systems.filter(hasTickSource.bind(this, sourceType)); + }.bind(this); + + //Only show 'real-time mode' if appropriate time systems available + if (timeSystemsForMode('realtime').length > 0) { + var realtimeMode = { + key: 'realtime', + cssclass: 'icon-clock', + label: 'Real-time', + name: 'Real-time Mode', + description: 'Monitor real-time streaming data as it comes in. The Time Conductor and displays will automatically advance themselves based on a UTC clock.' + }; + this.availModes[realtimeMode.key] = realtimeMode; + } + + //Only show 'LAD mode' if appropriate time systems available + if (timeSystemsForMode('lad').length > 0) { + var ladMode = { + key: 'lad', + cssclass: 'icon-database', + label: 'LAD', + name: 'LAD Mode', + description: 'Latest Available Data mode monitors real-time streaming data as it comes in. The Time Conductor and displays will only advance when data becomes available.' + }; + this.availModes[ladMode.key] = ladMode; + } + } + + /** + * Getter/Setter for the Time Conductor Mode. Modes determine the + * behavior of the time conductor, especially with regards to the + * bounds and how they change with time. + * + * In fixed mode, the bounds do not change with time, but can be + * modified by the used + * + * In realtime mode, the bounds change with time. Bounds are not + * directly modifiable by the user, however deltas can be. + * + * In Latest Available Data (LAD) mode, the bounds are updated when + * data is received. As with realtime mode the + * + * @param {string} newModeKey One of 'fixed', 'realtime', or 'LAD' + * @returns {string} the current mode, one of 'fixed', 'realtime', + * or 'LAD'. + * + */ + TimeConductorViewService.prototype.mode = function (newModeKey) { + function contains(timeSystems, ts) { + return timeSystems.filter(function (t) { + return t.metadata.key === ts.metadata.key; + }).length > 0; + } + + if (arguments.length === 1) { + var timeSystem = this.conductor.timeSystem(); + var modes = this.availableModes(); + var modeMetaData = modes[newModeKey]; + + if (this.currentMode) { + this.currentMode.destroy(); + } + this.currentMode = new TimeConductorMode(modeMetaData, this.conductor, this.systems); + + // If no time system set on time conductor, or the currently selected time system is not available in + // the new mode, default to first available time system + if (!timeSystem || !contains(this.currentMode.availableTimeSystems(), timeSystem)) { + timeSystem = this.currentMode.availableTimeSystems()[0]; + this.conductor.timeSystem(timeSystem, timeSystem.defaults().bounds); + } + } + return this.currentMode ? this.currentMode.metadata().key : undefined; + }; + + /** + * @typedef {object} Delta + * @property {number} start Used to set the start bound of the + * TimeConductor on tick. A positive value that will be subtracted + * from the value provided by a tick source to determine the start + * bound. + * @property {number} end Used to set the end bound of the + * TimeConductor on tick. A positive value that will be added + * from the value provided by a tick source to determine the start + * bound. + */ + /** + * Deltas define the offset from the latest time value provided by + * the current tick source. Deltas are only valid in realtime or LAD + * modes. + * + * Realtime mode: + * - start: A time in ms before now which will be used to + * determine the 'start' bound on tick + * - end: A time in ms after now which will be used to determine + * the 'end' bound on tick + * + * LAD mode: + * - start: A time in ms before the timestamp of the last data + * received which will be used to determine the 'start' bound on + * tick + * - end: A time in ms after the timestamp of the last data received + * which will be used to determine the 'end' bound on tick + * @returns {Delta} current value of the deltas + */ + TimeConductorViewService.prototype.deltas = function () { + //Deltas stored on mode. Use .apply to preserve arguments + return this.currentMode.deltas.apply(this.currentMode, arguments); + }; + + /** + * Availability of modes depends on the time systems and tick + * sources available. For example, Latest Available Data mode will + * not be available if there are no time systems and tick sources + * that support LAD mode. + * @returns {ModeMetadata[]} + */ + TimeConductorViewService.prototype.availableModes = function () { + return this.availModes; + }; + + /** + * Availability of time systems depends on the currently selected + * mode. Time systems and tick sources are mode dependent + */ + TimeConductorViewService.prototype.availableTimeSystems = function () { + return this.currentMode.availableTimeSystems(); + }; + + return TimeConductorViewService; + } +); diff --git a/platform/features/conductor-v2/conductor/src/ui/TimeConductorViewServiceSpec.js b/platform/features/conductor-v2/conductor/src/ui/TimeConductorViewServiceSpec.js new file mode 100644 index 0000000000..39e7810320 --- /dev/null +++ b/platform/features/conductor-v2/conductor/src/ui/TimeConductorViewServiceSpec.js @@ -0,0 +1,185 @@ +/***************************************************************************** + * 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. + *****************************************************************************/ + +define(['./TimeConductorViewService'], function (TimeConductorViewService) { + describe("The Time Conductor view service", function () { + var mockTimeConductor; + var basicTimeSystem; + var tickingTimeSystem; + var viewService; + var tickingTimeSystemDefaults; + + function mockConstructor(object) { + return function () { + return object; + }; + } + + beforeEach(function () { + mockTimeConductor = jasmine.createSpyObj("timeConductor", [ + "timeSystem", + "bounds", + "follow", + "on", + "off" + ]); + + basicTimeSystem = jasmine.createSpyObj("basicTimeSystem", [ + "tickSources", + "defaults" + ]); + basicTimeSystem.metadata = { + key: "basic" + }; + basicTimeSystem.tickSources.andReturn([]); + basicTimeSystem.defaults.andReturn({ + bounds: { + start: 0, + end: 1 + }, + deltas: { + start: 0, + end: 0 + } + }); + //Initialize conductor + mockTimeConductor.timeSystem.andReturn(basicTimeSystem); + mockTimeConductor.bounds.andReturn({start: 0, end: 1}); + + tickingTimeSystem = jasmine.createSpyObj("tickingTimeSystem", [ + "tickSources", + "defaults" + ]); + tickingTimeSystem.metadata = { + key: "ticking" + }; + tickingTimeSystemDefaults = { + bounds: { + start: 100, + end: 200 + }, + deltas: { + start: 1000, + end: 500 + } + }; + tickingTimeSystem.defaults.andReturn(tickingTimeSystemDefaults); + }); + + it("At a minimum supports fixed mode", function () { + var mockTimeSystems = [mockConstructor(basicTimeSystem)]; + viewService = new TimeConductorViewService(mockTimeConductor, mockTimeSystems); + + var availableModes = viewService.availableModes(); + expect(availableModes.fixed).toBeDefined(); + }); + + it("Supports realtime mode if appropriate tick source(s) availables", function () { + var mockTimeSystems = [mockConstructor(tickingTimeSystem)]; + var mockRealtimeTickSource = { + metadata: { + mode: 'realtime' + } + }; + tickingTimeSystem.tickSources.andReturn([mockRealtimeTickSource]); + + viewService = new TimeConductorViewService(mockTimeConductor, mockTimeSystems); + + var availableModes = viewService.availableModes(); + expect(availableModes.realtime).toBeDefined(); + }); + + it("Supports LAD mode if appropriate tick source(s) available", function () { + var mockTimeSystems = [mockConstructor(tickingTimeSystem)]; + var mockLADTickSource = { + metadata: { + mode: 'lad' + } + }; + tickingTimeSystem.tickSources.andReturn([mockLADTickSource]); + + viewService = new TimeConductorViewService(mockTimeConductor, mockTimeSystems); + + var availableModes = viewService.availableModes(); + expect(availableModes.lad).toBeDefined(); + }); + + describe("when mode is changed", function () { + + it("destroys previous mode", function () { + var mockTimeSystems = [mockConstructor(basicTimeSystem)]; + + var oldMode = jasmine.createSpyObj("conductorMode", [ + "destroy" + ]); + + viewService = new TimeConductorViewService(mockTimeConductor, mockTimeSystems); + viewService.currentMode = oldMode; + viewService.mode('fixed'); + expect(oldMode.destroy).toHaveBeenCalled(); + }); + + describe("the time system", function () { + it("is retained if available in new mode", function () { + var mockTimeSystems = [mockConstructor(basicTimeSystem), mockConstructor(tickingTimeSystem)]; + var mockRealtimeTickSource = { + metadata: { + mode: 'realtime' + }, + listen: function () {} + }; + tickingTimeSystem.tickSources.andReturn([mockRealtimeTickSource]); + + viewService = new TimeConductorViewService(mockTimeConductor, mockTimeSystems); + + //Set time system to one known to support realtime mode + mockTimeConductor.timeSystem.andReturn(tickingTimeSystem); + + //Select realtime mode + mockTimeConductor.timeSystem.reset(); + viewService.mode('realtime'); + expect(mockTimeConductor.timeSystem).not.toHaveBeenCalledWith(tickingTimeSystem, tickingTimeSystemDefaults.bounds); + }); + it("is defaulted if selected time system not available in new mode", function () { + var mockTimeSystems = [mockConstructor(basicTimeSystem), mockConstructor(tickingTimeSystem)]; + var mockRealtimeTickSource = { + metadata: { + mode: 'realtime' + }, + listen: function () {} + }; + tickingTimeSystem.tickSources.andReturn([mockRealtimeTickSource]); + + viewService = new TimeConductorViewService(mockTimeConductor, mockTimeSystems); + + //Set time system to one known to not support realtime mode + mockTimeConductor.timeSystem.andReturn(basicTimeSystem); + + //Select realtime mode + mockTimeConductor.timeSystem.reset(); + viewService.mode('realtime'); + expect(mockTimeConductor.timeSystem).toHaveBeenCalledWith(tickingTimeSystem, tickingTimeSystemDefaults.bounds); + }); + }); + }); + }); +}); diff --git a/platform/features/conductor-v2/utcTimeSystem/bundle.js b/platform/features/conductor-v2/utcTimeSystem/bundle.js new file mode 100644 index 0000000000..5b13e605d4 --- /dev/null +++ b/platform/features/conductor-v2/utcTimeSystem/bundle.js @@ -0,0 +1,40 @@ +/***************************************************************************** + * 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. + *****************************************************************************/ + +define([ + "./src/UTCTimeSystem", + 'legacyRegistry' +], function ( + UTCTimeSystem, + legacyRegistry +) { + legacyRegistry.register("platform/features/conductor-v2/utcTimeSystem", { + "extensions": { + "timeSystems": [ + { + "implementation": UTCTimeSystem, + "depends": ["$timeout"] + } + ] + } + }); +}); diff --git a/platform/features/conductor-v2/utcTimeSystem/src/UTCTimeSystem.js b/platform/features/conductor-v2/utcTimeSystem/src/UTCTimeSystem.js new file mode 100644 index 0000000000..b6e969c3eb --- /dev/null +++ b/platform/features/conductor-v2/utcTimeSystem/src/UTCTimeSystem.js @@ -0,0 +1,78 @@ +/***************************************************************************** + * 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. + *****************************************************************************/ + +define([ + '../../conductor/src/timeSystems/TimeSystem', + '../../conductor/src/timeSystems/LocalClock' +], function (TimeSystem, LocalClock) { + var FIFTEEN_MINUTES = 15 * 60 * 1000, + DEFAULT_PERIOD = 1000; + + /** + * This time system supports UTC dates and provides a ticking clock source. + * @implements TimeSystem + * @constructor + */ + function UTCTimeSystem($timeout) { + TimeSystem.call(this); + + /** + * Some metadata, which will be used to identify the time system in + * the UI + * @type {{key: string, name: string, cssclass: string}} + */ + this.metadata = { + 'key': 'utc', + 'name': 'UTC', + 'cssclass': 'icon-clock' + }; + + this.fmts = ['utc']; + this.sources = [new LocalClock($timeout, DEFAULT_PERIOD)]; + } + + UTCTimeSystem.prototype = Object.create(TimeSystem.prototype); + + UTCTimeSystem.prototype.formats = function () { + return this.fmts; + }; + + UTCTimeSystem.prototype.deltaFormat = function () { + return 'duration'; + }; + + UTCTimeSystem.prototype.tickSources = function () { + return this.sources; + }; + + UTCTimeSystem.prototype.defaults = function (key) { + var now = Math.ceil(Date.now() / 1000) * 1000; + return { + key: 'utc-default', + name: 'UTC time system defaults', + deltas: {start: FIFTEEN_MINUTES, end: 0}, + bounds: {start: now - FIFTEEN_MINUTES, end: now} + }; + }; + + return UTCTimeSystem; +}); diff --git a/platform/features/conductor-v2/utcTimeSystem/src/UTCTimeSystemSpec.js b/platform/features/conductor-v2/utcTimeSystem/src/UTCTimeSystemSpec.js new file mode 100644 index 0000000000..808eade72e --- /dev/null +++ b/platform/features/conductor-v2/utcTimeSystem/src/UTCTimeSystemSpec.js @@ -0,0 +1,46 @@ +/***************************************************************************** + * 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. + *****************************************************************************/ + +define(['./UTCTimeSystem'], function (UTCTimeSystem) { + describe("The UTCTimeSystem class", function () { + var timeSystem, + mockTimeout; + + beforeEach(function () { + mockTimeout = jasmine.createSpy("timeout"); + timeSystem = new UTCTimeSystem(mockTimeout); + }); + + it("defines at least one format", function () { + expect(timeSystem.formats().length).toBeGreaterThan(0); + }); + + it("defines a tick source", function () { + var tickSources = timeSystem.tickSources(); + expect(tickSources.length).toBeGreaterThan(0); + }); + + it("defines some defaults", function () { + expect(timeSystem.defaults()).toBeDefined(); + }); + }); +}); diff --git a/platform/features/conductor/bundle.js b/platform/features/conductor/bundle.js index 5def15a6b9..c1349b4e58 100644 --- a/platform/features/conductor/bundle.js +++ b/platform/features/conductor/bundle.js @@ -47,6 +47,11 @@ define([ ] } ], + "stylesheets": [ + { + "stylesheetUrl": "css/time-conductor.css" + } + ], "components": [ { "type": "decorator", diff --git a/platform/features/conductor/res/sass/time-conductor.scss b/platform/features/conductor/res/sass/time-conductor.scss new file mode 100644 index 0000000000..bc50b42da5 --- /dev/null +++ b/platform/features/conductor/res/sass/time-conductor.scss @@ -0,0 +1,300 @@ +/***************************************************************************** + * 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. + *****************************************************************************/ +@import "bourbon"; + +@import "../../../../commonUI/general/res/sass/constants"; +@import "../../../../commonUI/general/res/sass/mixins"; +@import "../../../../commonUI/general/res/sass/mobile/constants"; +@import "../../../../commonUI/general/res/sass/mobile/mixins"; +@import "../../../../commonUI/themes/espresso/res/sass/constants"; +@import "../../../../commonUI/themes/espresso/res/sass/mixins"; + +$ueTimeConductorH: (33px, 18px, 20px); + +@mixin toiLineHovEffects() { + &:before, + &:after { + background-color: $timeControllerToiLineColorHov; + } +} + +.l-time-controller { + $minW: 500px; + $knobHOffset: 0px; + $knobM: ($sliderKnobW + $knobHOffset) * -1; + $rangeValPad: $interiorMargin; + $rangeValOffset: $sliderKnobW + $interiorMargin; + $timeRangeSliderLROffset: 150px + ($sliderKnobW * 2); + $r1H: nth($ueTimeConductorH,1); + $r2H: nth($ueTimeConductorH,2); + $r3H: nth($ueTimeConductorH,3); + + min-width: $minW; + font-size: 0.8rem; + + .l-time-range-inputs-holder, + .l-time-range-slider-holder, + .l-time-range-ticks-holder + { + box-sizing: border-box; + position: relative; + &:not(:first-child) { + margin-top: $interiorMargin; + } + } + .l-time-range-slider, + .l-time-range-ticks { + @include absPosDefault(0, visible); + left: $timeRangeSliderLROffset; right: $timeRangeSliderLROffset; + } + + .l-time-range-inputs-holder { + border-top: 1px solid $colorInteriorBorder; + padding-top: $interiorMargin; + &.l-flex-row, + .l-flex-row { + @include align-items(center); + .flex-elem { + height: auto; + line-height: normal; + } + } + .type-icon { + font-size: 120%; + vertical-align: middle; + } + .l-time-range-input-w, + .l-time-range-inputs-elem { + margin-right: $interiorMargin; + .lbl { + color: $colorPlotLabelFg; + } + .ui-symbol.icon { + font-size: 11px; + } + } + .l-time-range-input-w { + // Wraps a datetime text input field + position: relative; + input[type="text"] { + width: 200px; + &.picker-icon { + padding-right: 20px; + } + } + .icon-calendar { + position: absolute; + right: 5px; + top: 5px; + } + } + } + + .l-time-range-slider-holder { + height: $r2H; + .range-holder { + box-shadow: none; + background: none; + border: none; + .range { + .toi-line { + $myC: $timeControllerToiLineColor; + $myW: 8px; + @include transform(translateX(50%)); + position: absolute; + top: 0; right: 0; bottom: 0px; left: auto; + width: $myW; + height: auto; + z-index: 2; + &:before { + // Vert line + background-color: $myC; + position: absolute; + content: ""; + top: 0; right: auto; bottom: -10px; left: floor($myW/2) - 1; + width: 1px; + } + } + &:hover .toi-line { + @include toiLineHovEffects; + } + } + } + &:not(:active) { + .knob, + .range { + @include transition-property(left, right); + @include transition-duration(500ms); + @include transition-timing-function(ease-in-out); + } + } + } + + .l-time-range-ticks-holder { + height: $r3H; + .l-time-range-ticks { + border-top: 1px solid $colorTick; + .tick { + background-color: $colorTick; + border:none; + height: 5px; + width: 1px; + margin-left: -1px; + position: absolute; + &:first-child { + margin-left: 0; + } + .l-time-range-tick-label { + @include webkitProp(transform, translateX(-50%)); + color: $colorPlotLabelFg; + display: inline-block; + font-size: 0.7rem; + position: absolute; + top: 5px; + white-space: nowrap; + z-index: 2; + } + } + } + } + + .knob { + z-index: 2; + &:before { + $mTB: 2px; + $grippyW: 3px; + $mLR: ($sliderKnobW - $grippyW)/2; + @include bgStripes($c: pullForward($sliderColorKnob, 20%), $a: 1, $bgsize: 4px, $angle: 0deg); + content: ''; + display: block; + position: absolute; + top: $mTB; right: $mLR; bottom: $mTB; left: $mLR; + } + .range-value { + @include trans-prop-nice-fade(.25s); + font-size: 0.7rem; + position: absolute; + height: $r2H; + line-height: $r2H; + white-space: nowrap; + z-index: 1; + } + &:hover { + .range-value { + color: $sliderColorKnobHov; + } + } + &.knob-l { + margin-left: $knobM; + .range-value { + text-align: right; + right: $rangeValOffset; + } + } + &.knob-r { + margin-right: $knobM; + .range-value { + left: $rangeValOffset; + } + &:hover + .range-holder .range .toi-line { + @include toiLineHovEffects; + } + } + } + + .l-time-domain-selector { + position: absolute; + right: 0px; + top: $interiorMargin; + } + +} + +.s-time-range-val { + border-radius: $controlCr; + background-color: $colorInputBg; + padding: 1px 1px 0 $interiorMargin; +} + +/******************************************************************** MOBILE */ + +@include phoneandtablet { + .l-time-controller { + min-width: 0; + .l-time-range-slider-holder, + .l-time-range-ticks-holder { + display: none; + } + } +} + +@include phone { + .l-time-controller { + .l-time-range-inputs-holder { + &.l-flex-row, + .l-flex-row { + @include align-items(flex-start); + } + .l-time-range-inputs-elem { + &.type-icon { + margin-top: 3px; + } + } + .t-inputs-w, + .l-time-range-inputs-elem { + @include flex-direction(column); + .l-time-range-input-w:not(:first-child) { + &:not(:first-child) { + margin-top: $interiorMargin; + } + margin-right: 0; + } + .l-time-range-inputs-elem { + &.lbl { display: none; } + } + } + } + } +} + +@include phonePortrait { + .l-time-controller { + .l-time-range-inputs-holder { + .t-inputs-w, + .l-time-range-inputs-elem { + @include flex(1 1 auto); + padding-top: 25px; // Make room for the ever lovin' Time Domain Selector + .flex-elem { + @include flex(1 1 auto); + width: 100%; + } + input[type="text"] { + width: 100%; + } + } + } + } + .l-time-domain-selector { + right: auto; + left: 20px; + } +} \ No newline at end of file diff --git a/platform/features/layout/res/templates/layout.html b/platform/features/layout/res/templates/layout.html index ed1f9a50c8..b2ffa16808 100644 --- a/platform/features/layout/res/templates/layout.html +++ b/platform/features/layout/res/templates/layout.html @@ -26,11 +26,10 @@ ng-repeat="childObject in composition" ng-style="controller.getFrameStyle(childObject.getId())"> -
- - -
+ + diff --git a/platform/forms/src/MCTControl.js b/platform/forms/src/MCTControl.js index 680fd9dbe9..a15a6b208f 100644 --- a/platform/forms/src/MCTControl.js +++ b/platform/forms/src/MCTControl.js @@ -71,6 +71,9 @@ define( // Allow controls to trigger blur-like events ngBlur: "&", + // Allow controls to trigger blur-like events + ngMouseup: "&", + // The state of the form value itself ngModel: "=", diff --git a/test-main.js b/test-main.js index 74d554701b..b300560588 100644 --- a/test-main.js +++ b/test-main.js @@ -63,7 +63,8 @@ requirejs.config({ "text": "bower_components/text/text", "uuid": "bower_components/node-uuid/uuid", "zepto": "bower_components/zepto/zepto.min", - "lodash": "bower_components/lodash/lodash" + "lodash": "bower_components/lodash/lodash", + "d3": "bower_components/d3/d3.min" }, "shim": { @@ -87,6 +88,9 @@ requirejs.config({ }, "lodash": { "exports": "lodash" + }, + "d3": { + "exports": "d3" } },