diff --git a/LICENSES.md b/LICENSES.md
index 0224a1ae4d..f7b6c98b11 100644
--- a/LICENSES.md
+++ b/LICENSES.md
@@ -345,6 +345,41 @@ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLI
---
+### moment-duration-format
+
+#### Info
+
+* Link: https://github.com/jsmreese/moment-duration-format
+
+* Version: 1.3.0
+
+* Authors: John Madhavan-Reese
+
+* Description: Duration parsing/formatting
+
+#### License
+
+Copyright 2014 John Madhavan-Reese
+
+Permission is hereby granted, free of charge, to any person obtaining a copy of
+this software and associated documentation files (the "Software"), to deal in
+the Software without restriction, including without limitation the rights to
+use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of
+the Software, and to permit persons to whom the Software is furnished to do so,
+subject to the following conditions:
+
+The above copyright notice and this permission notice shall be included in all
+copies or substantial portions of the Software.
+
+THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
+IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS
+FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR
+COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER
+IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN
+CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
+
+---
+
### Json.NET
#### Info
diff --git a/bundles.json b/bundles.json
index 291553ba11..6ebe71b189 100644
--- a/bundles.json
+++ b/bundles.json
@@ -15,6 +15,7 @@
"platform/containment",
"platform/execution",
"platform/telemetry",
+ "platform/features/clock",
"platform/features/imagery",
"platform/features/layout",
"platform/features/pages",
diff --git a/karma.conf.js b/karma.conf.js
index 47aa3f059b..16175556ae 100644
--- a/karma.conf.js
+++ b/karma.conf.js
@@ -34,7 +34,6 @@ module.exports = function(config) {
// List of files / patterns to load in the browser.
// By default, files are also included in a script tag.
files: [
- '**/moment*',
{pattern: 'example/**/*.js', included: false},
{pattern: 'platform/**/*.js', included: false},
{pattern: 'warp/**/*.js', included: false},
diff --git a/platform/features/clock/bundle.json b/platform/features/clock/bundle.json
new file mode 100644
index 0000000000..5468c46207
--- /dev/null
+++ b/platform/features/clock/bundle.json
@@ -0,0 +1,178 @@
+{
+ "name": "Clocks/Timers",
+ "descriptions": "Domain objects for displaying current & relative times.",
+ "configuration": {
+ "paths": {
+ "moment-duration-format": "moment-duration-format"
+ },
+ "shim": {
+ "moment-duration-format": {
+ "deps": [ "moment" ]
+ }
+ }
+ },
+ "extensions": {
+ "constants": [
+ {
+ "key": "CLOCK_INDICATOR_FORMAT",
+ "value": "YYYY/MM/DD HH:mm:ss"
+ }
+
+ ],
+ "indicators": [
+ {
+ "implementation": "indicators/ClockIndicator.js",
+ "depends": [ "tickerService", "CLOCK_INDICATOR_FORMAT" ],
+ "priority": "preferred"
+ }
+ ],
+ "services": [
+ {
+ "key": "tickerService",
+ "implementation": "services/TickerService.js",
+ "depends": [ "$timeout", "now" ]
+ }
+ ],
+ "controllers": [
+ {
+ "key": "ClockController",
+ "implementation": "controllers/ClockController.js",
+ "depends": [ "$scope", "tickerService" ]
+ },
+ {
+ "key": "TimerController",
+ "implementation": "controllers/TimerController.js",
+ "depends": [ "$scope", "$window", "now" ]
+ },
+ {
+ "key": "RefreshingController",
+ "implementation": "controllers/RefreshingController.js",
+ "depends": [ "$scope", "tickerService" ]
+ }
+ ],
+ "views": [
+ {
+ "key": "clock",
+ "type": "clock",
+ "templateUrl": "templates/clock.html"
+ },
+ {
+ "key": "timer",
+ "type": "timer",
+ "templateUrl": "templates/timer.html"
+ }
+ ],
+ "actions": [
+ {
+ "key": "timer.start",
+ "implementation": "actions/StartTimerAction.js",
+ "depends": ["now"],
+ "category": "contextual",
+ "name": "Start",
+ "glyph": "\u00EF",
+ "priority": "preferred"
+ },
+ {
+ "key": "timer.restart",
+ "implementation": "actions/RestartTimerAction.js",
+ "depends": ["now"],
+ "category": "contextual",
+ "name": "Restart at 0",
+ "glyph": "r",
+ "priority": "preferred"
+ }
+ ],
+ "types": [
+ {
+ "key": "clock",
+ "name": "Clock",
+ "glyph": "C",
+ "features": [ "creation" ],
+ "properties": [
+ {
+ "key": "clockFormat",
+ "name": "Display Format",
+ "control": "composite",
+ "items": [
+ {
+ "control": "select",
+ "options": [
+ {
+ "value": "YYYY/MM/DD hh:mm:ss",
+ "name": "YYYY/MM/DD hh:mm:ss"
+ },
+ {
+ "value": "YYYY/DDD hh:mm:ss",
+ "name": "YYYY/DDD hh:mm:ss"
+ },
+ {
+ "value": "hh:mm:ss",
+ "name": "hh:mm:ss"
+ }
+ ]
+ },
+ {
+ "control": "select",
+ "options": [
+ {
+ "value": "clock12",
+ "name": "12hr"
+ },
+ {
+ "value": "clock24",
+ "name": "24hr"
+ }
+ ]
+ }
+ ]
+ }
+ ],
+ "model": {
+ "clockFormat": [ "YYYY/MM/DD hh:mm:ss", "clock12" ]
+ }
+ },
+ {
+ "key": "timer",
+ "name": "Timer",
+ "glyph": "\u00F5",
+ "features": [ "creation" ],
+ "properties": [
+ {
+ "key": "timestamp",
+ "control": "datetime",
+ "name": "Target"
+ },
+ {
+ "key": "timerFormat",
+ "control": "select",
+ "options": [
+ {
+ "value": "long",
+ "name": "DDD hh:mm:ss"
+ },
+ {
+ "value": "short",
+ "name": "hh:mm:ss"
+ }
+ ]
+ }
+ ],
+ "model": {
+ "timerFormat": "DDD hh:mm:ss"
+ }
+ }
+ ],
+ "licenses": [
+ {
+ "name": "moment-duration-format",
+ "version": "1.3.0",
+ "author": "John Madhavan-Reese",
+ "description": "Duration parsing/formatting",
+ "website": "https://github.com/jsmreese/moment-duration-format",
+ "copyright": "Copyright 2014 John Madhavan-Reese",
+ "license": "license-mit",
+ "link": "https://github.com/jsmreese/moment-duration-format/blob/master/LICENSE"
+ }
+ ]
+ }
+}
diff --git a/platform/features/clock/lib/moment-duration-format.js b/platform/features/clock/lib/moment-duration-format.js
new file mode 100644
index 0000000000..9e85421003
--- /dev/null
+++ b/platform/features/clock/lib/moment-duration-format.js
@@ -0,0 +1,482 @@
+/*! Moment Duration Format v1.3.0
+ * https://github.com/jsmreese/moment-duration-format
+ * Date: 2014-07-15
+ *
+ * Duration format plugin function for the Moment.js library
+ * http://momentjs.com/
+ *
+ * Copyright 2014 John Madhavan-Reese
+ * Released under the MIT license
+ */
+
+(function (root, undefined) {
+
+ // repeatZero(qty)
+ // returns "0" repeated qty times
+ function repeatZero(qty) {
+ var result = "";
+
+ // exit early
+ // if qty is 0 or a negative number
+ // or doesn't coerce to an integer
+ qty = parseInt(qty, 10);
+ if (!qty || qty < 1) { return result; }
+
+ while (qty) {
+ result += "0";
+ qty -= 1;
+ }
+
+ return result;
+ }
+
+ // padZero(str, len [, isRight])
+ // pads a string with zeros up to a specified length
+ // will not pad a string if its length is aready
+ // greater than or equal to the specified length
+ // default output pads with zeros on the left
+ // set isRight to `true` to pad with zeros on the right
+ function padZero(str, len, isRight) {
+ if (str == null) { str = ""; }
+ str = "" + str;
+
+ return (isRight ? str : "") + repeatZero(len - str.length) + (isRight ? "" : str);
+ }
+
+ // isArray
+ function isArray(array) {
+ return Object.prototype.toString.call(array) === "[object Array]";
+ }
+
+ // isObject
+ function isObject(obj) {
+ return Object.prototype.toString.call(obj) === "[object Object]";
+ }
+
+ // findLast
+ function findLast(array, callback) {
+ var index = array.length;
+
+ while (index -= 1) {
+ if (callback(array[index])) { return array[index]; }
+ }
+ }
+
+ // find
+ function find(array, callback) {
+ var index = 0,
+ max = array.length,
+ match;
+
+ if (typeof callback !== "function") {
+ match = callback;
+ callback = function (item) {
+ return item === match;
+ };
+ }
+
+ while (index < max) {
+ if (callback(array[index])) { return array[index]; }
+ index += 1;
+ }
+ }
+
+ // each
+ function each(array, callback) {
+ var index = 0,
+ max = array.length;
+
+ if (!array || !max) { return; }
+
+ while (index < max) {
+ if (callback(array[index], index) === false) { return; }
+ index += 1;
+ }
+ }
+
+ // map
+ function map(array, callback) {
+ var index = 0,
+ max = array.length,
+ ret = [];
+
+ if (!array || !max) { return ret; }
+
+ while (index < max) {
+ ret[index] = callback(array[index], index);
+ index += 1;
+ }
+
+ return ret;
+ }
+
+ // pluck
+ function pluck(array, prop) {
+ return map(array, function (item) {
+ return item[prop];
+ });
+ }
+
+ // compact
+ function compact(array) {
+ var ret = [];
+
+ each(array, function (item) {
+ if (item) { ret.push(item); }
+ });
+
+ return ret;
+ }
+
+ // unique
+ function unique(array) {
+ var ret = [];
+
+ each(array, function (_a) {
+ if (!find(ret, _a)) { ret.push(_a); }
+ });
+
+ return ret;
+ }
+
+ // intersection
+ function intersection(a, b) {
+ var ret = [];
+
+ each(a, function (_a) {
+ each(b, function (_b) {
+ if (_a === _b) { ret.push(_a); }
+ });
+ });
+
+ return unique(ret);
+ }
+
+ // rest
+ function rest(array, callback) {
+ var ret = [];
+
+ each(array, function (item, index) {
+ if (!callback(item)) {
+ ret = array.slice(index);
+ return false;
+ }
+ });
+
+ return ret;
+ }
+
+ // initial
+ function initial(array, callback) {
+ var reversed = array.slice().reverse();
+
+ return rest(reversed, callback).reverse();
+ }
+
+ // extend
+ function extend(a, b) {
+ for (var key in b) {
+ if (b.hasOwnProperty(key)) { a[key] = b[key]; }
+ }
+
+ return a;
+ }
+
+ // define internal moment reference
+ var moment;
+
+ if (typeof require === "function") {
+ try { moment = require('moment'); }
+ catch (e) {}
+ }
+
+ if (!moment && root.moment) {
+ moment = root.moment;
+ }
+
+ if (!moment) {
+ throw "Moment Duration Format cannot find Moment.js";
+ }
+
+ // moment.duration.format([template] [, precision] [, settings])
+ moment.duration.fn.format = function () {
+
+ var tokenizer, tokens, types, typeMap, momentTypes, foundFirst, trimIndex,
+ args = [].slice.call(arguments),
+ settings = extend({}, this.format.defaults),
+ // keep a shadow copy of this moment for calculating remainders
+ remainder = moment.duration(this);
+
+ // add a reference to this duration object to the settings for use
+ // in a template function
+ settings.duration = this;
+
+ // parse arguments
+ each(args, function (arg) {
+ if (typeof arg === "string" || typeof arg === "function") {
+ settings.template = arg;
+ return;
+ }
+
+ if (typeof arg === "number") {
+ settings.precision = arg;
+ return;
+ }
+
+ if (isObject(arg)) {
+ extend(settings, arg);
+ }
+ });
+
+ // types
+ types = settings.types = (isArray(settings.types) ? settings.types : settings.types.split(" "));
+
+ // template
+ if (typeof settings.template === "function") {
+ settings.template = settings.template.apply(settings);
+ }
+
+ // tokenizer regexp
+ tokenizer = new RegExp(map(types, function (type) {
+ return settings[type].source;
+ }).join("|"), "g");
+
+ // token type map function
+ typeMap = function (token) {
+ return find(types, function (type) {
+ return settings[type].test(token);
+ });
+ };
+
+ // tokens array
+ tokens = map(settings.template.match(tokenizer), function (token, index) {
+ var type = typeMap(token),
+ length = token.length;
+
+ return {
+ index: index,
+ length: length,
+
+ // replace escaped tokens with the non-escaped token text
+ token: (type === "escape" ? token.replace(settings.escape, "$1") : token),
+
+ // ignore type on non-moment tokens
+ type: ((type === "escape" || type === "general") ? null : type)
+
+ // calculate base value for all moment tokens
+ //baseValue: ((type === "escape" || type === "general") ? null : this.as(type))
+ };
+ }, this);
+
+ // unique moment token types in the template (in order of descending magnitude)
+ momentTypes = intersection(types, unique(compact(pluck(tokens, "type"))));
+
+ // exit early if there are no momentTypes
+ if (!momentTypes.length) {
+ return pluck(tokens, "token").join("");
+ }
+
+ // calculate values for each token type in the template
+ each(momentTypes, function (momentType, index) {
+ var value, wholeValue, decimalValue, isLeast, isMost;
+
+ // calculate integer and decimal value portions
+ value = remainder.as(momentType);
+ wholeValue = (value > 0 ? Math.floor(value) : Math.ceil(value));
+ decimalValue = value - wholeValue;
+
+ // is this the least-significant moment token found?
+ isLeast = ((index + 1) === momentTypes.length);
+
+ // is this the most-significant moment token found?
+ isMost = (!index);
+
+ // update tokens array
+ // using this algorithm to not assume anything about
+ // the order or frequency of any tokens
+ each(tokens, function (token) {
+ if (token.type === momentType) {
+ extend(token, {
+ value: value,
+ wholeValue: wholeValue,
+ decimalValue: decimalValue,
+ isLeast: isLeast,
+ isMost: isMost
+ });
+
+ if (isMost) {
+ // note the length of the most-significant moment token:
+ // if it is greater than one and forceLength is not set, default forceLength to `true`
+ if (settings.forceLength == null && token.length > 1) {
+ settings.forceLength = true;
+ }
+
+ // rationale is this:
+ // if the template is "h:mm:ss" and the moment value is 5 minutes, the user-friendly output is "5:00", not "05:00"
+ // shouldn't pad the `minutes` token even though it has length of two
+ // if the template is "hh:mm:ss", the user clearly wanted everything padded so we should output "05:00"
+ // if the user wanted the full padded output, they can set `{ trim: false }` to get "00:05:00"
+ }
+ }
+ });
+
+ // update remainder
+ remainder.subtract(wholeValue, momentType);
+ });
+
+ // trim tokens array
+ if (settings.trim) {
+ tokens = (settings.trim === "left" ? rest : initial)(tokens, function (token) {
+ // return `true` if:
+ // the token is not the least moment token (don't trim the least moment token)
+ // the token is a moment token that does not have a value (don't trim moment tokens that have a whole value)
+ return !(token.isLeast || (token.type != null && token.wholeValue));
+ });
+ }
+
+
+ // build output
+
+ // the first moment token can have special handling
+ foundFirst = false;
+
+ // run the map in reverse order if trimming from the right
+ if (settings.trim === "right") {
+ tokens.reverse();
+ }
+
+ tokens = map(tokens, function (token) {
+ var val,
+ decVal;
+
+ if (!token.type) {
+ // if it is not a moment token, use the token as its own value
+ return token.token;
+ }
+
+ // apply negative precision formatting to the least-significant moment token
+ if (token.isLeast && (settings.precision < 0)) {
+ val = (Math.floor(token.wholeValue * Math.pow(10, settings.precision)) * Math.pow(10, -settings.precision)).toString();
+ } else {
+ val = token.wholeValue.toString();
+ }
+
+ // remove negative sign from the beginning
+ val = val.replace(/^\-/, "");
+
+ // apply token length formatting
+ // special handling for the first moment token that is not the most significant in a trimmed template
+ if (token.length > 1 && (foundFirst || token.isMost || settings.forceLength)) {
+ val = padZero(val, token.length);
+ }
+
+ // add decimal value if precision > 0
+ if (token.isLeast && (settings.precision > 0)) {
+ decVal = token.decimalValue.toString().replace(/^\-/, "").split(/\.|e\-/);
+ switch (decVal.length) {
+ case 1:
+ val += "." + padZero(decVal[0], settings.precision, true).slice(0, settings.precision);
+ break;
+
+ case 2:
+ val += "." + padZero(decVal[1], settings.precision, true).slice(0, settings.precision);
+ break;
+
+ case 3:
+ val += "." + padZero(repeatZero((+decVal[2]) - 1) + (decVal[0] || "0") + decVal[1], settings.precision, true).slice(0, settings.precision);
+ break;
+
+ default:
+ throw "Moment Duration Format: unable to parse token decimal value.";
+ }
+ }
+
+ // add a negative sign if the value is negative and token is most significant
+ if (token.isMost && token.value < 0) {
+ val = "-" + val;
+ }
+
+ foundFirst = true;
+
+ return val;
+ });
+
+ // undo the reverse if trimming from the right
+ if (settings.trim === "right") {
+ tokens.reverse();
+ }
+
+ return tokens.join("");
+ };
+
+ moment.duration.fn.format.defaults = {
+ // token definitions
+ escape: /\[(.+?)\]/,
+ years: /[Yy]+/,
+ months: /M+/,
+ weeks: /[Ww]+/,
+ days: /[Dd]+/,
+ hours: /[Hh]+/,
+ minutes: /m+/,
+ seconds: /s+/,
+ milliseconds: /S+/,
+ general: /.+?/,
+
+ // token type names
+ // in order of descending magnitude
+ // can be a space-separated token name list or an array of token names
+ types: "escape years months weeks days hours minutes seconds milliseconds general",
+
+ // format options
+
+ // trim
+ // "left" - template tokens are trimmed from the left until the first moment token that has a value >= 1
+ // "right" - template tokens are trimmed from the right until the first moment token that has a value >= 1
+ // (the final moment token is not trimmed, regardless of value)
+ // `false` - template tokens are not trimmed
+ trim: "left",
+
+ // precision
+ // number of decimal digits to include after (to the right of) the decimal point (positive integer)
+ // or the number of digits to truncate to 0 before (to the left of) the decimal point (negative integer)
+ precision: 0,
+
+ // force first moment token with a value to render at full length even when template is trimmed and first moment token has length of 1
+ forceLength: null,
+
+ // template used to format duration
+ // may be a function or a string
+ // template functions are executed with the `this` binding of the settings object
+ // so that template strings may be dynamically generated based on the duration object
+ // (accessible via `this.duration`)
+ // or any of the other settings
+ template: function () {
+ var types = this.types,
+ dur = this.duration,
+ lastType = findLast(types, function (type) {
+ return dur._data[type];
+ });
+
+ // default template strings for each duration dimension type
+ switch (lastType) {
+ case "seconds":
+ return "h:mm:ss";
+ case "minutes":
+ return "d[d] h:mm";
+ case "hours":
+ return "d[d] h[h]";
+ case "days":
+ return "M[m] d[d]";
+ case "weeks":
+ return "y[y] w[w]";
+ case "months":
+ return "y[y] M[m]";
+ case "years":
+ return "y[y]";
+ default:
+ return "y[y] M[m] d[d] h:mm:ss";
+ }
+ }
+ };
+
+})(this);
diff --git a/platform/features/clock/res/templates/clock.html b/platform/features/clock/res/templates/clock.html
new file mode 100644
index 0000000000..d3df62a095
--- /dev/null
+++ b/platform/features/clock/res/templates/clock.html
@@ -0,0 +1,34 @@
+
+
+
+
+ {{clock.zone()}}
+
+
+ {{clock.text()}}
+
+
+ {{clock.ampm()}}
+
+
+
\ No newline at end of file
diff --git a/platform/features/clock/res/templates/timer.html b/platform/features/clock/res/templates/timer.html
new file mode 100644
index 0000000000..ebd593435f
--- /dev/null
+++ b/platform/features/clock/res/templates/timer.html
@@ -0,0 +1,42 @@
+
+
\ No newline at end of file
diff --git a/platform/features/clock/src/actions/AbstractStartTimerAction.js b/platform/features/clock/src/actions/AbstractStartTimerAction.js
new file mode 100644
index 0000000000..8c1554965c
--- /dev/null
+++ b/platform/features/clock/src/actions/AbstractStartTimerAction.js
@@ -0,0 +1,62 @@
+/*****************************************************************************
+ * Open MCT Web, Copyright (c) 2009-2015, United States Government
+ * as represented by the Administrator of the National Aeronautics and Space
+ * Administration. All rights reserved.
+ *
+ * Open MCT Web is licensed under the Apache License, Version 2.0 (the
+ * "License"); you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ * http://www.apache.org/licenses/LICENSE-2.0.
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
+ * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
+ * License for the specific language governing permissions and limitations
+ * under the License.
+ *
+ * Open MCT Web includes source code licensed under additional open source
+ * licenses. See the Open Source Licenses file (LICENSES.md) included with
+ * this source code distribution or the Licensing information page available
+ * at runtime from the About dialog for additional information.
+ *****************************************************************************/
+/*global define*/
+
+define(
+ [],
+ function () {
+ "use strict";
+
+ /**
+ * Implements the "Start" and "Restart" action for timers.
+ *
+ * Sets the reference timestamp in a timer to the current
+ * time, such that it begins counting up.
+ *
+ * Both "Start" and "Restart" share this implementation, but
+ * control their visibility with different `appliesTo` behavior.
+ *
+ * @implements Action
+ */
+ function AbstractStartTimerAction(now, context) {
+ var domainObject = context.domainObject;
+
+ function doPersist() {
+ var persistence = domainObject.getCapability('persistence');
+ return persistence && persistence.persist();
+ }
+
+ function setTimestamp(model) {
+ model.timestamp = now();
+ }
+
+ return {
+ perform: function () {
+ return domainObject.useCapability('mutation', setTimestamp)
+ .then(doPersist);
+ }
+ };
+ }
+
+ return AbstractStartTimerAction;
+ }
+);
diff --git a/platform/features/clock/src/actions/RestartTimerAction.js b/platform/features/clock/src/actions/RestartTimerAction.js
new file mode 100644
index 0000000000..8c8a942281
--- /dev/null
+++ b/platform/features/clock/src/actions/RestartTimerAction.js
@@ -0,0 +1,54 @@
+/*****************************************************************************
+ * Open MCT Web, Copyright (c) 2009-2015, United States Government
+ * as represented by the Administrator of the National Aeronautics and Space
+ * Administration. All rights reserved.
+ *
+ * Open MCT Web is licensed under the Apache License, Version 2.0 (the
+ * "License"); you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ * http://www.apache.org/licenses/LICENSE-2.0.
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
+ * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
+ * License for the specific language governing permissions and limitations
+ * under the License.
+ *
+ * Open MCT Web includes source code licensed under additional open source
+ * licenses. See the Open Source Licenses file (LICENSES.md) included with
+ * this source code distribution or the Licensing information page available
+ * at runtime from the About dialog for additional information.
+ *****************************************************************************/
+/*global define*/
+
+define(
+ ['./AbstractStartTimerAction'],
+ function (AbstractStartTimerAction) {
+ "use strict";
+
+ /**
+ * Implements the "Restart at 0" action.
+ *
+ * Behaves the same as (and delegates functionality to)
+ * the "Start" action.
+ * @implements Action
+ */
+ function RestartTimerAction(now, context) {
+ return new AbstractStartTimerAction(now, context);
+ }
+
+ RestartTimerAction.appliesTo = function (context) {
+ var model =
+ (context.domainObject && context.domainObject.getModel())
+ || {};
+
+ // We show this variant for timers which already have
+ // a target time.
+ return model.type === 'timer' &&
+ model.timestamp !== undefined;
+ };
+
+ return RestartTimerAction;
+
+ }
+);
diff --git a/platform/features/clock/src/actions/StartTimerAction.js b/platform/features/clock/src/actions/StartTimerAction.js
new file mode 100644
index 0000000000..d7237c75e4
--- /dev/null
+++ b/platform/features/clock/src/actions/StartTimerAction.js
@@ -0,0 +1,55 @@
+/*****************************************************************************
+ * Open MCT Web, Copyright (c) 2009-2015, United States Government
+ * as represented by the Administrator of the National Aeronautics and Space
+ * Administration. All rights reserved.
+ *
+ * Open MCT Web is licensed under the Apache License, Version 2.0 (the
+ * "License"); you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ * http://www.apache.org/licenses/LICENSE-2.0.
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
+ * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
+ * License for the specific language governing permissions and limitations
+ * under the License.
+ *
+ * Open MCT Web includes source code licensed under additional open source
+ * licenses. See the Open Source Licenses file (LICENSES.md) included with
+ * this source code distribution or the Licensing information page available
+ * at runtime from the About dialog for additional information.
+ *****************************************************************************/
+/*global define*/
+
+define(
+ ['./AbstractStartTimerAction'],
+ function (AbstractStartTimerAction) {
+ "use strict";
+
+ /**
+ * Implements the "Start" action for timers.
+ *
+ * Sets the reference timestamp in a timer to the current
+ * time, such that it begins counting up.
+ *
+ * @implements Action
+ */
+ function StartTimerAction(now, context) {
+ return new AbstractStartTimerAction(now, context);
+ }
+
+ StartTimerAction.appliesTo = function (context) {
+ var model =
+ (context.domainObject && context.domainObject.getModel())
+ || {};
+
+ // We show this variant for timers which do not yet have
+ // a target time.
+ return model.type === 'timer' &&
+ model.timestamp === undefined;
+ };
+
+ return StartTimerAction;
+
+ }
+);
diff --git a/platform/features/clock/src/controllers/ClockController.js b/platform/features/clock/src/controllers/ClockController.js
new file mode 100644
index 0000000000..eba2a07e27
--- /dev/null
+++ b/platform/features/clock/src/controllers/ClockController.js
@@ -0,0 +1,100 @@
+/*****************************************************************************
+ * Open MCT Web, Copyright (c) 2009-2015, United States Government
+ * as represented by the Administrator of the National Aeronautics and Space
+ * Administration. All rights reserved.
+ *
+ * Open MCT Web is licensed under the Apache License, Version 2.0 (the
+ * "License"); you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ * http://www.apache.org/licenses/LICENSE-2.0.
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
+ * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
+ * License for the specific language governing permissions and limitations
+ * under the License.
+ *
+ * Open MCT Web includes source code licensed under additional open source
+ * licenses. See the Open Source Licenses file (LICENSES.md) included with
+ * this source code distribution or the Licensing information page available
+ * at runtime from the About dialog for additional information.
+ *****************************************************************************/
+/*global define*/
+
+define(
+ ['moment'],
+ function (moment) {
+ "use strict";
+
+ /**
+ * Controller for views of a Clock domain object.
+ *
+ * @constructor
+ */
+ function ClockController($scope, tickerService) {
+ var text,
+ ampm,
+ use24,
+ lastTimestamp,
+ unlisten,
+ timeFormat;
+
+ function update() {
+ var m = moment.utc(lastTimestamp);
+ text = timeFormat && m.format(timeFormat);
+ ampm = m.format("A"); // Just the AM or PM part
+ }
+
+ function tick(timestamp) {
+ lastTimestamp = timestamp;
+ update();
+ }
+
+ function updateFormat(clockFormat) {
+ var baseFormat;
+
+ if (clockFormat !== undefined) {
+ baseFormat = clockFormat[0];
+
+ use24 = clockFormat[1] === 'clock24';
+ timeFormat = use24 ?
+ baseFormat.replace('hh', "HH") : baseFormat;
+
+ update();
+ }
+ }
+ // Pull in the clock format from the domain object model
+ $scope.$watch('model.clockFormat', updateFormat);
+
+ // Listen for clock ticks ... and stop listening on destroy
+ unlisten = tickerService.listen(tick);
+ $scope.$on('$destroy', unlisten);
+
+ return {
+ /**
+ * Get the clock's time zone, as displayable text.
+ * @returns {string}
+ */
+ zone: function () {
+ return "UTC";
+ },
+ /**
+ * Get the current time, as displayable text.
+ * @returns {string}
+ */
+ text: function () {
+ return text;
+ },
+ /**
+ * Get the text to display to qualify a time as AM or PM.
+ * @returns {string}
+ */
+ ampm: function () {
+ return use24 ? '' : ampm;
+ }
+ };
+ }
+
+ return ClockController;
+ }
+);
diff --git a/platform/features/clock/src/controllers/RefreshingController.js b/platform/features/clock/src/controllers/RefreshingController.js
new file mode 100644
index 0000000000..4853da0f57
--- /dev/null
+++ b/platform/features/clock/src/controllers/RefreshingController.js
@@ -0,0 +1,50 @@
+/*****************************************************************************
+ * Open MCT Web, Copyright (c) 2009-2015, United States Government
+ * as represented by the Administrator of the National Aeronautics and Space
+ * Administration. All rights reserved.
+ *
+ * Open MCT Web is licensed under the Apache License, Version 2.0 (the
+ * "License"); you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ * http://www.apache.org/licenses/LICENSE-2.0.
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
+ * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
+ * License for the specific language governing permissions and limitations
+ * under the License.
+ *
+ * Open MCT Web includes source code licensed under additional open source
+ * licenses. See the Open Source Licenses file (LICENSES.md) included with
+ * this source code distribution or the Licensing information page available
+ * at runtime from the About dialog for additional information.
+ *****************************************************************************/
+/*global define*/
+
+define(
+ [],
+ function () {
+ "use strict";
+
+ /**
+ * Continually refreshes the represented domain object.
+ *
+ * This is a short-term workaround to assure Timer views stay
+ * up-to-date; should be replaced by a global auto-refresh.
+ */
+ function RefreshingController($scope, tickerService) {
+ var unlisten;
+
+ function triggerRefresh() {
+ var persistence = $scope.domainObject &&
+ $scope.domainObject.getCapability('persistence');
+ return persistence && persistence.refresh();
+ }
+
+ unlisten = tickerService.listen(triggerRefresh);
+ $scope.$on('$destroy', unlisten);
+ }
+
+ return RefreshingController;
+ }
+);
diff --git a/platform/features/clock/src/controllers/TimerController.js b/platform/features/clock/src/controllers/TimerController.js
new file mode 100644
index 0000000000..6bde70dd29
--- /dev/null
+++ b/platform/features/clock/src/controllers/TimerController.js
@@ -0,0 +1,167 @@
+/*****************************************************************************
+ * Open MCT Web, Copyright (c) 2009-2015, United States Government
+ * as represented by the Administrator of the National Aeronautics and Space
+ * Administration. All rights reserved.
+ *
+ * Open MCT Web is licensed under the Apache License, Version 2.0 (the
+ * "License"); you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ * http://www.apache.org/licenses/LICENSE-2.0.
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
+ * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
+ * License for the specific language governing permissions and limitations
+ * under the License.
+ *
+ * Open MCT Web includes source code licensed under additional open source
+ * licenses. See the Open Source Licenses file (LICENSES.md) included with
+ * this source code distribution or the Licensing information page available
+ * at runtime from the About dialog for additional information.
+ *****************************************************************************/
+/*global define*/
+
+define(
+ ['./TimerFormatter'],
+ function (TimerFormatter) {
+ "use strict";
+
+ var FORMATTER = new TimerFormatter();
+
+
+ /**
+ * Controller for views of a Timer domain object.
+ *
+ * @constructor
+ */
+ function TimerController($scope, $window, now) {
+ var timerObject,
+ relevantAction,
+ sign = '',
+ text = '',
+ formatter,
+ active = true,
+ relativeTimestamp,
+ lastTimestamp;
+
+ function update() {
+ var timeDelta = lastTimestamp - relativeTimestamp;
+
+ if (formatter && !isNaN(timeDelta)) {
+ text = formatter(timeDelta);
+ sign = timeDelta < 0 ? "-" : timeDelta >= 1000 ? "+" : "";
+ } else {
+ text = "";
+ sign = "";
+ }
+ }
+
+ function updateFormat(key) {
+ formatter = FORMATTER[key] || FORMATTER.long;
+ }
+
+ function updateTimestamp(timestamp) {
+ relativeTimestamp = timestamp;
+ }
+
+ function updateObject(domainObject) {
+ var model = domainObject.getModel(),
+ timestamp = model.timestamp,
+ formatKey = model.timerFormat,
+ actionCapability = domainObject.getCapability('action'),
+ actionKey = (timestamp === undefined) ?
+ 'timer.start' : 'timer.restart';
+
+ updateFormat(formatKey);
+ updateTimestamp(timestamp);
+
+ relevantAction = actionCapability &&
+ actionCapability.getActions(actionKey)[0];
+
+ update();
+ }
+
+ function handleObjectChange(domainObject) {
+ if (domainObject) {
+ updateObject(domainObject);
+ }
+ }
+
+ function handleModification() {
+ handleObjectChange($scope.domainObject);
+ }
+
+ function tick() {
+ var lastSign = sign, lastText = text;
+ lastTimestamp = now();
+ update();
+ // We're running in an animation frame, not in a digest cycle.
+ // We need to trigger a digest cycle if our displayable data
+ // changes.
+ if (lastSign !== sign || lastText !== text) {
+ $scope.$apply();
+ }
+ if (active) {
+ $window.requestAnimationFrame(tick);
+ }
+ }
+
+ $window.requestAnimationFrame(tick);
+
+ // Pull in the timer format from the domain object model
+ $scope.$watch('domainObject', handleObjectChange);
+ $scope.$watch('model.modified', handleModification);
+
+ // When the scope is destroyed, stop requesting anim. frames
+ $scope.$on('$destroy', function () {
+ active = false;
+ });
+
+ return {
+ /**
+ * Get the glyph to display for the start/restart button.
+ * @returns {string} glyph to display
+ */
+ buttonGlyph: function () {
+ return relevantAction ?
+ relevantAction.getMetadata().glyph : "";
+ },
+ /**
+ * Get the text to show for the start/restart button
+ * (e.g. in a tooltip)
+ * @returns {string} name of the action
+ */
+ buttonText: function () {
+ return relevantAction ?
+ relevantAction.getMetadata().name : "";
+ },
+ /**
+ * Perform the action associated with the start/restart button.
+ */
+ clickButton: function () {
+ if (relevantAction) {
+ relevantAction.perform();
+ updateObject($scope.domainObject);
+ }
+ },
+ /**
+ * Get the sign (+ or -) of the current timer value, as
+ * displayable text.
+ * @returns {string} sign of the current timer value
+ */
+ sign: function () {
+ return sign;
+ },
+ /**
+ * Get the text to display for the current timer value.
+ * @returns {string} current timer value
+ */
+ text: function () {
+ return text;
+ }
+ };
+ }
+
+ return TimerController;
+ }
+);
diff --git a/platform/features/clock/src/controllers/TimerFormatter.js b/platform/features/clock/src/controllers/TimerFormatter.js
new file mode 100644
index 0000000000..bea92b38f4
--- /dev/null
+++ b/platform/features/clock/src/controllers/TimerFormatter.js
@@ -0,0 +1,81 @@
+/*****************************************************************************
+ * Open MCT Web, Copyright (c) 2009-2015, United States Government
+ * as represented by the Administrator of the National Aeronautics and Space
+ * Administration. All rights reserved.
+ *
+ * Open MCT Web is licensed under the Apache License, Version 2.0 (the
+ * "License"); you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ * http://www.apache.org/licenses/LICENSE-2.0.
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
+ * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
+ * License for the specific language governing permissions and limitations
+ * under the License.
+ *
+ * Open MCT Web includes source code licensed under additional open source
+ * licenses. See the Open Source Licenses file (LICENSES.md) included with
+ * this source code distribution or the Licensing information page available
+ * at runtime from the About dialog for additional information.
+ *****************************************************************************/
+/*global define*/
+
+define(
+ ['moment', 'moment-duration-format'],
+ function (moment) {
+ "use strict";
+
+ var SHORT_FORMAT = "HH:mm:ss",
+ LONG_FORMAT = "d[D] HH:mm:ss";
+
+ /**
+ * Provides formatting functions for Timers.
+ *
+ * Display formats for timers are a little different from what
+ * moment.js provides, so we have custom logic here. This specifically
+ * supports `TimerController`.
+ *
+ * @constructor
+ */
+ function TimerFormatter() {
+
+ // Round this timestamp down to the second boundary
+ // (e.g. 1124ms goes down to 1000ms, -2400ms goes down to -3000ms)
+ function toWholeSeconds(duration) {
+ return Math.abs(Math.floor(duration / 1000) * 1000);
+ }
+
+ // Short-form format, e.g. 02:22:11
+ function short(duration) {
+ return moment.duration(toWholeSeconds(duration), 'ms')
+ .format(SHORT_FORMAT, { trim: false });
+ }
+
+ // Long-form format, e.g. 3d 02:22:11
+ function long(duration) {
+ return moment.duration(toWholeSeconds(duration), 'ms')
+ .format(LONG_FORMAT, { trim: false });
+ }
+
+ return {
+ /**
+ * Format a duration for display, using the short form.
+ * (e.g. 03:33:11)
+ * @param {number} duration the duration, in milliseconds
+ * @param {boolean} sign true if positive
+ */
+ short: short,
+ /**
+ * Format a duration for display, using the long form.
+ * (e.g. 0d 03:33:11)
+ * @param {number} duration the duration, in milliseconds
+ * @param {boolean} sign true if positive
+ */
+ long: long
+ };
+ }
+
+ return TimerFormatter;
+ }
+);
diff --git a/platform/features/clock/src/indicators/ClockIndicator.js b/platform/features/clock/src/indicators/ClockIndicator.js
new file mode 100644
index 0000000000..194e22067c
--- /dev/null
+++ b/platform/features/clock/src/indicators/ClockIndicator.js
@@ -0,0 +1,59 @@
+/*****************************************************************************
+ * Open MCT Web, Copyright (c) 2009-2015, United States Government
+ * as represented by the Administrator of the National Aeronautics and Space
+ * Administration. All rights reserved.
+ *
+ * Open MCT Web is licensed under the Apache License, Version 2.0 (the
+ * "License"); you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ * http://www.apache.org/licenses/LICENSE-2.0.
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
+ * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
+ * License for the specific language governing permissions and limitations
+ * under the License.
+ *
+ * Open MCT Web includes source code licensed under additional open source
+ * licenses. See the Open Source Licenses file (LICENSES.md) included with
+ * this source code distribution or the Licensing information page available
+ * at runtime from the About dialog for additional information.
+ *****************************************************************************/
+/*global define*/
+
+define(
+ ['moment'],
+ function (moment) {
+ "use strict";
+
+ /**
+ * Indicator that displays the current UTC time in the status area.
+ * @implements Indicator
+ */
+ function ClockIndicator(tickerService, CLOCK_INDICATOR_FORMAT) {
+ var text = "";
+
+ tickerService.listen(function (timestamp) {
+ text = moment.utc(timestamp).format(CLOCK_INDICATOR_FORMAT) + " UTC";
+ });
+
+ return {
+ getGlyph: function () {
+ return "C";
+ },
+ getGlyphClass: function () {
+ return "";
+ },
+ getText: function () {
+ return text;
+ },
+ getDescription: function () {
+ return "";
+ }
+ };
+
+ }
+
+ return ClockIndicator;
+ }
+);
diff --git a/platform/features/clock/src/services/TickerService.js b/platform/features/clock/src/services/TickerService.js
new file mode 100644
index 0000000000..4da85133fc
--- /dev/null
+++ b/platform/features/clock/src/services/TickerService.js
@@ -0,0 +1,89 @@
+/*****************************************************************************
+ * Open MCT Web, Copyright (c) 2009-2015, United States Government
+ * as represented by the Administrator of the National Aeronautics and Space
+ * Administration. All rights reserved.
+ *
+ * Open MCT Web is licensed under the Apache License, Version 2.0 (the
+ * "License"); you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ * http://www.apache.org/licenses/LICENSE-2.0.
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
+ * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
+ * License for the specific language governing permissions and limitations
+ * under the License.
+ *
+ * Open MCT Web includes source code licensed under additional open source
+ * licenses. See the Open Source Licenses file (LICENSES.md) included with
+ * this source code distribution or the Licensing information page available
+ * at runtime from the About dialog for additional information.
+ *****************************************************************************/
+/*global define*/
+
+define(
+ ['moment'],
+ function (moment) {
+ "use strict";
+
+ /**
+ * Calls functions every second, as close to the actual second
+ * tick as is feasible.
+ * @constructor
+ * @param $timeout Angular's $timeout
+ * @param {Function} now function to provide the current time in ms
+ */
+ function TickerService($timeout, now) {
+ var callbacks = [],
+ last = now() - 1000;
+
+ function tick() {
+ var timestamp = now(),
+ millis = timestamp % 1000;
+
+ // Only update callbacks if a second has actually passed.
+ if (timestamp >= last + 1000) {
+ callbacks.forEach(function (callback) {
+ callback(timestamp);
+ });
+ last = timestamp - millis;
+ }
+
+ // Try to update at exactly the next second
+ $timeout(tick, 1000 - millis, true);
+ }
+
+ tick();
+
+ return {
+ /**
+ * Listen for clock ticks. The provided callback will
+ * be invoked with the current timestamp (in milliseconds
+ * since Jan 1 1970) at regular intervals, as near to the
+ * second boundary as possible.
+ *
+ * @method listen
+ * @name TickerService#listen
+ * @param {Function} callback callback to invoke
+ * @returns {Function} a function to unregister this listener
+ */
+ listen: function (callback) {
+ callbacks.push(callback);
+
+ // Provide immediate feedback
+ callback(last);
+
+ // Provide a deregistration function
+ return function () {
+ callbacks = callbacks.filter(function (cb) {
+ return cb !== callback;
+ });
+ };
+ }
+ };
+
+ }
+
+ return TickerService;
+ }
+);
diff --git a/platform/features/clock/test/actions/AbstractStartTimerActionSpec.js b/platform/features/clock/test/actions/AbstractStartTimerActionSpec.js
new file mode 100644
index 0000000000..613bf666da
--- /dev/null
+++ b/platform/features/clock/test/actions/AbstractStartTimerActionSpec.js
@@ -0,0 +1,87 @@
+/*****************************************************************************
+ * Open MCT Web, Copyright (c) 2009-2015, United States Government
+ * as represented by the Administrator of the National Aeronautics and Space
+ * Administration. All rights reserved.
+ *
+ * Open MCT Web is licensed under the Apache License, Version 2.0 (the
+ * "License"); you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ * http://www.apache.org/licenses/LICENSE-2.0.
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
+ * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
+ * License for the specific language governing permissions and limitations
+ * under the License.
+ *
+ * Open MCT Web includes source code licensed under additional open source
+ * licenses. See the Open Source Licenses file (LICENSES.md) included with
+ * this source code distribution or the Licensing information page available
+ * at runtime from the About dialog for additional information.
+ *****************************************************************************/
+/*global define,describe,it,expect,beforeEach,waitsFor,jasmine,window,afterEach*/
+
+define(
+ ["../../src/actions/AbstractStartTimerAction"],
+ function (AbstractStartTimerAction) {
+ "use strict";
+
+ describe("A timer's start/restart action", function () {
+ var mockNow,
+ mockDomainObject,
+ mockPersistence,
+ testModel,
+ action;
+
+ function asPromise(value) {
+ return (value || {}).then ? value : {
+ then: function (callback) {
+ return asPromise(callback(value));
+ }
+ };
+ }
+
+ beforeEach(function () {
+ mockNow = jasmine.createSpy('now');
+ mockDomainObject = jasmine.createSpyObj(
+ 'domainObject',
+ [ 'getCapability', 'useCapability' ]
+ );
+ mockPersistence = jasmine.createSpyObj(
+ 'persistence',
+ ['persist']
+ );
+
+ mockDomainObject.getCapability.andCallFake(function (c) {
+ return (c === 'persistence') && mockPersistence;
+ });
+ mockDomainObject.useCapability.andCallFake(function (c, v) {
+ if (c === 'mutation') {
+ testModel = v(testModel) || testModel;
+ return asPromise(true);
+ }
+ });
+
+ testModel = {};
+
+ action = new AbstractStartTimerAction(mockNow, {
+ domainObject: mockDomainObject
+ });
+ });
+
+ it("updates the model with a timestamp and persists", function () {
+ mockNow.andReturn(12000);
+ action.perform();
+ expect(testModel.timestamp).toEqual(12000);
+ expect(mockPersistence.persist).toHaveBeenCalled();
+ });
+
+ it("does not truncate milliseconds", function () {
+ mockNow.andReturn(42321);
+ action.perform();
+ expect(testModel.timestamp).toEqual(42321);
+ expect(mockPersistence.persist).toHaveBeenCalled();
+ });
+ });
+ }
+);
diff --git a/platform/features/clock/test/actions/RestartTimerActionSpec.js b/platform/features/clock/test/actions/RestartTimerActionSpec.js
new file mode 100644
index 0000000000..df9ea0cba7
--- /dev/null
+++ b/platform/features/clock/test/actions/RestartTimerActionSpec.js
@@ -0,0 +1,97 @@
+/*****************************************************************************
+ * Open MCT Web, Copyright (c) 2009-2015, United States Government
+ * as represented by the Administrator of the National Aeronautics and Space
+ * Administration. All rights reserved.
+ *
+ * Open MCT Web is licensed under the Apache License, Version 2.0 (the
+ * "License"); you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ * http://www.apache.org/licenses/LICENSE-2.0.
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
+ * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
+ * License for the specific language governing permissions and limitations
+ * under the License.
+ *
+ * Open MCT Web includes source code licensed under additional open source
+ * licenses. See the Open Source Licenses file (LICENSES.md) included with
+ * this source code distribution or the Licensing information page available
+ * at runtime from the About dialog for additional information.
+ *****************************************************************************/
+/*global define,describe,it,expect,beforeEach,waitsFor,jasmine,window,afterEach*/
+
+define(
+ ["../../src/actions/RestartTimerAction"],
+ function (RestartTimerAction) {
+ "use strict";
+
+ describe("A timer's restart action", function () {
+ var mockNow,
+ mockDomainObject,
+ mockPersistence,
+ testModel,
+ testContext,
+ action;
+
+ function asPromise(value) {
+ return (value || {}).then ? value : {
+ then: function (callback) {
+ return asPromise(callback(value));
+ }
+ };
+ }
+
+ beforeEach(function () {
+ mockNow = jasmine.createSpy('now');
+ mockDomainObject = jasmine.createSpyObj(
+ 'domainObject',
+ [ 'getCapability', 'useCapability', 'getModel' ]
+ );
+ mockPersistence = jasmine.createSpyObj(
+ 'persistence',
+ ['persist']
+ );
+
+ mockDomainObject.getCapability.andCallFake(function (c) {
+ return (c === 'persistence') && mockPersistence;
+ });
+ mockDomainObject.useCapability.andCallFake(function (c, v) {
+ if (c === 'mutation') {
+ testModel = v(testModel) || testModel;
+ return asPromise(true);
+ }
+ });
+ mockDomainObject.getModel.andCallFake(function () {
+ return testModel;
+ });
+
+ testModel = {};
+ testContext = { domainObject: mockDomainObject };
+
+ action = new RestartTimerAction(mockNow, testContext);
+ });
+
+ it("updates the model with a timestamp and persists", function () {
+ mockNow.andReturn(12000);
+ action.perform();
+ expect(testModel.timestamp).toEqual(12000);
+ expect(mockPersistence.persist).toHaveBeenCalled();
+ });
+
+ it("applies only to timers with a target time", function () {
+ testModel.type = 'timer';
+ testModel.timestamp = 12000;
+ expect(RestartTimerAction.appliesTo(testContext)).toBeTruthy();
+
+ testModel.type = 'timer';
+ testModel.timestamp = undefined;
+ expect(RestartTimerAction.appliesTo(testContext)).toBeFalsy();
+
+ testModel.type = 'clock';
+ testModel.timestamp = 12000;
+ expect(RestartTimerAction.appliesTo(testContext)).toBeFalsy();
+ });
+ });
+ }
+);
diff --git a/platform/features/clock/test/actions/StartTimerActionSpec.js b/platform/features/clock/test/actions/StartTimerActionSpec.js
new file mode 100644
index 0000000000..97aee20e12
--- /dev/null
+++ b/platform/features/clock/test/actions/StartTimerActionSpec.js
@@ -0,0 +1,97 @@
+/*****************************************************************************
+ * Open MCT Web, Copyright (c) 2009-2015, United States Government
+ * as represented by the Administrator of the National Aeronautics and Space
+ * Administration. All rights reserved.
+ *
+ * Open MCT Web is licensed under the Apache License, Version 2.0 (the
+ * "License"); you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ * http://www.apache.org/licenses/LICENSE-2.0.
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
+ * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
+ * License for the specific language governing permissions and limitations
+ * under the License.
+ *
+ * Open MCT Web includes source code licensed under additional open source
+ * licenses. See the Open Source Licenses file (LICENSES.md) included with
+ * this source code distribution or the Licensing information page available
+ * at runtime from the About dialog for additional information.
+ *****************************************************************************/
+/*global define,describe,it,expect,beforeEach,waitsFor,jasmine,window,afterEach*/
+
+define(
+ ["../../src/actions/StartTimerAction"],
+ function (StartTimerAction) {
+ "use strict";
+
+ describe("A timer's start action", function () {
+ var mockNow,
+ mockDomainObject,
+ mockPersistence,
+ testModel,
+ testContext,
+ action;
+
+ function asPromise(value) {
+ return (value || {}).then ? value : {
+ then: function (callback) {
+ return asPromise(callback(value));
+ }
+ };
+ }
+
+ beforeEach(function () {
+ mockNow = jasmine.createSpy('now');
+ mockDomainObject = jasmine.createSpyObj(
+ 'domainObject',
+ [ 'getCapability', 'useCapability', 'getModel' ]
+ );
+ mockPersistence = jasmine.createSpyObj(
+ 'persistence',
+ ['persist']
+ );
+
+ mockDomainObject.getCapability.andCallFake(function (c) {
+ return (c === 'persistence') && mockPersistence;
+ });
+ mockDomainObject.useCapability.andCallFake(function (c, v) {
+ if (c === 'mutation') {
+ testModel = v(testModel) || testModel;
+ return asPromise(true);
+ }
+ });
+ mockDomainObject.getModel.andCallFake(function () {
+ return testModel;
+ });
+
+ testModel = {};
+ testContext = { domainObject: mockDomainObject };
+
+ action = new StartTimerAction(mockNow, testContext);
+ });
+
+ it("updates the model with a timestamp and persists", function () {
+ mockNow.andReturn(12000);
+ action.perform();
+ expect(testModel.timestamp).toEqual(12000);
+ expect(mockPersistence.persist).toHaveBeenCalled();
+ });
+
+ it("applies only to timers without a target time", function () {
+ testModel.type = 'timer';
+ testModel.timestamp = 12000;
+ expect(StartTimerAction.appliesTo(testContext)).toBeFalsy();
+
+ testModel.type = 'timer';
+ testModel.timestamp = undefined;
+ expect(StartTimerAction.appliesTo(testContext)).toBeTruthy();
+
+ testModel.type = 'clock';
+ testModel.timestamp = 12000;
+ expect(StartTimerAction.appliesTo(testContext)).toBeFalsy();
+ });
+ });
+ }
+);
diff --git a/platform/features/clock/test/controllers/ClockControllerSpec.js b/platform/features/clock/test/controllers/ClockControllerSpec.js
new file mode 100644
index 0000000000..c815ec8c5a
--- /dev/null
+++ b/platform/features/clock/test/controllers/ClockControllerSpec.js
@@ -0,0 +1,104 @@
+/*****************************************************************************
+ * Open MCT Web, Copyright (c) 2009-2015, United States Government
+ * as represented by the Administrator of the National Aeronautics and Space
+ * Administration. All rights reserved.
+ *
+ * Open MCT Web is licensed under the Apache License, Version 2.0 (the
+ * "License"); you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ * http://www.apache.org/licenses/LICENSE-2.0.
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
+ * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
+ * License for the specific language governing permissions and limitations
+ * under the License.
+ *
+ * Open MCT Web includes source code licensed under additional open source
+ * licenses. See the Open Source Licenses file (LICENSES.md) included with
+ * this source code distribution or the Licensing information page available
+ * at runtime from the About dialog for additional information.
+ *****************************************************************************/
+/*global define,describe,it,expect,beforeEach,waitsFor,jasmine,window,afterEach*/
+
+define(
+ ["../../src/controllers/ClockController"],
+ function (ClockController) {
+ "use strict";
+
+ // Wed, 03 Jun 2015 17:56:14 GMT
+ var TEST_TIMESTAMP = 1433354174000;
+
+ describe("A clock view's controller", function () {
+ var mockScope,
+ mockTicker,
+ mockUnticker,
+ mockDomainObject,
+ controller;
+
+ beforeEach(function () {
+ mockScope = jasmine.createSpyObj('$scope', ['$watch', '$on']);
+ mockTicker = jasmine.createSpyObj('ticker', ['listen']);
+ mockUnticker = jasmine.createSpy('unticker');
+
+ mockTicker.listen.andReturn(mockUnticker);
+
+ controller = new ClockController(mockScope, mockTicker);
+ });
+
+ it("watches for clock format from the domain object model", function () {
+ expect(mockScope.$watch).toHaveBeenCalledWith(
+ "model.clockFormat",
+ jasmine.any(Function)
+ );
+ });
+
+ it("subscribes to clock ticks", function () {
+ expect(mockTicker.listen)
+ .toHaveBeenCalledWith(jasmine.any(Function));
+ });
+
+ it("unsubscribes to ticks when destroyed", function () {
+ // Make sure $destroy is being listened for...
+ expect(mockScope.$on.mostRecentCall.args[0]).toEqual('$destroy');
+ expect(mockUnticker).not.toHaveBeenCalled();
+
+ // ...and makes sure that its listener unsubscribes from ticker
+ mockScope.$on.mostRecentCall.args[1]();
+ expect(mockUnticker).toHaveBeenCalled();
+ });
+
+ it("formats using the format string from the model", function () {
+ mockTicker.listen.mostRecentCall.args[0](TEST_TIMESTAMP);
+ mockScope.$watch.mostRecentCall.args[1]([
+ "YYYY-DDD hh:mm:ss",
+ "clock24"
+ ]);
+
+ expect(controller.zone()).toEqual("UTC");
+ expect(controller.text()).toEqual("2015-154 17:56:14");
+ expect(controller.ampm()).toEqual("");
+ });
+
+ it("formats 12-hour time", function () {
+ mockTicker.listen.mostRecentCall.args[0](TEST_TIMESTAMP);
+ mockScope.$watch.mostRecentCall.args[1]([
+ "YYYY-DDD hh:mm:ss",
+ "clock12"
+ ]);
+
+ expect(controller.zone()).toEqual("UTC");
+ expect(controller.text()).toEqual("2015-154 05:56:14");
+ expect(controller.ampm()).toEqual("PM");
+ });
+
+ it("does not throw exceptions when clockFormat is undefined", function () {
+ mockTicker.listen.mostRecentCall.args[0](TEST_TIMESTAMP);
+ expect(function () {
+ mockScope.$watch.mostRecentCall.args[1](undefined);
+ }).not.toThrow();
+ });
+
+ });
+ }
+);
diff --git a/platform/features/clock/test/controllers/RefreshingControllerSpec.js b/platform/features/clock/test/controllers/RefreshingControllerSpec.js
new file mode 100644
index 0000000000..8aee11279f
--- /dev/null
+++ b/platform/features/clock/test/controllers/RefreshingControllerSpec.js
@@ -0,0 +1,84 @@
+/*****************************************************************************
+ * Open MCT Web, Copyright (c) 2009-2015, United States Government
+ * as represented by the Administrator of the National Aeronautics and Space
+ * Administration. All rights reserved.
+ *
+ * Open MCT Web is licensed under the Apache License, Version 2.0 (the
+ * "License"); you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ * http://www.apache.org/licenses/LICENSE-2.0.
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
+ * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
+ * License for the specific language governing permissions and limitations
+ * under the License.
+ *
+ * Open MCT Web includes source code licensed under additional open source
+ * licenses. See the Open Source Licenses file (LICENSES.md) included with
+ * this source code distribution or the Licensing information page available
+ * at runtime from the About dialog for additional information.
+ *****************************************************************************/
+/*global define,describe,it,expect,beforeEach,waitsFor,jasmine,window,afterEach*/
+
+define(
+ ["../../src/controllers/RefreshingController"],
+ function (RefreshingController) {
+ "use strict";
+
+
+
+ describe("The refreshing controller", function () {
+ var mockScope,
+ mockTicker,
+ mockUnticker,
+ controller;
+
+ beforeEach(function () {
+ mockScope = jasmine.createSpyObj('$scope', ['$on']);
+ mockTicker = jasmine.createSpyObj('ticker', ['listen']);
+ mockUnticker = jasmine.createSpy('unticker');
+
+ mockTicker.listen.andReturn(mockUnticker);
+
+ controller = new RefreshingController(mockScope, mockTicker);
+ });
+
+ it("refreshes the represented object on every tick", function () {
+ var mockDomainObject = jasmine.createSpyObj(
+ 'domainObject',
+ [ 'getCapability' ]
+ ),
+ mockPersistence = jasmine.createSpyObj(
+ 'persistence',
+ [ 'persist', 'refresh' ]
+ );
+
+ mockDomainObject.getCapability.andCallFake(function (c) {
+ return (c === 'persistence') && mockPersistence;
+ });
+
+ mockScope.domainObject = mockDomainObject;
+
+ mockTicker.listen.mostRecentCall.args[0](12321);
+ expect(mockPersistence.refresh).toHaveBeenCalled();
+ expect(mockPersistence.persist).not.toHaveBeenCalled();
+ });
+
+ it("subscribes to clock ticks", function () {
+ expect(mockTicker.listen)
+ .toHaveBeenCalledWith(jasmine.any(Function));
+ });
+
+ it("unsubscribes to ticks when destroyed", function () {
+ // Make sure $destroy is being listened for...
+ expect(mockScope.$on.mostRecentCall.args[0]).toEqual('$destroy');
+ expect(mockUnticker).not.toHaveBeenCalled();
+
+ // ...and makes sure that its listener unsubscribes from ticker
+ mockScope.$on.mostRecentCall.args[1]();
+ expect(mockUnticker).toHaveBeenCalled();
+ });
+ });
+ }
+);
diff --git a/platform/features/clock/test/controllers/TimerControllerSpec.js b/platform/features/clock/test/controllers/TimerControllerSpec.js
new file mode 100644
index 0000000000..d0259a0528
--- /dev/null
+++ b/platform/features/clock/test/controllers/TimerControllerSpec.js
@@ -0,0 +1,199 @@
+/*****************************************************************************
+ * Open MCT Web, Copyright (c) 2009-2015, United States Government
+ * as represented by the Administrator of the National Aeronautics and Space
+ * Administration. All rights reserved.
+ *
+ * Open MCT Web is licensed under the Apache License, Version 2.0 (the
+ * "License"); you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ * http://www.apache.org/licenses/LICENSE-2.0.
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
+ * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
+ * License for the specific language governing permissions and limitations
+ * under the License.
+ *
+ * Open MCT Web includes source code licensed under additional open source
+ * licenses. See the Open Source Licenses file (LICENSES.md) included with
+ * this source code distribution or the Licensing information page available
+ * at runtime from the About dialog for additional information.
+ *****************************************************************************/
+/*global define,describe,it,expect,beforeEach,waitsFor,jasmine,window,afterEach*/
+
+define(
+ ["../../src/controllers/TimerController"],
+ function (TimerController) {
+ "use strict";
+
+ // Wed, 03 Jun 2015 17:56:14 GMT
+ var TEST_TIMESTAMP = 1433354174000;
+
+ describe("A timer view's controller", function () {
+ var mockScope,
+ mockWindow,
+ mockNow,
+ mockDomainObject,
+ mockActionCapability,
+ mockStart,
+ mockRestart,
+ testModel,
+ controller;
+
+ function invokeWatch(expr, value) {
+ mockScope.$watch.calls.forEach(function (call) {
+ if (call.args[0] === expr) {
+ call.args[1](value);
+ }
+ });
+ }
+
+ beforeEach(function () {
+ mockScope = jasmine.createSpyObj(
+ '$scope',
+ ['$watch', '$on', '$apply']
+ );
+ mockWindow = jasmine.createSpyObj(
+ '$window',
+ ['requestAnimationFrame']
+ );
+ mockDomainObject = jasmine.createSpyObj(
+ 'domainObject',
+ [ 'getCapability', 'useCapability', 'getModel' ]
+ );
+ mockActionCapability = jasmine.createSpyObj(
+ 'action',
+ ['getActions']
+ );
+ mockStart = jasmine.createSpyObj(
+ 'start',
+ ['getMetadata', 'perform']
+ );
+ mockRestart = jasmine.createSpyObj(
+ 'restart',
+ ['getMetadata', 'perform']
+ );
+ mockNow = jasmine.createSpy('now');
+
+ mockDomainObject.getCapability.andCallFake(function (c) {
+ return (c === 'action') && mockActionCapability;
+ });
+ mockDomainObject.getModel.andCallFake(function () {
+ return testModel;
+ });
+ mockActionCapability.getActions.andCallFake(function (k) {
+ return [{
+ 'timer.start': mockStart,
+ 'timer.restart': mockRestart
+ }[k]];
+ });
+ mockStart.getMetadata.andReturn({ glyph: "S", name: "Start" });
+ mockRestart.getMetadata.andReturn({ glyph: "R", name: "Restart" });
+ mockScope.domainObject = mockDomainObject;
+
+ testModel = {};
+
+ controller = new TimerController(mockScope, mockWindow, mockNow);
+ });
+
+ it("watches for the domain object in view", function () {
+ expect(mockScope.$watch).toHaveBeenCalledWith(
+ "domainObject",
+ jasmine.any(Function)
+ );
+ });
+
+ it("watches for domain object modifications", function () {
+ expect(mockScope.$watch).toHaveBeenCalledWith(
+ "model.modified",
+ jasmine.any(Function)
+ );
+ });
+
+ it("updates on a timer", function () {
+ expect(mockWindow.requestAnimationFrame)
+ .toHaveBeenCalledWith(jasmine.any(Function));
+ });
+
+ it("displays nothing when there is no target", function () {
+ // Notify that domain object is available via scope
+ invokeWatch('domainObject', mockDomainObject);
+ mockNow.andReturn(TEST_TIMESTAMP);
+ mockWindow.requestAnimationFrame.mostRecentCall.args[0]();
+ expect(controller.sign()).toEqual("");
+ expect(controller.text()).toEqual("");
+ });
+
+ it("formats time to display relative to target", function () {
+ testModel.timestamp = TEST_TIMESTAMP;
+ testModel.timerFormat = 'long';
+ // Notify that domain object is available via scope
+ invokeWatch('domainObject', mockDomainObject);
+
+ mockNow.andReturn(TEST_TIMESTAMP + 121000);
+ mockWindow.requestAnimationFrame.mostRecentCall.args[0]();
+ expect(controller.sign()).toEqual("+");
+ expect(controller.text()).toEqual("0D 00:02:01");
+
+ mockNow.andReturn(TEST_TIMESTAMP - 121000);
+ mockWindow.requestAnimationFrame.mostRecentCall.args[0]();
+ expect(controller.sign()).toEqual("-");
+ expect(controller.text()).toEqual("0D 00:02:01");
+
+ mockNow.andReturn(TEST_TIMESTAMP);
+ mockWindow.requestAnimationFrame.mostRecentCall.args[0]();
+ expect(controller.sign()).toEqual("");
+ expect(controller.text()).toEqual("0D 00:00:00");
+ });
+
+ it("shows glyph & name for the applicable start/restart action", function () {
+ invokeWatch('domainObject', mockDomainObject);
+ expect(controller.buttonGlyph()).toEqual("S");
+ expect(controller.buttonText()).toEqual("Start");
+
+ testModel.timestamp = 12321;
+ invokeWatch('model.modified', 1);
+ expect(controller.buttonGlyph()).toEqual("R");
+ expect(controller.buttonText()).toEqual("Restart");
+ });
+
+ it("performs correct start/restart action on click", function () {
+ invokeWatch('domainObject', mockDomainObject);
+ expect(mockStart.perform).not.toHaveBeenCalled();
+ controller.clickButton();
+ expect(mockStart.perform).toHaveBeenCalled();
+
+ testModel.timestamp = 12321;
+ invokeWatch('model.modified', 1);
+ expect(mockRestart.perform).not.toHaveBeenCalled();
+ controller.clickButton();
+ expect(mockRestart.perform).toHaveBeenCalled();
+ });
+
+ it("stops requesting animation frames when destroyed", function () {
+ var initialCount = mockWindow.requestAnimationFrame.calls.length;
+
+ // First, check that normally new frames keep getting requested
+ mockWindow.requestAnimationFrame.mostRecentCall.args[0]();
+ expect(mockWindow.requestAnimationFrame.calls.length)
+ .toEqual(initialCount + 1);
+ mockWindow.requestAnimationFrame.mostRecentCall.args[0]();
+ expect(mockWindow.requestAnimationFrame.calls.length)
+ .toEqual(initialCount + 2);
+
+ // Now, verify that it stops after $destroy
+ expect(mockScope.$on.mostRecentCall.args[0])
+ .toEqual('$destroy');
+ mockScope.$on.mostRecentCall.args[1]();
+
+ // Frames should no longer get requested
+ mockWindow.requestAnimationFrame.mostRecentCall.args[0]();
+ expect(mockWindow.requestAnimationFrame.calls.length)
+ .toEqual(initialCount + 2);
+ mockWindow.requestAnimationFrame.mostRecentCall.args[0]();
+ expect(mockWindow.requestAnimationFrame.calls.length)
+ .toEqual(initialCount + 2);
+ });
+ });
+ }
+);
diff --git a/platform/features/clock/test/controllers/TimerFormatterSpec.js b/platform/features/clock/test/controllers/TimerFormatterSpec.js
new file mode 100644
index 0000000000..558bafceb2
--- /dev/null
+++ b/platform/features/clock/test/controllers/TimerFormatterSpec.js
@@ -0,0 +1,117 @@
+/*****************************************************************************
+ * Open MCT Web, Copyright (c) 2009-2015, United States Government
+ * as represented by the Administrator of the National Aeronautics and Space
+ * Administration. All rights reserved.
+ *
+ * Open MCT Web is licensed under the Apache License, Version 2.0 (the
+ * "License"); you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ * http://www.apache.org/licenses/LICENSE-2.0.
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
+ * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
+ * License for the specific language governing permissions and limitations
+ * under the License.
+ *
+ * Open MCT Web includes source code licensed under additional open source
+ * licenses. See the Open Source Licenses file (LICENSES.md) included with
+ * this source code distribution or the Licensing information page available
+ * at runtime from the About dialog for additional information.
+ *****************************************************************************/
+/*global define,describe,it,expect,beforeEach,waitsFor,jasmine,window,afterEach*/
+
+define(
+ ["../../src/controllers/TimerFormatter"],
+ function (TimerFormatter) {
+ "use strict";
+
+ var MS_IN_SEC = 1000,
+ MS_IN_MIN = MS_IN_SEC * 60,
+ MS_IN_HR = MS_IN_MIN * 60,
+ MS_IN_DAY = MS_IN_HR * 24;
+
+ describe("The timer value formatter", function () {
+ var formatter = new TimerFormatter();
+
+ function sum(a, b) {
+ return a + b;
+ }
+
+ function toDuration(days, hours, mins, secs) {
+ return [
+ days * MS_IN_DAY,
+ hours * MS_IN_HR,
+ mins * MS_IN_MIN,
+ secs * MS_IN_SEC
+ ].reduce(sum, 0);
+ }
+
+ function twoDigits(n) {
+ return n < 10 ? ('0' + n) : n;
+ }
+
+ it("formats short-form values (no days)", function () {
+ expect(formatter.short(toDuration(0, 123, 2, 3) + 123))
+ .toEqual("123:02:03");
+ });
+
+ it("formats negative short-form values (no days)", function () {
+ expect(formatter.short(-toDuration(0, 123, 2, 3) + 123))
+ .toEqual("123:02:03");
+ });
+
+ it("formats long-form values (with days)", function () {
+ expect(formatter.long(toDuration(0, 123, 2, 3) + 123))
+ .toEqual("5D 03:02:03");
+ });
+
+ it("formats negative long-form values (no days)", function () {
+ expect(formatter.long(-toDuration(0, 123, 2, 3) + 123))
+ .toEqual("5D 03:02:03");
+ });
+
+ it("rounds seconds down for positive durations", function () {
+ expect(formatter.short(MS_IN_SEC + 600))
+ .toEqual("00:00:01");
+ });
+
+ it("rounds seconds up for negative durations", function () {
+ expect(formatter.short(-MS_IN_SEC - 600))
+ .toEqual("00:00:02");
+ });
+
+ it("short-formats correctly around negative time borders", function () {
+ expect(formatter.short(-1)).toEqual("00:00:01");
+ expect(formatter.short(-1000)).toEqual("00:00:01");
+ expect(formatter.short(-1001)).toEqual("00:00:02");
+ expect(formatter.short(-2000)).toEqual("00:00:02");
+ expect(formatter.short(-59001)).toEqual("00:01:00");
+ expect(formatter.short(-60000)).toEqual("00:01:00");
+
+ expect(formatter.short(-MS_IN_HR + 999)).toEqual("01:00:00");
+ expect(formatter.short(-MS_IN_HR)).toEqual("01:00:00");
+ });
+
+ it("differentiates between values around zero", function () {
+ // These are more than 1000 ms apart so should not appear
+ // as the same second
+ expect(formatter.short(-999))
+ .not.toEqual(formatter.short(999));
+ });
+
+ it("handles negative days", function () {
+ expect(formatter.long(-10 * MS_IN_DAY))
+ .toEqual("10D 00:00:00");
+ expect(formatter.long(-10 * MS_IN_DAY + 100))
+ .toEqual("10D 00:00:00");
+ expect(formatter.long(-10 * MS_IN_DAY + 999))
+ .toEqual("10D 00:00:00");
+
+ expect(formatter.short(-10 * MS_IN_DAY + 100))
+ .toEqual("240:00:00");
+ });
+
+ });
+ }
+);
diff --git a/platform/features/clock/test/indicators/ClockIndicatorSpec.js b/platform/features/clock/test/indicators/ClockIndicatorSpec.js
new file mode 100644
index 0000000000..fd6ac72a96
--- /dev/null
+++ b/platform/features/clock/test/indicators/ClockIndicatorSpec.js
@@ -0,0 +1,61 @@
+/*****************************************************************************
+ * Open MCT Web, Copyright (c) 2009-2015, United States Government
+ * as represented by the Administrator of the National Aeronautics and Space
+ * Administration. All rights reserved.
+ *
+ * Open MCT Web is licensed under the Apache License, Version 2.0 (the
+ * "License"); you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ * http://www.apache.org/licenses/LICENSE-2.0.
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
+ * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
+ * License for the specific language governing permissions and limitations
+ * under the License.
+ *
+ * Open MCT Web includes source code licensed under additional open source
+ * licenses. See the Open Source Licenses file (LICENSES.md) included with
+ * this source code distribution or the Licensing information page available
+ * at runtime from the About dialog for additional information.
+ *****************************************************************************/
+/*global define,describe,it,expect,beforeEach,waitsFor,jasmine,window,afterEach*/
+
+define(
+ ["../../src/indicators/ClockIndicator"],
+ function (ClockIndicator) {
+ "use strict";
+
+ // Wed, 03 Jun 2015 17:56:14 GMT
+ var TEST_TIMESTAMP = 1433354174000,
+ TEST_FORMAT = "YYYY-DDD HH:mm:ss";
+
+ describe("The clock indicator", function () {
+ var mockTicker,
+ mockUnticker,
+ indicator;
+
+ beforeEach(function () {
+ mockTicker = jasmine.createSpyObj('ticker', ['listen']);
+ mockUnticker = jasmine.createSpy('unticker');
+
+ mockTicker.listen.andReturn(mockUnticker);
+
+ indicator = new ClockIndicator(mockTicker, TEST_FORMAT);
+ });
+
+ it("displays the current time", function () {
+ mockTicker.listen.mostRecentCall.args[0](TEST_TIMESTAMP);
+ expect(indicator.getText()).toEqual("2015-154 17:56:14 UTC");
+ });
+
+ it("implements the Indicator interface", function () {
+ expect(indicator.getGlyph()).toEqual(jasmine.any(String));
+ expect(indicator.getGlyphClass()).toEqual(jasmine.any(String));
+ expect(indicator.getText()).toEqual(jasmine.any(String));
+ expect(indicator.getDescription()).toEqual(jasmine.any(String));
+ });
+
+ });
+ }
+);
diff --git a/platform/features/clock/test/services/TickerServiceSpec.js b/platform/features/clock/test/services/TickerServiceSpec.js
new file mode 100644
index 0000000000..9b5370d1d1
--- /dev/null
+++ b/platform/features/clock/test/services/TickerServiceSpec.js
@@ -0,0 +1,64 @@
+/*****************************************************************************
+ * Open MCT Web, Copyright (c) 2009-2015, United States Government
+ * as represented by the Administrator of the National Aeronautics and Space
+ * Administration. All rights reserved.
+ *
+ * Open MCT Web is licensed under the Apache License, Version 2.0 (the
+ * "License"); you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ * http://www.apache.org/licenses/LICENSE-2.0.
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
+ * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
+ * License for the specific language governing permissions and limitations
+ * under the License.
+ *
+ * Open MCT Web includes source code licensed under additional open source
+ * licenses. See the Open Source Licenses file (LICENSES.md) included with
+ * this source code distribution or the Licensing information page available
+ * at runtime from the About dialog for additional information.
+ *****************************************************************************/
+/*global define,describe,it,expect,beforeEach,waitsFor,jasmine,window,afterEach*/
+
+define(
+ ["../../src/services/TickerService"],
+ function (TickerService) {
+ "use strict";
+
+ var TEST_TIMESTAMP = 1433354174000;
+
+ describe("The ticker service", function () {
+ var mockTimeout,
+ mockNow,
+ mockCallback,
+ tickerService;
+
+ beforeEach(function () {
+ mockTimeout = jasmine.createSpy('$timeout');
+ mockNow = jasmine.createSpy('now');
+ mockCallback = jasmine.createSpy('callback');
+
+ mockNow.andReturn(TEST_TIMESTAMP);
+
+ tickerService = new TickerService(mockTimeout, mockNow);
+ });
+
+ it("notifies listeners of clock ticks", function () {
+ tickerService.listen(mockCallback);
+ mockNow.andReturn(TEST_TIMESTAMP + 12321);
+ mockTimeout.mostRecentCall.args[0]();
+ expect(mockCallback)
+ .toHaveBeenCalledWith(TEST_TIMESTAMP + 12321);
+ });
+
+ it("allows listeners to unregister", function () {
+ tickerService.listen(mockCallback)(); // Unregister immediately
+ mockNow.andReturn(TEST_TIMESTAMP + 12321);
+ mockTimeout.mostRecentCall.args[0]();
+ expect(mockCallback).not
+ .toHaveBeenCalledWith(TEST_TIMESTAMP + 12321);
+ });
+ });
+ }
+);
diff --git a/platform/features/clock/test/suite.json b/platform/features/clock/test/suite.json
new file mode 100644
index 0000000000..be10ff57f8
--- /dev/null
+++ b/platform/features/clock/test/suite.json
@@ -0,0 +1,11 @@
+[
+ "actions/AbstractStartTimerAction",
+ "actions/RestartTimerAction",
+ "actions/StartTimerAction",
+ "controllers/ClockController",
+ "controllers/RefreshingController",
+ "controllers/TimerController",
+ "controllers/TimerFormatter",
+ "indicators/ClockIndicator",
+ "services/TickerService"
+]
diff --git a/platform/features/timeline/README.md b/platform/features/timeline/README.md
new file mode 100644
index 0000000000..38439df43e
--- /dev/null
+++ b/platform/features/timeline/README.md
@@ -0,0 +1,70 @@
+This bundle provides the Timeline domain object type, as well
+as other associated domain object types and relevant views.
+
+# Implementation notes
+
+## Model Properties
+
+The properties below record properties relevant to using and
+understanding timelines based on their JSON representation.
+Additional common properties, such as `modified`
+or `persisted` timestamps, may also be present.
+
+### Timeline Model
+
+A timeline's model looks like:
+
+```
+{
+ "type": "timeline",
+ "start": {
+ "timestamp": (milliseconds since epoch),
+ "epoch": (currently, always "SET")
+ },
+ "capacity": (optional; battery capacity in watt-hours)
+ "composition": (array of identifiers for contained objects)
+}
+```
+
+The identifiers in a timeline's `composition` field should refer to
+other Timeline objects, or to Activity objects.
+
+### Activity Model
+
+An activity's model looks like:
+
+```
+{
+ "type": "activity",
+ "start": {
+ "timestamp": (milliseconds since epoch),
+ "epoch": (currently, always "SET")
+ },
+ "duration": {
+ "timestamp": (duration of this activity, in milliseconds)
+ "epoch": "SET" (this is ignored)
+ },
+ "relationships": {
+ "modes": (array of applicable Activity Mode ids)
+ },
+ "link": (optional; URL linking to associated external resource)
+ "composition": (array of identifiers for contained objects)
+}
+```
+
+The identifiers in a timeline's `composition` field should only refer to
+other Activity objects.
+
+### Activity Mode Model
+
+An activity mode's model looks like:
+
+```
+{
+ "type": "mode",
+ "resources": {
+ "comms": (communications utilization, in Kbps)
+ "power": (power utilization, in watts)
+ }
+}
+```
diff --git a/platform/features/timeline/bundle.json b/platform/features/timeline/bundle.json
new file mode 100644
index 0000000000..4bd6c9dd7c
--- /dev/null
+++ b/platform/features/timeline/bundle.json
@@ -0,0 +1,372 @@
+{
+ "name": "Timelines",
+ "description": "Resources, templates, CSS, and code for Timelines.",
+ "resources": "res",
+ "extensions": {
+ "constants": [
+ {
+ "key": "TIMELINE_MINIMUM_DURATION",
+ "description": "The minimum duration to display in a timeline view (one hour.)",
+ "value": 3600000
+ },
+ {
+ "key": "TIMELINE_MAXIMUM_OFFSCREEN",
+ "description": "Maximum amount, in pixels, of a Gantt bar which may go off screen.",
+ "value": 1000
+ },
+ {
+ "key": "TIMELINE_ZOOM_CONFIGURATION",
+ "description": "Describes major tick sizes in milliseconds, and width in pixels.",
+ "value": {
+ "levels": [
+ 1000,
+ 2000,
+ 5000,
+
+ 10000,
+ 20000,
+ 30000,
+ 60000,
+
+ 120000,
+ 300000,
+ 600000,
+
+ 1200000,
+ 1800000,
+ 3600000,
+ 7200000,
+
+ 14400000,
+ 28800000,
+ 43200000,
+ 86400000
+ ],
+ "width": 200
+ }
+ }
+ ],
+ "types": [
+ {
+ "key": "timeline",
+ "name": "Timeline",
+ "glyph": "S",
+ "description": "A container for arranging Timelines and Activities in time.",
+ "features": [ "creation" ],
+ "contains": [ "timeline", "activity" ],
+ "properties": [
+ {
+ "name": "Start date/time",
+ "control": "datetime",
+ "required": true,
+ "property": [ "start" ],
+ "options": [ "SET" ]
+ },
+ {
+ "name": "Battery capacity (Watt-hours)",
+ "control": "textfield",
+ "required": false,
+ "conversion": "number",
+ "property": [ "capacity" ],
+ "pattern": "^-?\\d+(\\.\\d*)?$"
+ }
+ ],
+ "model": { "composition": [] }
+ },
+ {
+ "key": "activity",
+ "name": "Activity",
+ "glyph": "a",
+ "features": [ "creation" ],
+ "contains": [ "activity" ],
+ "description": "An action that takes place in time. You can define a start time and duration. Activities can be nested within other Activities, or within Timelines.",
+ "properties": [
+ {
+ "name": "Start date/time",
+ "control": "datetime",
+ "required": true,
+ "property": [ "start" ],
+ "options": [ "SET" ]
+ },
+ {
+ "name": "Duration",
+ "control": "duration",
+ "required": true,
+ "property": [ "duration" ]
+ }
+ ],
+ "model": { "composition": [], "relationships": { "modes": [] } }
+ },
+ {
+ "key": "mode",
+ "name": "Activity Mode",
+ "glyph": "A",
+ "features": [ "creation" ],
+ "description": "Define resource utilizations over time, then apply to an Activity.",
+ "model": { "resources": { "comms": 0, "power": 0 } },
+ "properties": [
+ {
+ "name": "Comms (Kbps)",
+ "control": "textfield",
+ "conversion": "number",
+ "pattern": "^-?\\d+(\\.\\d*)?$",
+ "property": [ "resources", "comms" ]
+ },
+ {
+ "name": "Power (watts)",
+ "control": "textfield",
+ "conversion": "number",
+ "pattern": "^-?\\d+(\\.\\d*)?$",
+ "property": [ "resources", "power" ]
+ }
+ ]
+ }
+ ],
+ "views": [
+ {
+ "key": "values",
+ "name": "Values",
+ "glyph": "A",
+ "templateUrl": "templates/values.html",
+ "type": "mode",
+ "uses": [ "cost" ],
+ "editable": false
+ },
+ {
+ "key": "timeline",
+ "name": "Timeline",
+ "glyph": "S",
+ "type": "timeline",
+ "description": "A timeline view of Timelines and Activities.",
+ "templateUrl": "templates/timeline.html",
+ "toolbar": {
+ "sections": [
+ {
+ "items": [
+ {
+ "method": "add",
+ "glyph": "+",
+ "control": "menu-button",
+ "text": "Add",
+ "options": [
+ {
+ "name": "Timeline",
+ "glyph": "S",
+ "key": "timeline"
+ },
+ {
+ "name": "Activity",
+ "glyph": "a",
+ "key": "activity"
+ }
+ ]
+ }
+ ]
+ },
+ {
+ "items": [
+ {
+ "glyph": "\u00E9",
+ "description": "Graph resource utilization",
+ "control": "button",
+ "method": "toggleGraph"
+ },
+ {
+ "glyph": "A",
+ "control": "dialog-button",
+ "description": "Apply Activity Modes...",
+ "title": "Apply Activity Modes",
+ "dialog": {
+ "control": "selector",
+ "name": "Modes",
+ "type": "mode"
+ },
+ "property": "modes"
+ },
+ {
+ "glyph": "\u00E8",
+ "description": "Edit Activity Link",
+ "title": "Activity Link",
+ "control": "dialog-button",
+ "dialog": {
+ "control": "textfield",
+ "name": "Link",
+ "pattern": "^(ftp|https?)\\:\\/\\/\\w+(\\.\\w+)*(\\:\\d+)?(\\/\\S*)*$"
+ },
+ "property": "link"
+ },
+ {
+ "glyph": "\u0047",
+ "description": "Edit Properties...",
+ "control": "button",
+ "method": "properties"
+ }
+ ]
+ },
+ {
+ "items": [
+ {
+ "method": "remove",
+ "description": "Remove item",
+ "control": "button",
+ "glyph": "Z"
+ }
+ ]
+ }
+ ]
+ }
+ }
+ ],
+ "representations": [
+ {
+ "key": "gantt",
+ "templateUrl": "templates/activity-gantt.html",
+ "uses": [ "timespan", "type" ]
+ }
+ ],
+ "templates": [
+ {
+ "key": "timeline-tabular-swimlane-cols-tree",
+ "priority": "mandatory",
+ "templateUrl": "templates/tabular-swimlane-cols-tree.html"
+ },
+ {
+ "key": "timeline-tabular-swimlane-cols-data",
+ "priority": "mandatory",
+ "templateUrl": "templates/tabular-swimlane-cols-data.html"
+ },
+ {
+ "key": "timeline-resource-graphs",
+ "priority": "mandatory",
+ "templateUrl": "templates/resource-graphs.html"
+ },
+ {
+ "key": "timeline-resource-graph-labels",
+ "priority": "mandatory",
+ "templateUrl": "templates/resource-graph-labels.html"
+ },
+ {
+ "key": "timeline-legend-item",
+ "priority": "mandatory",
+ "templateUrl": "templates/legend-item.html"
+ },
+ {
+ "key": "timeline-ticks",
+ "priority": "mandatory",
+ "templateUrl": "templates/ticks.html"
+ }
+ ],
+ "controls": [
+ {
+ "key": "datetime",
+ "templateUrl": "templates/controls/datetime.html"
+ },
+ {
+ "key": "duration",
+ "templateUrl": "templates/controls/datetime.html"
+ }
+ ],
+ "controllers": [
+ {
+ "key": "TimelineController",
+ "implementation": "controllers/TimelineController.js",
+ "depends": [ "$scope", "$q", "objectLoader", "TIMELINE_MINIMUM_DURATION" ]
+ },
+ {
+ "key": "TimelineGraphController",
+ "implementation": "controllers/TimelineGraphController.js",
+ "depends": [ "$scope", "resources[]" ]
+ },
+ {
+ "key": "TimelineDateTimeController",
+ "implementation": "controllers/TimelineDateTimeController.js",
+ "depends": [ "$scope" ]
+ },
+ {
+ "key": "TimelineZoomController",
+ "implementation": "controllers/TimelineZoomController.js",
+ "depends": [ "$scope", "TIMELINE_ZOOM_CONFIGURATION" ]
+ },
+ {
+ "key": "TimelineTickController",
+ "implementation": "controllers/TimelineTickController.js"
+ },
+ {
+ "key": "TimelineTableController",
+ "implementation": "controllers/TimelineTableController.js"
+ },
+ {
+ "key": "TimelineGanttController",
+ "implementation": "controllers/TimelineGanttController.js",
+ "depends": [ "TIMELINE_MAXIMUM_OFFSCREEN" ]
+ },
+ {
+ "key": "ActivityModeValuesController",
+ "implementation": "controllers/ActivityModeValuesController.js",
+ "depends": [ "resources[]" ]
+ }
+ ],
+ "capabilities": [
+ {
+ "key": "timespan",
+ "implementation": "capabilities/ActivityTimespanCapability.js",
+ "depends": [ "$q" ]
+ },
+ {
+ "key": "timespan",
+ "implementation": "capabilities/TimelineTimespanCapability.js",
+ "depends": [ "$q" ]
+ },
+ {
+ "key": "utilization",
+ "implementation": "capabilities/UtilizationCapability.js",
+ "depends": [ "$q" ]
+ },
+ {
+ "key": "graph",
+ "implementation": "capabilities/GraphCapability.js",
+ "depends": [ "$q" ]
+ },
+ {
+ "key": "cost",
+ "implementation": "capabilities/CostCapability.js"
+ }
+ ],
+ "directives": [
+ {
+ "key": "mctSwimlaneDrop",
+ "implementation": "directives/MCTSwimlaneDrop.js",
+ "depends": [ "dndService" ]
+ },
+ {
+ "key": "mctSwimlaneDrag",
+ "implementation": "directives/MCTSwimlaneDrag.js",
+ "depends": [ "dndService" ]
+ }
+ ],
+ "services": [
+ {
+ "key": "objectLoader",
+ "implementation": "services/ObjectLoader.js",
+ "depends": [ "$q" ]
+ }
+ ],
+ "resources": [
+ {
+ "key": "power",
+ "name": "Power",
+ "units": "watts"
+ },
+ {
+ "key": "comms",
+ "name": "Comms",
+ "units": "Kbps"
+ },
+ {
+ "key": "battery",
+ "name": "Battery State-of-Charge",
+ "units": "%"
+ }
+ ]
+ }
+}
diff --git a/platform/features/timeline/res/templates/activity-gantt.html b/platform/features/timeline/res/templates/activity-gantt.html
new file mode 100644
index 0000000000..2af4900dc9
--- /dev/null
+++ b/platform/features/timeline/res/templates/activity-gantt.html
@@ -0,0 +1,39 @@
+
+
+
+
+
+ {{type.getGlyph()}}
+
+
+ {{model.name}}
+
+
+
+
diff --git a/platform/features/timeline/res/templates/controls/datetime.html b/platform/features/timeline/res/templates/controls/datetime.html
new file mode 100644
index 0000000000..e19ee46ff7
--- /dev/null
+++ b/platform/features/timeline/res/templates/controls/datetime.html
@@ -0,0 +1,83 @@
+
+
diff --git a/platform/features/timeline/res/templates/legend-item.html b/platform/features/timeline/res/templates/legend-item.html
new file mode 100644
index 0000000000..8ea8df9c06
--- /dev/null
+++ b/platform/features/timeline/res/templates/legend-item.html
@@ -0,0 +1,34 @@
+
+
+
+
+
+
+ {{ngModel.path}}
+ {{ngModel.domainObject.getModel().name}}
+
+
\ No newline at end of file
diff --git a/platform/features/timeline/res/templates/resource-graph-labels.html b/platform/features/timeline/res/templates/resource-graph-labels.html
new file mode 100644
index 0000000000..192188f554
--- /dev/null
+++ b/platform/features/timeline/res/templates/resource-graph-labels.html
@@ -0,0 +1,37 @@
+
+
+ {{parameters.title}}
+
+
+
+
+ {{parameters.high}}
+
+
+ {{parameters.middle}}
+
+
+ {{parameters.low}}
+
+
+
\ No newline at end of file
diff --git a/platform/features/timeline/res/templates/resource-graphs.html b/platform/features/timeline/res/templates/resource-graphs.html
new file mode 100644
index 0000000000..b96973c80f
--- /dev/null
+++ b/platform/features/timeline/res/templates/resource-graphs.html
@@ -0,0 +1,34 @@
+
+
+
+
\ No newline at end of file
diff --git a/platform/features/timeline/res/templates/tabular-swimlane-cols-data.html b/platform/features/timeline/res/templates/tabular-swimlane-cols-data.html
new file mode 100644
index 0000000000..273ffb8734
--- /dev/null
+++ b/platform/features/timeline/res/templates/tabular-swimlane-cols-data.html
@@ -0,0 +1,37 @@
+
+
+
+ {{tabularVal.niceTime(ngModel.timespan().getStart())}}
+ {{tabularVal.niceTime(ngModel.timespan().getEnd())}}
+ {{tabularVal.niceTime(ngModel.timespan().getDuration())}}
+
+
+
\ No newline at end of file
diff --git a/platform/features/timeline/res/templates/tabular-swimlane-cols-tree.html b/platform/features/timeline/res/templates/tabular-swimlane-cols-tree.html
new file mode 100644
index 0000000000..9819b317d1
--- /dev/null
+++ b/platform/features/timeline/res/templates/tabular-swimlane-cols-tree.html
@@ -0,0 +1,57 @@
+
+
+
+
+
+ é
+
+
+
+
+ è
+
+
+
+
+
+
+
+
diff --git a/platform/features/timeline/res/templates/ticks.html b/platform/features/timeline/res/templates/ticks.html
new file mode 100644
index 0000000000..3eb739c847
--- /dev/null
+++ b/platform/features/timeline/res/templates/ticks.html
@@ -0,0 +1,39 @@
+
+
diff --git a/platform/features/timeline/res/templates/timeline.html b/platform/features/timeline/res/templates/timeline.html
new file mode 100644
index 0000000000..4a7f5455fd
--- /dev/null
+++ b/platform/features/timeline/res/templates/timeline.html
@@ -0,0 +1,218 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
{{ngModel.title}}Resource Graph Legend
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/platform/features/timeline/res/templates/values.html b/platform/features/timeline/res/templates/values.html
new file mode 100644
index 0000000000..36a50ebb5e
--- /dev/null
+++ b/platform/features/timeline/res/templates/values.html
@@ -0,0 +1,27 @@
+
+
+ -
+ {{controller.metadata(key).name}}
+ {{value}} {{controller.metadata(key).units}}
+
+
\ No newline at end of file
diff --git a/platform/features/timeline/src/TimelineConstants.js b/platform/features/timeline/src/TimelineConstants.js
new file mode 100644
index 0000000000..3b7de588a4
--- /dev/null
+++ b/platform/features/timeline/src/TimelineConstants.js
@@ -0,0 +1,32 @@
+/*****************************************************************************
+ * Open MCT Web, Copyright (c) 2009-2015, United States Government
+ * as represented by the Administrator of the National Aeronautics and Space
+ * Administration. All rights reserved.
+ *
+ * Open MCT Web is licensed under the Apache License, Version 2.0 (the
+ * "License"); you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ * http://www.apache.org/licenses/LICENSE-2.0.
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
+ * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
+ * License for the specific language governing permissions and limitations
+ * under the License.
+ *
+ * Open MCT Web includes source code licensed under additional open source
+ * licenses. See the Open Source Licenses file (LICENSES.md) included with
+ * this source code distribution or the Licensing information page available
+ * at runtime from the About dialog for additional information.
+ *****************************************************************************/
+/*global define*/
+
+/**
+ * Defines constant values for use in timeline view.
+ */
+define({
+ // Pixel width of start/end handles
+ HANDLE_WIDTH: 32,
+ // Pixel tolerance for snapping behavior
+ SNAP_WIDTH: 16
+});
\ No newline at end of file
diff --git a/platform/features/timeline/src/TimelineFormatter.js b/platform/features/timeline/src/TimelineFormatter.js
new file mode 100644
index 0000000000..f9980546b3
--- /dev/null
+++ b/platform/features/timeline/src/TimelineFormatter.js
@@ -0,0 +1,78 @@
+/*****************************************************************************
+ * Open MCT Web, Copyright (c) 2009-2015, United States Government
+ * as represented by the Administrator of the National Aeronautics and Space
+ * Administration. All rights reserved.
+ *
+ * Open MCT Web is licensed under the Apache License, Version 2.0 (the
+ * "License"); you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ * http://www.apache.org/licenses/LICENSE-2.0.
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
+ * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
+ * License for the specific language governing permissions and limitations
+ * under the License.
+ *
+ * Open MCT Web includes source code licensed under additional open source
+ * licenses. See the Open Source Licenses file (LICENSES.md) included with
+ * this source code distribution or the Licensing information page available
+ * at runtime from the About dialog for additional information.
+ *****************************************************************************/
+/*global define*/
+
+define(
+ [],
+ function () {
+ 'use strict';
+
+ // Conversion factors from time units to milliseconds
+ var SECONDS = 1000,
+ MINUTES = SECONDS * 60,
+ HOURS = MINUTES * 60,
+ DAYS = HOURS * 24;
+
+ /**
+ * Formatters for durations shown in a timeline view.
+ * @constructor
+ */
+ function TimelineFormatter() {
+
+ // Format a numeric value to a string with some number of digits
+ function formatValue(value, digits) {
+ var v = value.toString(10);
+ // Pad with zeroes
+ while (v.length < digits) {
+ v = "0" + v;
+ }
+ return v;
+ }
+
+ // Format duration to string
+ function formatDuration(duration) {
+ var days = Math.floor(duration / DAYS),
+ hours = Math.floor(duration / HOURS) % 24,
+ minutes = Math.floor(duration / MINUTES) % 60,
+ seconds = Math.floor(duration / SECONDS) % 60,
+ millis = Math.floor(duration) % 1000;
+
+ return formatValue(days, 3) + " " +
+ formatValue(hours, 2) + ":" +
+ formatValue(minutes, 2) + ":" +
+ formatValue(seconds, 2) + "." +
+ formatValue(millis, 3);
+ }
+
+ return {
+ /**
+ * Format the provided duration.
+ * @param {number} duration duration, in milliseconds
+ * @returns {string} displayable representation of duration
+ */
+ format: formatDuration
+ };
+ }
+
+ return TimelineFormatter;
+ }
+);
\ No newline at end of file
diff --git a/platform/features/timeline/src/capabilities/ActivityTimespan.js b/platform/features/timeline/src/capabilities/ActivityTimespan.js
new file mode 100644
index 0000000000..7bf8ac06b1
--- /dev/null
+++ b/platform/features/timeline/src/capabilities/ActivityTimespan.js
@@ -0,0 +1,121 @@
+/*****************************************************************************
+ * Open MCT Web, Copyright (c) 2009-2015, United States Government
+ * as represented by the Administrator of the National Aeronautics and Space
+ * Administration. All rights reserved.
+ *
+ * Open MCT Web is licensed under the Apache License, Version 2.0 (the
+ * "License"); you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ * http://www.apache.org/licenses/LICENSE-2.0.
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
+ * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
+ * License for the specific language governing permissions and limitations
+ * under the License.
+ *
+ * Open MCT Web includes source code licensed under additional open source
+ * licenses. See the Open Source Licenses file (LICENSES.md) included with
+ * this source code distribution or the Licensing information page available
+ * at runtime from the About dialog for additional information.
+ *****************************************************************************/
+/*global define*/
+
+define(
+ [],
+ function () {
+ 'use strict';
+
+ /**
+ * Describes the time span of an activity object.
+ * @param model the activity's object model
+ */
+ function ActivityTimespan(model, mutation) {
+ // Get the start time for this timeline
+ function getStart() {
+ return model.start.timestamp;
+ }
+
+ // Get the end time for this timeline
+ function getEnd() {
+ return model.start.timestamp + model.duration.timestamp;
+ }
+
+ // Get the duration of this timeline
+ function getDuration() {
+ return model.duration.timestamp;
+ }
+
+ // Get the epoch used by this timeline
+ function getEpoch() {
+ return model.start.epoch; // Surface elapsed time
+ }
+
+ // Set the start time associated with this object
+ function setStart(value) {
+ var end = getEnd();
+ mutation.mutate(function (model) {
+ model.start.timestamp = Math.max(value, 0);
+ // Update duration to keep end time
+ model.duration.timestamp = Math.max(end - value, 0);
+ }, model.modified);
+ }
+
+ // Set the duration associated with this object
+ function setDuration(value) {
+ mutation.mutate(function (model) {
+ model.duration.timestamp = Math.max(value, 0);
+ }, model.modified);
+ }
+
+ // Set the end time associated with this object
+ function setEnd(value) {
+ var start = getStart();
+ mutation.mutate(function (model) {
+ model.duration.timestamp = Math.max(value - start, 0);
+ }, model.modified);
+ }
+
+ return {
+ /**
+ * Get the start time, in milliseconds relative to the epoch.
+ * @returns {number} the start time
+ */
+ getStart: getStart,
+ /**
+ * Get the duration, in milliseconds.
+ * @returns {number} the duration
+ */
+ getDuration: getDuration,
+ /**
+ * Get the end time, in milliseconds relative to the epoch.
+ * @returns {number} the end time
+ */
+ getEnd: getEnd,
+ /**
+ * Set the start time, in milliseconds relative to the epoch.
+ * @param {number} the new value
+ */
+ setStart: setStart,
+ /**
+ * Set the duration, in milliseconds.
+ * @param {number} the new value
+ */
+ setDuration: setDuration,
+ /**
+ * Set the end time, in milliseconds relative to the epoch.
+ * @param {number} the new value
+ */
+ setEnd: setEnd,
+ /**
+ * Get a string identifying the reference epoch used for
+ * start and end times.
+ * @returns {string} the epoch
+ */
+ getEpoch: getEpoch
+ };
+ }
+
+ return ActivityTimespan;
+ }
+);
\ No newline at end of file
diff --git a/platform/features/timeline/src/capabilities/ActivityTimespanCapability.js b/platform/features/timeline/src/capabilities/ActivityTimespanCapability.js
new file mode 100644
index 0000000000..c5852fa4d1
--- /dev/null
+++ b/platform/features/timeline/src/capabilities/ActivityTimespanCapability.js
@@ -0,0 +1,63 @@
+/*****************************************************************************
+ * Open MCT Web, Copyright (c) 2009-2015, United States Government
+ * as represented by the Administrator of the National Aeronautics and Space
+ * Administration. All rights reserved.
+ *
+ * Open MCT Web is licensed under the Apache License, Version 2.0 (the
+ * "License"); you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ * http://www.apache.org/licenses/LICENSE-2.0.
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
+ * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
+ * License for the specific language governing permissions and limitations
+ * under the License.
+ *
+ * Open MCT Web includes source code licensed under additional open source
+ * licenses. See the Open Source Licenses file (LICENSES.md) included with
+ * this source code distribution or the Licensing information page available
+ * at runtime from the About dialog for additional information.
+ *****************************************************************************/
+/*global define*/
+
+define(
+ ['./ActivityTimespan'],
+ function (ActivityTimespan) {
+ 'use strict';
+
+ /**
+ * Implements the `timespan` capability for Activity objects.
+ *
+ * @constructor
+ * @param $q Angular's $q, for promise-handling
+ * @param {DomainObject} domainObject the Activity
+ */
+ function ActivityTimespanCapability($q, domainObject) {
+ // Promise time span
+ function promiseTimeSpan() {
+ return $q.when(new ActivityTimespan(
+ domainObject.getModel(),
+ domainObject.getCapability('mutation')
+ ));
+ }
+
+ return {
+ /**
+ * Get the time span (start, end, duration) of this activity.
+ * @returns {Promise.} the time span of
+ * this activity
+ */
+ invoke: promiseTimeSpan
+ };
+ }
+
+ // Only applies to timeline objects
+ ActivityTimespanCapability.appliesTo = function (model) {
+ return model && (model.type === 'activity');
+ };
+
+ return ActivityTimespanCapability;
+
+ }
+);
diff --git a/platform/features/timeline/src/capabilities/ActivityUtilization.js b/platform/features/timeline/src/capabilities/ActivityUtilization.js
new file mode 100644
index 0000000000..5db644833d
--- /dev/null
+++ b/platform/features/timeline/src/capabilities/ActivityUtilization.js
@@ -0,0 +1,52 @@
+/*****************************************************************************
+ * Open MCT Web, Copyright (c) 2009-2015, United States Government
+ * as represented by the Administrator of the National Aeronautics and Space
+ * Administration. All rights reserved.
+ *
+ * Open MCT Web is licensed under the Apache License, Version 2.0 (the
+ * "License"); you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ * http://www.apache.org/licenses/LICENSE-2.0.
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
+ * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
+ * License for the specific language governing permissions and limitations
+ * under the License.
+ *
+ * Open MCT Web includes source code licensed under additional open source
+ * licenses. See the Open Source Licenses file (LICENSES.md) included with
+ * this source code distribution or the Licensing information page available
+ * at runtime from the About dialog for additional information.
+ *****************************************************************************/
+/*global define*/
+
+define(
+ [],
+ function () {
+ "use strict";
+
+ /**
+ * Provides data to populate resource graphs associated
+ * with activities in a timeline view.
+ * This is a placeholder until WTD-918.
+ * @constructor
+ */
+ function ActivityUtilization() {
+ return {
+ getPointCount: function () {
+ return 0;
+ },
+ getDomainValue: function (index) {
+ return 0;
+ },
+ getRangeValue: function (index) {
+ return 0;
+ }
+ };
+ }
+
+ return ActivityUtilization;
+ }
+
+);
\ No newline at end of file
diff --git a/platform/features/timeline/src/capabilities/CostCapability.js b/platform/features/timeline/src/capabilities/CostCapability.js
new file mode 100644
index 0000000000..c977b5751f
--- /dev/null
+++ b/platform/features/timeline/src/capabilities/CostCapability.js
@@ -0,0 +1,77 @@
+/*****************************************************************************
+ * Open MCT Web, Copyright (c) 2009-2015, United States Government
+ * as represented by the Administrator of the National Aeronautics and Space
+ * Administration. All rights reserved.
+ *
+ * Open MCT Web is licensed under the Apache License, Version 2.0 (the
+ * "License"); you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ * http://www.apache.org/licenses/LICENSE-2.0.
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
+ * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
+ * License for the specific language governing permissions and limitations
+ * under the License.
+ *
+ * Open MCT Web includes source code licensed under additional open source
+ * licenses. See the Open Source Licenses file (LICENSES.md) included with
+ * this source code distribution or the Licensing information page available
+ * at runtime from the About dialog for additional information.
+ *****************************************************************************/
+/*global define*/
+
+define(
+ [],
+ function () {
+ "use strict";
+
+ /**
+ * Exposes costs associated with a subsystem mode.
+ * @constructor
+ */
+ function CostCapability(domainObject) {
+ var model = domainObject.getModel();
+
+ return {
+ /**
+ * Get a list of resource types which have associated
+ * costs for this object. Returned values are machine-readable
+ * keys, and should be paired with external metadata for
+ * presentation (see category of extension `resources`).
+ * @returns {string[]} resource types
+ */
+ resources: function () {
+ return Object.keys(model.resources || {}).sort();
+ },
+ /**
+ * Get the cost associated with a resource of an identified
+ * type (typically, one of the types reported from a
+ * `resources` call.)
+ * @param {string} key the resource type
+ * @returns {number} the associated cost
+ */
+ cost: function (key) {
+ return (model.resources || {})[key] || 0;
+ },
+ /**
+ * Get an object containing key-value pairs describing
+ * resource utilization as described by this object.
+ * Keys are resource types; values are levels of associated
+ * resource utilization.
+ * @returns {object} resource utilizations
+ */
+ invoke: function () {
+ return model.resources || {};
+ }
+ };
+ }
+
+ // Only applies to subsystem modes.
+ CostCapability.appliesTo = function (model) {
+ return (model || {}).type === 'mode';
+ };
+
+ return CostCapability;
+ }
+);
diff --git a/platform/features/timeline/src/capabilities/CumulativeGraph.js b/platform/features/timeline/src/capabilities/CumulativeGraph.js
new file mode 100644
index 0000000000..62d799d0b0
--- /dev/null
+++ b/platform/features/timeline/src/capabilities/CumulativeGraph.js
@@ -0,0 +1,155 @@
+/*****************************************************************************
+ * Open MCT Web, Copyright (c) 2009-2015, United States Government
+ * as represented by the Administrator of the National Aeronautics and Space
+ * Administration. All rights reserved.
+ *
+ * Open MCT Web is licensed under the Apache License, Version 2.0 (the
+ * "License"); you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ * http://www.apache.org/licenses/LICENSE-2.0.
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
+ * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
+ * License for the specific language governing permissions and limitations
+ * under the License.
+ *
+ * Open MCT Web includes source code licensed under additional open source
+ * licenses. See the Open Source Licenses file (LICENSES.md) included with
+ * this source code distribution or the Licensing information page available
+ * at runtime from the About dialog for additional information.
+ *****************************************************************************/
+/*global define*/
+
+define(
+ [],
+ function () {
+ "use strict";
+
+ /**
+ * Provide points for a cumulative resource summary graph, using
+ * a provided instantaneous resource summary graph.
+ *
+ * @param {ResourceGraph} graph the resource graph
+ * @param {number} minimum the minimum allowable level
+ * @param {number} maximum the maximum allowable level
+ * @param {number} initial the initial state of the resource
+ * @param {number} rate the rate at which one unit of instantaneous
+ * utilization changes the available level in one unit
+ * of domain values (that is, per millisecond)
+ * @constructor
+ */
+ function CumulativeGraph(graph, minimum, maximum, initial, rate) {
+ var values;
+
+ // Calculate the domain value at which a line starting at
+ // (domain, range) and proceeding with the specified slope
+ // will have the specified range value.
+ function intercept(domain, range, slope, value) {
+ // value = slope * (intercept - domain) + range
+ // value - range = slope * ...
+ // intercept - domain = (value - range) / slope
+ // intercept = domain + (value - range) / slope
+ return domain + (value - range) / slope;
+ }
+
+ // Initialize the data values
+ function initializeValues() {
+ var values = [],
+ slope = 0,
+ previous = 0,
+ i;
+
+ // Add a point (or points, if needed) reaching to the provided
+ // domain and/or range value
+ function addPoint(domain, range) {
+ var previous = values[values.length - 1],
+ delta = domain - previous.domain, // time delta
+ change = delta * slope * rate, // change
+ next = previous.range + change;
+
+ // Crop to minimum boundary...
+ if (next < minimum) {
+ values.push({
+ domain: intercept(
+ previous.domain,
+ previous.range,
+ slope * rate,
+ minimum
+ ),
+ range: minimum
+ });
+ next = minimum;
+ }
+
+ // ...and maximum boundary
+ if (next > maximum) {
+ values.push({
+ domain: intercept(
+ previous.domain,
+ previous.range,
+ slope * rate,
+ maximum
+ ),
+ range: maximum
+ });
+ next = maximum;
+ }
+
+ // Add the new data value
+ if (delta > 0) {
+ values.push({ domain: domain, range: next });
+ }
+
+ slope = range;
+ }
+
+ values.push({ domain: 0, range: initial });
+
+ for (i = 0; i < graph.getPointCount(); i += 1) {
+ addPoint(graph.getDomainValue(i), graph.getRangeValue(i));
+ }
+
+ return values;
+ }
+
+ function convertToPercent(point) {
+ point.range = 100 *
+ (point.range - minimum) / (maximum - minimum);
+ }
+
+ // Calculate cumulative values...
+ values = initializeValues();
+
+ // ...and convert to percentages.
+ values.forEach(convertToPercent);
+
+ return {
+ /**
+ * Get the total number of points in this graph.
+ * @returns {number} the total number of points
+ */
+ getPointCount: function () {
+ return values.length;
+ },
+ /**
+ * Get the domain value (timestamp) for a point in this graph.
+ * @returns {number} the domain value
+ */
+ getDomainValue: function (index) {
+ return values[index].domain;
+ },
+ /**
+ * Get the range value (utilization level) for a point in
+ * this graph.
+ * @returns {number} the range value
+ */
+ getRangeValue: function (index) {
+ return values[index].range;
+ }
+ };
+ }
+
+ return CumulativeGraph;
+ }
+);
\ No newline at end of file
diff --git a/platform/features/timeline/src/capabilities/GraphCapability.js b/platform/features/timeline/src/capabilities/GraphCapability.js
new file mode 100644
index 0000000000..b701b69e9e
--- /dev/null
+++ b/platform/features/timeline/src/capabilities/GraphCapability.js
@@ -0,0 +1,99 @@
+/*****************************************************************************
+ * Open MCT Web, Copyright (c) 2009-2015, United States Government
+ * as represented by the Administrator of the National Aeronautics and Space
+ * Administration. All rights reserved.
+ *
+ * Open MCT Web is licensed under the Apache License, Version 2.0 (the
+ * "License"); you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ * http://www.apache.org/licenses/LICENSE-2.0.
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
+ * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
+ * License for the specific language governing permissions and limitations
+ * under the License.
+ *
+ * Open MCT Web includes source code licensed under additional open source
+ * licenses. See the Open Source Licenses file (LICENSES.md) included with
+ * this source code distribution or the Licensing information page available
+ * at runtime from the About dialog for additional information.
+ *****************************************************************************/
+/*global define*/
+
+define(
+ ['./ResourceGraph', './CumulativeGraph'],
+ function (ResourceGraph, CumulativeGraph) {
+ 'use strict';
+
+ /**
+ * Implements the `graph` capability for Timeline and
+ * Activity objects.
+ *
+ * @constructor
+ * @param {DomainObject} domainObject the Timeline or Activity
+ */
+ function GraphCapability($q, domainObject) {
+
+
+ // Build graphs for this group of utilizations
+ function buildGraphs(utilizations) {
+ var utilizationMap = {},
+ result = {};
+
+ // Bucket utilizations by type
+ utilizations.forEach(function (u) {
+ var k = u.key;
+ utilizationMap[k] = utilizationMap[k] || [];
+ utilizationMap[k].push(u);
+ });
+
+ // ...then convert to graphs
+ Object.keys(utilizationMap).forEach(function (k) {
+ result[k] = new ResourceGraph(utilizationMap[k]);
+ });
+
+ // Add battery state of charge
+ if (domainObject.getModel().type === 'timeline' &&
+ result.power &&
+ domainObject.getModel().capacity > 0) {
+
+ result.battery = new CumulativeGraph(
+ result.power,
+ 0,
+ domainObject.getModel().capacity, // Watts
+ domainObject.getModel().capacity,
+ 1 / 3600000 // millis-to-hour (since units are watt-hours)
+ );
+ }
+
+ return result;
+ }
+
+ return {
+ /**
+ * Get resource graphs associated with this object.
+ * This is given as a promise for key-value pairs,
+ * where keys are resource types and values are graph
+ * objects.
+ * @returns {Promise} a promise for resource graphs
+ */
+ invoke: function () {
+ return $q.when(
+ domainObject.useCapability('utilization') || []
+ ).then(buildGraphs);
+ }
+ };
+ }
+
+ // Only applies to timeline objects
+ GraphCapability.appliesTo = function (model) {
+ return model &&
+ ((model.type === 'timeline') ||
+ (model.type === 'activity'));
+ };
+
+ return GraphCapability;
+
+ }
+);
diff --git a/platform/features/timeline/src/capabilities/ResourceGraph.js b/platform/features/timeline/src/capabilities/ResourceGraph.js
new file mode 100644
index 0000000000..70dba5af64
--- /dev/null
+++ b/platform/features/timeline/src/capabilities/ResourceGraph.js
@@ -0,0 +1,149 @@
+/*****************************************************************************
+ * Open MCT Web, Copyright (c) 2009-2015, United States Government
+ * as represented by the Administrator of the National Aeronautics and Space
+ * Administration. All rights reserved.
+ *
+ * Open MCT Web is licensed under the Apache License, Version 2.0 (the
+ * "License"); you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ * http://www.apache.org/licenses/LICENSE-2.0.
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
+ * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
+ * License for the specific language governing permissions and limitations
+ * under the License.
+ *
+ * Open MCT Web includes source code licensed under additional open source
+ * licenses. See the Open Source Licenses file (LICENSES.md) included with
+ * this source code distribution or the Licensing information page available
+ * at runtime from the About dialog for additional information.
+ *****************************************************************************/
+/*global define*/
+
+define(
+ [],
+ function () {
+ "use strict";
+
+ // Utility function to copy an array, sorted by a specific field
+ function sort(array, field) {
+ return array.slice().sort(function (a, b) {
+ return a[field] - b[field];
+ });
+ }
+
+ /**
+ * Provides data to populate resource graphs associated
+ * with timelines and activities.
+ * @param {Array} utilizations resource utilizations
+ * @constructor
+ */
+ function ResourceGraph(utilizations) {
+ // Overview of algorithm here:
+ // * Goal: Have a list of time/value pairs which represents
+ // points along a stepped chart of resource utilization.
+ // Each change (stepping up or down) should have two points,
+ // at the bottom and top of the step respectively.
+ // * Step 1: Prepare two lists of utilizations sorted by start
+ // and end times. The "starts" will become step-ups, the
+ // "ends" will become step-downs.
+ // * Step 2: Initialize empty arrays for results, and a variable
+ // for the current utilization level.
+ // * Step 3: While there are still start or end times to add...
+ // * Step 3a: Determine whether the next change should be a
+ // step-up (start) or step-down (end) based on which of the
+ // next start/end times comes next (note that starts and ends
+ // are both sorted, so we look at the head of the array.)
+ // * Step 3b: Pull the next start or end (per previous decision)
+ // and convert it to a time-delta pair, negating if it's an
+ // end time (to step down or "un-step")
+ // * Step 3c: Add a point at the new time and the current
+ // running total (first point in the step, before the change)
+ // then increment the running total and add a new point
+ // (second point in the step, after the change)
+ // * Step 4: Filter out unnecessary points (if two activities
+ // run up against each other, there will be a zero-duration
+ // spike if we don't filter out the extra points from their
+ // start/end times.)
+ //
+ var starts = sort(utilizations, "start"),
+ ends = sort(utilizations, "end"),
+ values = [],
+ running = 0;
+
+ // If there are sequences of points with the same timestamp,
+ // allow only the first and last.
+ function filterPoint(value, index, values) {
+ // Allow the first or last point as a base case; aside from
+ // that, allow only points that have different timestamps
+ // from their predecessor or successor.
+ return (index === 0) || (index === values.length - 1) ||
+ (value.domain !== values[index - 1].domain) ||
+ (value.domain !== values[index + 1].domain);
+ }
+
+ // Add a step up or down (Step 3c above)
+ function addDelta(time, delta) {
+ values.push({ domain: time, range: running });
+ running += delta;
+ values.push({ domain: time, range: running });
+ }
+
+ // Add a start time (Step 3b above)
+ function addStart() {
+ var next = starts.shift();
+ addDelta(next.start, next.value);
+ }
+
+ // Add an end time (Step 3b above)
+ function addEnd() {
+ var next = ends.shift();
+ addDelta(next.end, -next.value);
+ }
+
+ // Decide whether next step should correspond to a start or
+ // an end. (Step 3c above)
+ function pickStart() {
+ return ends.length < 1 ||
+ (starts.length > 0 && starts[0].start <= ends[0].end);
+ }
+
+ // Build up start/end arrays (step 3 above)
+ while (starts.length > 0 || ends.length > 0) {
+ (pickStart() ? addStart : addEnd)();
+ }
+
+ // Filter out excess points
+ values = values.filter(filterPoint);
+
+ return {
+ /**
+ * Get the total number of points in this graph.
+ * @returns {number} the total number of points
+ */
+ getPointCount: function () {
+ return values.length;
+ },
+ /**
+ * Get the domain value (timestamp) for a point in this graph.
+ * @returns {number} the domain value
+ */
+ getDomainValue: function (index) {
+ return values[index].domain;
+ },
+ /**
+ * Get the range value (utilization level) for a point in
+ * this graph.
+ * @returns {number} the range value
+ */
+ getRangeValue: function (index) {
+ return values[index].range;
+ }
+ };
+ }
+
+ return ResourceGraph;
+ }
+
+);
\ No newline at end of file
diff --git a/platform/features/timeline/src/capabilities/TimelineTimespan.js b/platform/features/timeline/src/capabilities/TimelineTimespan.js
new file mode 100644
index 0000000000..48b5c5609f
--- /dev/null
+++ b/platform/features/timeline/src/capabilities/TimelineTimespan.js
@@ -0,0 +1,126 @@
+/*****************************************************************************
+ * Open MCT Web, Copyright (c) 2009-2015, United States Government
+ * as represented by the Administrator of the National Aeronautics and Space
+ * Administration. All rights reserved.
+ *
+ * Open MCT Web is licensed under the Apache License, Version 2.0 (the
+ * "License"); you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ * http://www.apache.org/licenses/LICENSE-2.0.
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
+ * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
+ * License for the specific language governing permissions and limitations
+ * under the License.
+ *
+ * Open MCT Web includes source code licensed under additional open source
+ * licenses. See the Open Source Licenses file (LICENSES.md) included with
+ * this source code distribution or the Licensing information page available
+ * at runtime from the About dialog for additional information.
+ *****************************************************************************/
+/*global define*/
+
+define(
+ [],
+ function () {
+ 'use strict';
+
+ /**
+ * Describes the time span of a timeline object.
+ * @param model the timeline's object model
+ * @param {Timespan[]} time spans of contained activities
+ */
+ function TimelineTimespan(model, mutation, timespans) {
+ // Get the start time for this timeline
+ function getStart() {
+ return model.start.timestamp;
+ }
+
+ // Get the end time for another time span
+ function getTimespanEnd(timespan) {
+ return timespan.getEnd();
+ }
+
+ // Wrapper for Math.max; used for max-finding of end time
+ function max(a, b) {
+ return Math.max(a, b);
+ }
+
+ // Get the end time for this timeline
+ function getEnd() {
+ return timespans.map(getTimespanEnd).reduce(max, getStart());
+ }
+
+ // Get the duration of this timeline
+ function getDuration() {
+ return getEnd() - getStart();
+ }
+
+ // Set the start time associated with this object
+ function setStart(value) {
+ mutation.mutate(function (model) {
+ model.start.timestamp = Math.max(value, 0);
+ }, model.modified);
+ }
+
+ // Set the duration associated with this object
+ function setDuration(value) {
+ // No-op; duration is implicit
+ }
+
+ // Set the end time associated with this object
+ function setEnd(value) {
+ // No-op; end time is implicit
+ }
+
+ // Get the epoch used by this timeline
+ function getEpoch() {
+ return model.start.epoch;
+ }
+
+ return {
+ /**
+ * Get the start time, in milliseconds relative to the epoch.
+ * @returns {number} the start time
+ */
+ getStart: getStart,
+ /**
+ * Get the duration, in milliseconds.
+ * @returns {number} the duration
+ */
+ getDuration: getDuration,
+ /**
+ * Get the end time, in milliseconds relative to the epoch.
+ * @returns {number} the end time
+ */
+ getEnd: getEnd,
+ /**
+ * Set the start time, in milliseconds relative to the epoch.
+ * @param {number} the new value
+ */
+ setStart: setStart,
+ /**
+ * Set the duration, in milliseconds. Timeline durations are
+ * implicit, so this is actually a no-op
+ * @param {number} the new value
+ */
+ setDuration: setDuration,
+ /**
+ * Set the end time, in milliseconds. Timeline end times are
+ * implicit, so this is actually a no-op.
+ * @param {number} the new value
+ */
+ setEnd: setEnd,
+ /**
+ * Get a string identifying the reference epoch used for
+ * start and end times.
+ * @returns {string} the epoch
+ */
+ getEpoch: getEpoch
+ };
+ }
+
+ return TimelineTimespan;
+ }
+);
\ No newline at end of file
diff --git a/platform/features/timeline/src/capabilities/TimelineTimespanCapability.js b/platform/features/timeline/src/capabilities/TimelineTimespanCapability.js
new file mode 100644
index 0000000000..4700f8ccea
--- /dev/null
+++ b/platform/features/timeline/src/capabilities/TimelineTimespanCapability.js
@@ -0,0 +1,89 @@
+/*****************************************************************************
+ * Open MCT Web, Copyright (c) 2009-2015, United States Government
+ * as represented by the Administrator of the National Aeronautics and Space
+ * Administration. All rights reserved.
+ *
+ * Open MCT Web is licensed under the Apache License, Version 2.0 (the
+ * "License"); you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ * http://www.apache.org/licenses/LICENSE-2.0.
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
+ * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
+ * License for the specific language governing permissions and limitations
+ * under the License.
+ *
+ * Open MCT Web includes source code licensed under additional open source
+ * licenses. See the Open Source Licenses file (LICENSES.md) included with
+ * this source code distribution or the Licensing information page available
+ * at runtime from the About dialog for additional information.
+ *****************************************************************************/
+/*global define*/
+
+define(
+ ['./TimelineTimespan'],
+ function (TimelineTimespan) {
+ 'use strict';
+
+ /**
+ * Implements the `timespan` capability for Timeline objects.
+ *
+ * @constructor
+ * @param $q Angular's $q, for promise-handling
+ * @param {DomainObject} domainObject the Timeline
+ */
+ function TimelineTimespanCapability($q, domainObject) {
+ // Check if a capability is defin
+
+ // Look up a child object's time span
+ function lookupTimeSpan(childObject) {
+ return childObject.useCapability('timespan');
+ }
+
+ // Check if a child object exposes a time span
+ function hasTimeSpan(childObject) {
+ return childObject.hasCapability('timespan');
+ }
+
+ // Instantiate a time span bounding other time spans
+ function giveTimeSpan(timespans) {
+ return new TimelineTimespan(
+ domainObject.getModel(),
+ domainObject.getCapability('mutation'),
+ timespans
+ );
+ }
+
+ // Build a time span object that fits all children
+ function buildTimeSpan(childObjects) {
+ return $q.all(
+ childObjects.filter(hasTimeSpan).map(lookupTimeSpan)
+ ).then(giveTimeSpan);
+ }
+
+ // Promise
+ function promiseTimeSpan() {
+ return domainObject.useCapability('composition')
+ .then(buildTimeSpan);
+ }
+
+ return {
+ /**
+ * Get the time span (start, end, duration) of this timeline.
+ * @returns {Promise.} the time span of
+ * this timeline
+ */
+ invoke: promiseTimeSpan
+ };
+ }
+
+ // Only applies to timeline objects
+ TimelineTimespanCapability.appliesTo = function (model) {
+ return model && (model.type === 'timeline');
+ };
+
+ return TimelineTimespanCapability;
+
+ }
+);
diff --git a/platform/features/timeline/src/capabilities/TimelineUtilization.js b/platform/features/timeline/src/capabilities/TimelineUtilization.js
new file mode 100644
index 0000000000..bb90c67142
--- /dev/null
+++ b/platform/features/timeline/src/capabilities/TimelineUtilization.js
@@ -0,0 +1,52 @@
+/*****************************************************************************
+ * Open MCT Web, Copyright (c) 2009-2015, United States Government
+ * as represented by the Administrator of the National Aeronautics and Space
+ * Administration. All rights reserved.
+ *
+ * Open MCT Web is licensed under the Apache License, Version 2.0 (the
+ * "License"); you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ * http://www.apache.org/licenses/LICENSE-2.0.
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
+ * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
+ * License for the specific language governing permissions and limitations
+ * under the License.
+ *
+ * Open MCT Web includes source code licensed under additional open source
+ * licenses. See the Open Source Licenses file (LICENSES.md) included with
+ * this source code distribution or the Licensing information page available
+ * at runtime from the About dialog for additional information.
+ *****************************************************************************/
+/*global define*/
+
+define(
+ [],
+ function () {
+ "use strict";
+
+ /**
+ * Provides data to populate resource graphs associated
+ * with timelines in a timeline view.
+ * This is a placeholder until WTD-918.
+ * @constructor
+ */
+ function TimelineUtilization() {
+ return {
+ getPointCount: function () {
+ return 1000;
+ },
+ getDomainValue: function (index) {
+ return 60000 * index;
+ },
+ getRangeValue: function (index) {
+ return Math.sin(index) * (index % 10);
+ }
+ };
+ }
+
+ return TimelineUtilization;
+ }
+
+);
\ No newline at end of file
diff --git a/platform/features/timeline/src/capabilities/UtilizationCapability.js b/platform/features/timeline/src/capabilities/UtilizationCapability.js
new file mode 100644
index 0000000000..140f527c0d
--- /dev/null
+++ b/platform/features/timeline/src/capabilities/UtilizationCapability.js
@@ -0,0 +1,219 @@
+/*****************************************************************************
+ * Open MCT Web, Copyright (c) 2009-2015, United States Government
+ * as represented by the Administrator of the National Aeronautics and Space
+ * Administration. All rights reserved.
+ *
+ * Open MCT Web is licensed under the Apache License, Version 2.0 (the
+ * "License"); you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ * http://www.apache.org/licenses/LICENSE-2.0.
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
+ * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
+ * License for the specific language governing permissions and limitations
+ * under the License.
+ *
+ * Open MCT Web includes source code licensed under additional open source
+ * licenses. See the Open Source Licenses file (LICENSES.md) included with
+ * this source code distribution or the Licensing information page available
+ * at runtime from the About dialog for additional information.
+ *****************************************************************************/
+/*global define*/
+
+define(
+ [],
+ function () {
+ "use strict";
+
+ /**
+ * Provide the resource utilization over time for a timeline
+ * or activity object. A utilization is presented as an object
+ * with four properties:
+ * * `key`: The resource being utilized.
+ * * `value`: The numeric utilization of that resource.
+ * * `start`: The start time of the resource's utilization.
+ * * `end`: The duration of this resource's utilization.
+ * * `epoch`: The epoch to which `start` is relative.
+ * @constructor
+ */
+ function UtilizationCapability($q, domainObject) {
+
+ // Utility function for array reduction
+ function concatenate(a, b) {
+ return (a || []).concat(b || []);
+ }
+
+ // Check whether an element in an array looks unique (for below)
+ function unique(element, index, array) {
+ return (index === 0) || (array[index - 1] !== element);
+ }
+
+ // Utility function to ensure sorted array is all unique
+ function uniquify(array) {
+ return array.filter(unique);
+ }
+
+ // Utility function for sorting strings arrays
+ function sort(array) {
+ return array.sort();
+ }
+
+ // Combine into one big array
+ function flatten(arrayOfArrays) {
+ return arrayOfArrays.reduce(concatenate, []);
+ }
+
+ // Promise the objects contained by this timeline/activity
+ function promiseComposition() {
+ return $q.when(domainObject.useCapability('composition') || []);
+ }
+
+ // Promise all subsystem modes associated with this object
+ function promiseModes() {
+ var relationship = domainObject.getCapability('relationship'),
+ modes = relationship && relationship.getRelatedObjects('modes');
+ return $q.when(modes || []);
+ }
+
+ // Promise the utilization which results directly from this object
+ function promiseInternalUtilization() {
+ var utilizations = {};
+
+ // Record the cost of a given activity mode
+ function addUtilization(mode) {
+ var cost = mode.getCapability('cost');
+ if (cost) {
+ cost.resources().forEach(function (k) {
+ utilizations[k] = utilizations[k] || 0;
+ utilizations[k] += cost.cost(k);
+ });
+ }
+ }
+
+ // Record costs for these modes
+ function addUtilizations(modes) {
+ modes.forEach(addUtilization);
+ }
+
+ // Look up start/end times for this object
+ function lookupTimespan() {
+ return domainObject.useCapability('timespan');
+ }
+
+ // Provide the result
+ function giveResult(timespan) {
+ // Convert to utilization objects
+ return Object.keys(utilizations).sort().map(function (k) {
+ return {
+ key: k,
+ value: utilizations[k],
+ start: timespan.getStart(),
+ end: timespan.getEnd(),
+ epoch: timespan.getEpoch()
+ };
+ });
+ }
+
+ return promiseModes()
+ .then(addUtilizations)
+ .then(lookupTimespan)
+ .then(giveResult);
+ }
+
+ // Look up a specific object's resource utilization
+ function lookupUtilization(domainObject) {
+ return domainObject.useCapability('utilization');
+ }
+
+ // Look up a specific object's resource utilization keys
+ function lookupUtilizationResources(domainObject) {
+ var utilization = domainObject.getCapability('utilization');
+ return utilization && utilization.resources();
+ }
+
+ // Promise a consolidated list of resource utilizations
+ function mapUtilization(objects) {
+ return $q.all(objects.map(lookupUtilization))
+ .then(flatten);
+ }
+
+ // Promise a consolidated list of resource utilization keys
+ function mapUtilizationResources(objects) {
+ return $q.all(objects.map(lookupUtilizationResources))
+ .then(flatten);
+ }
+
+ // Promise utilization associated with contained objects
+ function promiseExternalUtilization() {
+ // Get the composition, then consolidate their utilizations
+ return promiseComposition().then(mapUtilization);
+ }
+
+ // Get resource keys for this mode
+ function getModeKeys(mode) {
+ var cost = mode.getCapability('cost');
+ return cost ? cost.resources() : [];
+ }
+
+ // Map the above (for use in below)
+ function mapModeKeys(modes) {
+ return modes.map(getModeKeys);
+ }
+
+ // Promise identifiers for resources associated with modes
+ function promiseInternalKeys() {
+ return promiseModes().then(mapModeKeys).then(flatten);
+ }
+
+ // Promise identifiers for resources associated with modes
+ function promiseExternalKeys() {
+ return promiseComposition().then(mapUtilizationResources);
+ }
+
+ // Promise identifiers for resources used
+ function promiseResourceKeys() {
+ return $q.all([
+ promiseInternalKeys(),
+ promiseExternalKeys()
+ ]).then(flatten).then(sort).then(uniquify);
+ }
+
+ // Promise all utilization
+ function promiseAllUtilization() {
+ // Concatenate internal utilization (from activity modes)
+ // with external utilization (from subactivities)
+ return $q.all([
+ promiseInternalUtilization(),
+ promiseExternalUtilization()
+ ]).then(flatten);
+ }
+
+ return {
+ /**
+ * Get the keys for resources associated with this object.
+ * @returns {Promise.} a promise for resource identifiers
+ */
+ resources: promiseResourceKeys,
+ /**
+ * Get the resource utilization associated with this
+ * object. Results are not sorted. This requires looking
+ * at contained objects, which in turn must happen
+ * asynchronously, so this returns a promise.
+ * @returns {Promise.} a promise for all resource
+ * utilizations
+ */
+ invoke: promiseAllUtilization
+ };
+ }
+
+ // Only applies to timelines and activities
+ UtilizationCapability.appliesTo = function (model) {
+ return model &&
+ ((model.type === 'timeline') ||
+ (model.type === 'activity'));
+ };
+
+ return UtilizationCapability;
+ }
+);
diff --git a/platform/features/timeline/src/controllers/ActivityModeValuesController.js b/platform/features/timeline/src/controllers/ActivityModeValuesController.js
new file mode 100644
index 0000000000..2545c8b370
--- /dev/null
+++ b/platform/features/timeline/src/controllers/ActivityModeValuesController.js
@@ -0,0 +1,62 @@
+/*****************************************************************************
+ * Open MCT Web, Copyright (c) 2009-2015, United States Government
+ * as represented by the Administrator of the National Aeronautics and Space
+ * Administration. All rights reserved.
+ *
+ * Open MCT Web is licensed under the Apache License, Version 2.0 (the
+ * "License"); you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ * http://www.apache.org/licenses/LICENSE-2.0.
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
+ * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
+ * License for the specific language governing permissions and limitations
+ * under the License.
+ *
+ * Open MCT Web includes source code licensed under additional open source
+ * licenses. See the Open Source Licenses file (LICENSES.md) included with
+ * this source code distribution or the Licensing information page available
+ * at runtime from the About dialog for additional information.
+ *****************************************************************************/
+/*global define*/
+
+define(
+ [],
+ function () {
+ "use strict";
+
+ /**
+ * Controller which support the Values view of Activity Modes.
+ * @constructor
+ * @param {Array} resources definitions for extensions of
+ * category `resources`
+ */
+ function ActivityModeValuesController(resources) {
+ var metadata = {};
+
+ // Store metadata for a specific resource type
+ function storeMetadata(resource) {
+ var key = (resource || {}).key;
+ if (key) {
+ metadata[key] = resource;
+ }
+ }
+
+ // Populate the lookup table to resource metadata
+ resources.forEach(storeMetadata);
+
+ return {
+ /**
+ * Look up metadata associated with the specified
+ * resource type.
+ */
+ metadata: function (key) {
+ return metadata[key];
+ }
+ };
+ }
+
+ return ActivityModeValuesController;
+ }
+);
diff --git a/platform/features/timeline/src/controllers/TimelineController.js b/platform/features/timeline/src/controllers/TimelineController.js
new file mode 100644
index 0000000000..8667bfec5f
--- /dev/null
+++ b/platform/features/timeline/src/controllers/TimelineController.js
@@ -0,0 +1,149 @@
+/*****************************************************************************
+ * Open MCT Web, Copyright (c) 2009-2015, United States Government
+ * as represented by the Administrator of the National Aeronautics and Space
+ * Administration. All rights reserved.
+ *
+ * Open MCT Web is licensed under the Apache License, Version 2.0 (the
+ * "License"); you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ * http://www.apache.org/licenses/LICENSE-2.0.
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
+ * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
+ * License for the specific language governing permissions and limitations
+ * under the License.
+ *
+ * Open MCT Web includes source code licensed under additional open source
+ * licenses. See the Open Source Licenses file (LICENSES.md) included with
+ * this source code distribution or the Licensing information page available
+ * at runtime from the About dialog for additional information.
+ *****************************************************************************/
+/*global define*/
+
+define(
+ [
+ './swimlane/TimelineSwimlanePopulator',
+ './graph/TimelineGraphPopulator',
+ './drag/TimelineDragPopulator'
+ ],
+ function (
+ TimelineSwimlanePopulator,
+ TimelineGraphPopulator,
+ TimelineDragPopulator
+ ) {
+ 'use strict';
+
+ /**
+ * Controller for the Timeline view.
+ * @constructor
+ */
+ function TimelineController($scope, $q, objectLoader, MINIMUM_DURATION) {
+ var swimlanePopulator = new TimelineSwimlanePopulator(
+ objectLoader,
+ $scope.configuration || {},
+ $scope.selection
+ ),
+ graphPopulator = new TimelineGraphPopulator($q),
+ dragPopulator = new TimelineDragPopulator(objectLoader);
+
+ // Hash together all modification times. A sum is sufficient here,
+ // since modified timestamps should be non-decreasing.
+ function modificationSum() {
+ var sum = 0;
+ swimlanePopulator.get().forEach(function (swimlane) {
+ sum += swimlane.domainObject.getModel().modified || 0;
+ });
+ return sum;
+ }
+
+ // Reduce graph states to a watch-able number. A bitmask is
+ // sufficient here, since only ~30 graphed elements make sense
+ // (due to limits on recognizably unique line colors)
+ function graphMask() {
+ var mask = 0, bit = 1;
+ swimlanePopulator.get().forEach(function (swimlane) {
+ mask += swimlane.graph() ? 0 : bit;
+ bit *= 2;
+ });
+ return mask;
+ }
+
+ // Repopulate based on detected modification to in-view objects
+ function repopulateSwimlanes() {
+ swimlanePopulator.populate($scope.domainObject);
+ dragPopulator.populate($scope.domainObject);
+ graphPopulator.populate(swimlanePopulator.get());
+ }
+
+ // Repopulate graphs based on modification to swimlane graph state
+ function repopulateGraphs() {
+ graphPopulator.populate(swimlanePopulator.get());
+ }
+
+ // Get pixel width for right pane, using zoom controller
+ function width(zoomController) {
+ var start = swimlanePopulator.start(),
+ end = swimlanePopulator.end();
+ return zoomController.toPixels(zoomController.duration(
+ Math.max(end - start, MINIMUM_DURATION)
+ ));
+ }
+
+ // Refresh resource graphs
+ function refresh() {
+ if (graphPopulator) {
+ graphPopulator.get().forEach(function (graph) {
+ graph.refresh();
+ });
+ }
+ }
+
+ // Recalculate swimlane state on changes
+ $scope.$watch("domainObject", swimlanePopulator.populate);
+
+ // Also recalculate whenever anything in view is modified
+ $scope.$watch(modificationSum, repopulateSwimlanes);
+
+ // Carry over changes in swimlane set to changes in graphs
+ $scope.$watch(graphMask, repopulateGraphs);
+
+ // Convey current selection to drag handle populator
+ $scope.$watch("selection.get()", dragPopulator.select);
+
+ // Provide initial scroll bar state, container for pane positions
+ $scope.scroll = { x: 0, y: 0 };
+ $scope.panes = {};
+
+ // Expose active set of swimlanes
+ return {
+ /**
+ * Get the width, in pixels, of the timeline area
+ * @returns {number} width, in pixels
+ */
+ width: width,
+ /**
+ * Get the swimlanes which should currently be displayed.
+ * @returns {TimelineSwimlane[]} the swimlanes
+ */
+ swimlanes: swimlanePopulator.get,
+ /**
+ * Get the resource graphs which should currently be displayed.
+ * @returns {TimelineGraph[]} the graphs
+ */
+ graphs: graphPopulator.get,
+ /**
+ * Get drag handles for the current selection.
+ * @returns {TimelineDragHandle[]} the drag handles
+ */
+ handles: dragPopulator.get,
+ /**
+ * Refresh resource graphs (during drag.)
+ */
+ refresh: refresh
+ };
+ }
+
+ return TimelineController;
+ }
+);
diff --git a/platform/features/timeline/src/controllers/TimelineDateTimeController.js b/platform/features/timeline/src/controllers/TimelineDateTimeController.js
new file mode 100644
index 0000000000..f986b3fe7e
--- /dev/null
+++ b/platform/features/timeline/src/controllers/TimelineDateTimeController.js
@@ -0,0 +1,93 @@
+/*****************************************************************************
+ * Open MCT Web, Copyright (c) 2009-2015, United States Government
+ * as represented by the Administrator of the National Aeronautics and Space
+ * Administration. All rights reserved.
+ *
+ * Open MCT Web is licensed under the Apache License, Version 2.0 (the
+ * "License"); you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ * http://www.apache.org/licenses/LICENSE-2.0.
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
+ * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
+ * License for the specific language governing permissions and limitations
+ * under the License.
+ *
+ * Open MCT Web includes source code licensed under additional open source
+ * licenses. See the Open Source Licenses file (LICENSES.md) included with
+ * this source code distribution or the Licensing information page available
+ * at runtime from the About dialog for additional information.
+ *****************************************************************************/
+/*global define,moment*/
+
+define(
+ [],
+ function () {
+ "use strict";
+
+ /**
+ * Controller for the `datetime` form control.
+ * This is a composite control; it includes multiple
+ * input fields but outputs a single timestamp (in
+ * milliseconds since start of 1970) to the ngModel.
+ *
+ * @constructor
+ */
+ function DateTimeController($scope) {
+
+ // Update the data model
+ function updateModel(datetime) {
+ var days = parseInt(datetime.days, 10) || 0,
+ hour = parseInt(datetime.hours, 10) || 0,
+ min = parseInt(datetime.minutes, 10) || 0,
+ sec = parseInt(datetime.seconds, 10) || 0,
+ epoch = "SET", // Only permit SET, for now
+ timestamp;
+
+ // Build up timestamp
+ timestamp = days * 24;
+ timestamp = (hour + timestamp) * 60;
+ timestamp = (min + timestamp) * 60;
+ timestamp = (sec + timestamp) * 1000;
+
+ // Set in the model
+ $scope.ngModel[$scope.field] = {
+ timestamp: timestamp,
+ epoch: epoch
+ };
+ }
+
+ // Update the displayed state
+ function updateForm(modelState) {
+ var timestamp = (modelState || {}).timestamp || 0,
+ datetime = $scope.datetime;
+
+ timestamp = Math.floor(timestamp / 1000);
+ datetime.seconds = timestamp % 60;
+ timestamp = Math.floor(timestamp / 60);
+ datetime.minutes = timestamp % 60;
+ timestamp = Math.floor(timestamp / 60);
+ datetime.hours = timestamp % 24;
+ timestamp = Math.floor(timestamp / 24);
+ datetime.days = timestamp;
+ }
+
+ // Retrieve state from field, for watch
+ function getModelState() {
+ return $scope.ngModel[$scope.field];
+ }
+
+ // Update value whenever any field changes.
+ $scope.$watchCollection("datetime", updateModel);
+ $scope.$watchCollection(getModelState, updateForm);
+
+ // Initialize the scope
+ $scope.datetime = {};
+ updateForm(getModelState());
+ }
+
+ return DateTimeController;
+
+ }
+);
\ No newline at end of file
diff --git a/platform/features/timeline/src/controllers/TimelineGanttController.js b/platform/features/timeline/src/controllers/TimelineGanttController.js
new file mode 100644
index 0000000000..163e8dca8a
--- /dev/null
+++ b/platform/features/timeline/src/controllers/TimelineGanttController.js
@@ -0,0 +1,88 @@
+/*****************************************************************************
+ * Open MCT Web, Copyright (c) 2009-2015, United States Government
+ * as represented by the Administrator of the National Aeronautics and Space
+ * Administration. All rights reserved.
+ *
+ * Open MCT Web is licensed under the Apache License, Version 2.0 (the
+ * "License"); you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ * http://www.apache.org/licenses/LICENSE-2.0.
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
+ * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
+ * License for the specific language governing permissions and limitations
+ * under the License.
+ *
+ * Open MCT Web includes source code licensed under additional open source
+ * licenses. See the Open Source Licenses file (LICENSES.md) included with
+ * this source code distribution or the Licensing information page available
+ * at runtime from the About dialog for additional information.
+ *****************************************************************************/
+/*global define*/
+
+define(
+ [],
+ function () {
+ "use strict";
+
+ /**
+ * Control for Gantt bars in a timeline view.
+ * Primarily reesponsible for supporting the positioning of Gantt
+ * bars; particularly, this ensures that the left and right edges
+ * never go to far off screen, because in some environments this
+ * will effect rendering performance without visible results.
+ * @constructor
+ * @param {number} MAXIMUM_OFFSCREEN the maximum number of pixels
+ * allowed to go off-screen (to either the left or the right)
+ */
+ function TimelineGanttController(MAXIMUM_OFFSCREEN) {
+ // Pixel position for the CSS left property
+ function left(timespan, scroll, toPixels) {
+ return Math.max(
+ toPixels(timespan.getStart()),
+ scroll.x - MAXIMUM_OFFSCREEN
+ );
+ }
+
+ // Pixel value for the CSS width property
+ function width(timespan, scroll, toPixels) {
+ var x = left(timespan, scroll, toPixels),
+ right = Math.min(
+ toPixels(timespan.getEnd()),
+ scroll.x + scroll.width + MAXIMUM_OFFSCREEN
+ );
+ return right - x;
+ }
+
+ return {
+ /**
+ * Get the pixel position for the `left` style property
+ * of a Gantt bar for the specified timespan.
+ * @param {Timespan} timespan the timespan to be represented
+ * @param scroll an object containing an `x` and `width`
+ * property, representing the scroll position and
+ * visible width, respectively.
+ * @param {Function} toPixels a function to convert
+ * a timestamp to a pixel position
+ * @returns {number} the pixel position of the left edge
+ */
+ left: left,
+ /**
+ * Get the pixel value for the `width` style property
+ * of a Gantt bar for the specified timespan.
+ * @param {Timespan} timespan the timespan to be represented
+ * @param scroll an object containing an `x` and `width`
+ * property, representing the scroll position and
+ * visible width, respectively.
+ * @param {Function} toPixels a function to convert
+ * a timestamp to a pixel position
+ * @returns {number} the pixel width of this Gantt bar
+ */
+ width: width
+ };
+ }
+
+ return TimelineGanttController;
+ }
+);
\ No newline at end of file
diff --git a/platform/features/timeline/src/controllers/TimelineGraphController.js b/platform/features/timeline/src/controllers/TimelineGraphController.js
new file mode 100644
index 0000000000..1b45efad5c
--- /dev/null
+++ b/platform/features/timeline/src/controllers/TimelineGraphController.js
@@ -0,0 +1,97 @@
+/*****************************************************************************
+ * Open MCT Web, Copyright (c) 2009-2015, United States Government
+ * as represented by the Administrator of the National Aeronautics and Space
+ * Administration. All rights reserved.
+ *
+ * Open MCT Web is licensed under the Apache License, Version 2.0 (the
+ * "License"); you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ * http://www.apache.org/licenses/LICENSE-2.0.
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
+ * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
+ * License for the specific language governing permissions and limitations
+ * under the License.
+ *
+ * Open MCT Web includes source code licensed under additional open source
+ * licenses. See the Open Source Licenses file (LICENSES.md) included with
+ * this source code distribution or the Licensing information page available
+ * at runtime from the About dialog for additional information.
+ *****************************************************************************/
+/*global define*/
+define(
+ [],
+ function () {
+ 'use strict';
+
+ /**
+ * Controller for the graph area of a timeline view.
+ * The set of graphs to show is provided by the timeline
+ * controller and communicated into the template via "parameters"
+ * in scope.
+ * @constructor
+ */
+ function TimelineGraphController($scope, resources) {
+ var resourceMap = {},
+ labelCache = {};
+
+ // Add an element to the resource map
+ function addToResourceMap(resource) {
+ var key = resource.key;
+ if (key && !resourceMap[key]) {
+ resourceMap[key] = resource;
+ }
+ }
+
+ // Update the display bounds for all graphs to match
+ // scroll and/or width.
+ function updateGraphs(parameters) {
+ (parameters.graphs || []).forEach(function (graph) {
+ graph.setBounds(parameters.origin, parameters.duration);
+ });
+ }
+
+ // Add all resources to map for simpler lookup
+ resources.forEach(addToResourceMap);
+
+ // Update graphs as parameters change
+ $scope.$watchCollection("parameters", updateGraphs);
+
+ return {
+ /**
+ * Get a label object (suitable to pass into the
+ * `timeline-resource-graph-labels` template) for
+ * the specified graph.
+ * @param {TimelineGraph} the graph to label
+ * @returns {object} an object containing labels
+ */
+ label: function (graph) {
+ var key = graph.key,
+ resource = resourceMap[key] || {},
+ name = resource.name || "",
+ units = resource.units,
+ min = graph.minimum() || 0,
+ max = graph.maximum() || 0,
+ label = labelCache[key] || {};
+
+ // Cache the label (this is passed into a template,
+ // so avoid excessive digest cycles)
+ labelCache[key] = label;
+
+ // Include units in title
+ label.title = name + (units ? (" (" + units + ")") : "");
+
+ // Provide low, middle, high data values
+ label.low = min.toFixed(3);
+ label.middle = ((min + max) / 2).toFixed(3);
+ label.high = max.toFixed(3);
+
+ return label;
+ }
+ };
+ }
+
+ return TimelineGraphController;
+ }
+);
\ No newline at end of file
diff --git a/platform/features/timeline/src/controllers/TimelineTableController.js b/platform/features/timeline/src/controllers/TimelineTableController.js
new file mode 100644
index 0000000000..986a44222f
--- /dev/null
+++ b/platform/features/timeline/src/controllers/TimelineTableController.js
@@ -0,0 +1,53 @@
+/*****************************************************************************
+ * Open MCT Web, Copyright (c) 2009-2015, United States Government
+ * as represented by the Administrator of the National Aeronautics and Space
+ * Administration. All rights reserved.
+ *
+ * Open MCT Web is licensed under the Apache License, Version 2.0 (the
+ * "License"); you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ * http://www.apache.org/licenses/LICENSE-2.0.
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
+ * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
+ * License for the specific language governing permissions and limitations
+ * under the License.
+ *
+ * Open MCT Web includes source code licensed under additional open source
+ * licenses. See the Open Source Licenses file (LICENSES.md) included with
+ * this source code distribution or the Licensing information page available
+ * at runtime from the About dialog for additional information.
+ *****************************************************************************/
+/*global define*/
+
+define(
+ ["../TimelineFormatter"],
+ function (TimelineFormatter) {
+ "use strict";
+
+ var FORMATTER = new TimelineFormatter();
+
+ /**
+ * Provides tabular data for the Timeline's tabular view area.
+ */
+ function TimelineTableController() {
+
+ function getNiceTime(millis) {
+ return FORMATTER.format(millis);
+ }
+
+ return {
+ /**
+ * Return human-readable time in the expected format,
+ * currently SET.
+ * @param {number} millis duration, in millisecond
+ * @return {string} human-readable duration
+ */
+ niceTime: getNiceTime
+ };
+ }
+
+ return TimelineTableController;
+ }
+);
diff --git a/platform/features/timeline/src/controllers/TimelineTickController.js b/platform/features/timeline/src/controllers/TimelineTickController.js
new file mode 100644
index 0000000000..62b6095fc0
--- /dev/null
+++ b/platform/features/timeline/src/controllers/TimelineTickController.js
@@ -0,0 +1,118 @@
+/*****************************************************************************
+ * Open MCT Web, Copyright (c) 2009-2015, United States Government
+ * as represented by the Administrator of the National Aeronautics and Space
+ * Administration. All rights reserved.
+ *
+ * Open MCT Web is licensed under the Apache License, Version 2.0 (the
+ * "License"); you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ * http://www.apache.org/licenses/LICENSE-2.0.
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
+ * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
+ * License for the specific language governing permissions and limitations
+ * under the License.
+ *
+ * Open MCT Web includes source code licensed under additional open source
+ * licenses. See the Open Source Licenses file (LICENSES.md) included with
+ * this source code distribution or the Licensing information page available
+ * at runtime from the About dialog for additional information.
+ *****************************************************************************/
+/*global define*/
+
+define(
+ ["../TimelineFormatter"],
+ function (TimelineFormatter) {
+ "use strict";
+
+ var FORMATTER = new TimelineFormatter();
+
+ /**
+ * Provides labels for the tick mark area of a timeline view.
+ * Since the tick mark regin is potentially extremeley large,
+ * only the subset of ticks which will actually be shown in
+ * view are provided.
+ * @constructor
+ */
+ function TimelineTickController() {
+ var labels = [],
+ lastFirst,
+ lastStep,
+ lastCount,
+ lastStartMillis,
+ lastEndMillis;
+
+ // Actually recalculate the labels from scratch
+ function calculateLabels(first, count, step, toMillis) {
+ var result = [],
+ current;
+
+ // Create enough labels to fill the visible area
+ while (result.length < count) {
+ current = first + step * result.length;
+ result.push({
+ // Horizontal pixel position of this label
+ left: current,
+ // Text to display in this label
+ text: FORMATTER.format(toMillis(current))
+ });
+ }
+
+ return result;
+ }
+
+ // Get tick labels for this pixel span (recalculating if needed)
+ function getLabels(start, width, step, toMillis) {
+ // Calculate parameters for labels (first pixel position, last
+ // pixel position.) These are checked to detect changes.
+ var first = Math.floor(start / step) * step,
+ last = Math.ceil((start + width) / step) * step,
+ count = ((last - first) / step) + 1,
+ startMillis = toMillis(first),
+ endMillis = toMillis(last),
+ changed = (lastFirst !== first) ||
+ (lastCount !== count) ||
+ (lastStep !== step) ||
+ (lastStartMillis !== startMillis) ||
+ (lastEndMillis !== endMillis);
+
+ // This will be used in a template, so only recalculate on
+ // change.
+ if (changed) {
+ labels = calculateLabels(first, count, step, toMillis);
+ // Cache to avoid recomputing later
+ lastFirst = first;
+ lastCount = count;
+ lastStep = step;
+ lastStartMillis = startMillis;
+ lastEndMillis = endMillis;
+ }
+
+ return labels;
+ }
+
+
+ return {
+ /**
+ * Get labels for use in the visible region of a timeline's
+ * tick mark area. This will return the same array instance
+ * (without recalculating its contents) if called with the
+ * same parameters (and same apparent zoom state, as determined
+ * via `toMillis`), so it is safe to use in a template.
+ *
+ * @param {number} start left-most pixel position in view
+ * @param {number} width pixel width in view
+ * @param {number} step size, in pixels, of each major tick
+ * @param {Function} toMillis function to convert from pixel
+ * positions to milliseconds
+ * @returns {Array} an array of tick mark labels, suitable
+ * for use in the `timeline-ticks` template
+ */
+ labels: getLabels
+ };
+ }
+
+ return TimelineTickController;
+ }
+);
\ No newline at end of file
diff --git a/platform/features/timeline/src/controllers/TimelineZoomController.js b/platform/features/timeline/src/controllers/TimelineZoomController.js
new file mode 100644
index 0000000000..13fb600923
--- /dev/null
+++ b/platform/features/timeline/src/controllers/TimelineZoomController.js
@@ -0,0 +1,130 @@
+/*****************************************************************************
+ * Open MCT Web, Copyright (c) 2009-2015, United States Government
+ * as represented by the Administrator of the National Aeronautics and Space
+ * Administration. All rights reserved.
+ *
+ * Open MCT Web is licensed under the Apache License, Version 2.0 (the
+ * "License"); you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ * http://www.apache.org/licenses/LICENSE-2.0.
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
+ * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
+ * License for the specific language governing permissions and limitations
+ * under the License.
+ *
+ * Open MCT Web includes source code licensed under additional open source
+ * licenses. See the Open Source Licenses file (LICENSES.md) included with
+ * this source code distribution or the Licensing information page available
+ * at runtime from the About dialog for additional information.
+ *****************************************************************************/
+/*global define*/
+define(
+ ['../TimelineFormatter'],
+ function (TimelineFormatter) {
+ "use strict";
+
+
+ var FORMATTER = new TimelineFormatter();
+
+ /**
+ * Controls the pan-zoom state of a timeline view.
+ * @constructor
+ */
+ function TimelineZoomController($scope, ZOOM_CONFIGURATION) {
+ // Prefer to start with the middle index
+ var zoomLevels = ZOOM_CONFIGURATION.levels || [ 1000 ],
+ zoomIndex = Math.floor(zoomLevels.length / 2),
+ tickWidth = ZOOM_CONFIGURATION.width || 200,
+ duration = 86400000; // Default duration in view
+
+ // Round a duration to a larger value, to ensure space for editing
+ function roundDuration(value) {
+ // Ensure there's always an extra day or so
+ var sz = zoomLevels[zoomLevels.length - 1];
+ value *= 1.25; // Add 25% padding to start
+ return Math.ceil(value / sz) * sz;
+ }
+
+ // Get/set zoom level
+ function setZoomLevel(level) {
+ if (!isNaN(level)) {
+ // Modify zoom level, keeping it in range
+ zoomIndex = Math.min(
+ Math.max(level, 0),
+ zoomLevels.length - 1
+ );
+ }
+ }
+
+ // Persist current zoom level
+ function storeZoom() {
+ var isEditMode = $scope.commit &&
+ $scope.domainObject &&
+ $scope.domainObject.hasCapability('editor');
+ if (isEditMode) {
+ $scope.configuration = $scope.configuration || {};
+ $scope.configuration.zoomLevel = zoomIndex;
+ $scope.commit();
+ }
+ }
+
+ $scope.$watch("configuration.zoomLevel", setZoomLevel);
+
+ return {
+ /**
+ * Increase or decrease the current zoom level by a given
+ * number of steps. Positive steps zoom in, negative steps
+ * zoom out.
+ * If called with no arguments, this returns the current
+ * zoom level, expressed as the number of milliseconds
+ * associated with a given tick mark.
+ * @param {number} steps how many steps to zoom in
+ * @returns {number} current zoom level (as the size of a
+ * major tick mark, in pixels)
+ */
+ zoom: function (amount) {
+ // Update the zoom level if called with an argument
+ if (arguments.length > 0 && !isNaN(amount)) {
+ setZoomLevel(zoomIndex + amount);
+ storeZoom(zoomIndex);
+ }
+ return zoomLevels[zoomIndex];
+ },
+ /**
+ * Get the width, in pixels, of a specific time duration at
+ * the current zoom level.
+ * @returns {number} the number of pixels
+ */
+ toPixels: function (millis) {
+ return tickWidth * millis / zoomLevels[zoomIndex];
+ },
+ /**
+ * Get the time duration, in milliseconds, occupied by the
+ * width (specified in pixels) at the current zoom level.
+ * @returns {number} the number of pixels
+ */
+ toMillis: function (pixels) {
+ return (pixels / tickWidth) * zoomLevels[zoomIndex];
+ },
+ /**
+ * Get or set the current displayed duration. If used as a
+ * setter, this will typically be rounded up to ensure extra
+ * space is available at the right.
+ * @returns {number} duration, in milliseconds
+ */
+ duration: function (value) {
+ var prior = duration;
+ if (arguments.length > 0) {
+ duration = roundDuration(value);
+ }
+ return duration;
+ }
+ };
+ }
+
+ return TimelineZoomController;
+
+ }
+);
diff --git a/platform/features/timeline/src/controllers/drag/TimelineDragHandleFactory.js b/platform/features/timeline/src/controllers/drag/TimelineDragHandleFactory.js
new file mode 100644
index 0000000000..6f1dc56d7c
--- /dev/null
+++ b/platform/features/timeline/src/controllers/drag/TimelineDragHandleFactory.js
@@ -0,0 +1,76 @@
+/*****************************************************************************
+ * Open MCT Web, Copyright (c) 2009-2015, United States Government
+ * as represented by the Administrator of the National Aeronautics and Space
+ * Administration. All rights reserved.
+ *
+ * Open MCT Web is licensed under the Apache License, Version 2.0 (the
+ * "License"); you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ * http://www.apache.org/licenses/LICENSE-2.0.
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
+ * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
+ * License for the specific language governing permissions and limitations
+ * under the License.
+ *
+ * Open MCT Web includes source code licensed under additional open source
+ * licenses. See the Open Source Licenses file (LICENSES.md) included with
+ * this source code distribution or the Licensing information page available
+ * at runtime from the About dialog for additional information.
+ *****************************************************************************/
+/*global define*/
+
+define(
+ ['./TimelineStartHandle', './TimelineEndHandle', './TimelineMoveHandle'],
+ function (TimelineStartHandle, TimelineEndHandle, TimelineMoveHandle) {
+ "use strict";
+
+
+ var DEFAULT_HANDLES = [
+ TimelineStartHandle,
+ TimelineMoveHandle,
+ TimelineEndHandle
+ ],
+ TIMELINE_HANDLES = [
+ TimelineStartHandle,
+ TimelineMoveHandle
+ ];
+
+ /**
+ * Create a factory for drag handles for timelines/activities
+ * in a timeline view.
+ * @constructor
+ */
+ function TimelineDragHandleFactory(dragHandler, snapHandler) {
+ return {
+ /**
+ * Create drag handles for this domain object.
+ * @param {DomainObject} domainObject the object to be
+ * manipulated by these gestures
+ * @returns {Array} array of drag handles
+ */
+ handles: function (domainObject) {
+ var type = domainObject.getCapability('type'),
+ id = domainObject.getId();
+
+ // Instantiate a handle
+ function instantiate(Handle) {
+ return new Handle(
+ id,
+ dragHandler,
+ snapHandler
+ );
+ }
+
+ // Instantiate smaller set of handles for timelines
+ return (type && type.instanceOf('timeline') ?
+ TIMELINE_HANDLES : DEFAULT_HANDLES)
+ .map(instantiate);
+ }
+ };
+ }
+
+ return TimelineDragHandleFactory;
+ }
+);
diff --git a/platform/features/timeline/src/controllers/drag/TimelineDragHandler.js b/platform/features/timeline/src/controllers/drag/TimelineDragHandler.js
new file mode 100644
index 0000000000..ff1147bbae
--- /dev/null
+++ b/platform/features/timeline/src/controllers/drag/TimelineDragHandler.js
@@ -0,0 +1,258 @@
+/*****************************************************************************
+ * Open MCT Web, Copyright (c) 2009-2015, United States Government
+ * as represented by the Administrator of the National Aeronautics and Space
+ * Administration. All rights reserved.
+ *
+ * Open MCT Web is licensed under the Apache License, Version 2.0 (the
+ * "License"); you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ * http://www.apache.org/licenses/LICENSE-2.0.
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
+ * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
+ * License for the specific language governing permissions and limitations
+ * under the License.
+ *
+ * Open MCT Web includes source code licensed under additional open source
+ * licenses. See the Open Source Licenses file (LICENSES.md) included with
+ * this source code distribution or the Licensing information page available
+ * at runtime from the About dialog for additional information.
+ *****************************************************************************/
+/*global define*/
+
+define(
+ [],
+ function () {
+ "use strict";
+
+ /**
+ * Handles business logic (mutation of objects, retrieval of start/end
+ * times) associated with drag gestures to manipulate start/end times
+ * of activities and timelines in a Timeline view.
+ * @constructor
+ * @param {DomainObject} domainObject the object being viewed
+ * @param {ObjectLoader} objectLoader service to assist in loading
+ * subtrees
+ */
+ function TimelineDragHandler(domainObject, objectLoader) {
+ var timespans = {},
+ persists = {},
+ mutations = {},
+ compositions = {},
+ dirty = {};
+
+ // "Cast" a domainObject to an id, if necessary
+ function toId(value) {
+ return (typeof value !== 'string' && value.getId) ?
+ value.getId() : value;
+ }
+
+ // Get the timespan associated with this domain object
+ function populateCapabilityMaps(domainObject) {
+ var id = domainObject.getId(),
+ timespanPromise = domainObject.useCapability('timespan');
+ if (timespanPromise) {
+ timespanPromise.then(function (timespan) {
+ // Cache that timespan
+ timespans[id] = timespan;
+ // And its mutation capability
+ mutations[id] = domainObject.getCapability('mutation');
+ // Also cache the persistence capability for later
+ persists[id] = domainObject.getCapability('persistence');
+ // And the composition, for bulk moves
+ compositions[id] = domainObject.getModel().composition || [];
+ });
+ }
+ }
+
+ // Populate the id->timespan map
+ function populateTimespans(subgraph) {
+ populateCapabilityMaps(subgraph.domainObject);
+ subgraph.composition.forEach(populateTimespans);
+ }
+
+ // Persist changes for objects by id (when dragging ends)
+ function doPersist(id) {
+ var persistence = persists[id],
+ mutation = mutations[id];
+ if (mutation) {
+ // Mutate just to update the timestamp (since we
+ // explicitly don't do this during the drag to
+ // avoid firing a ton of refreshes.)
+ mutation.mutate(function () {});
+ }
+ if (persistence) {
+ // Persist the changes
+ persistence.persist();
+ }
+ }
+
+ // Use the object loader to get objects which have timespans
+ objectLoader.load(domainObject, 'timespan').then(populateTimespans);
+
+ return {
+ /**
+ * Get a list of identifiers for domain objects which have
+ * timespans that are managed here.
+ * @returns {string[]} ids for all objects which have managed
+ * timespans here
+ */
+ ids: function () {
+ return Object.keys(timespans).sort();
+ },
+ /**
+ * Persist any changes to timespans that have been made through
+ * this handler.
+ */
+ persist: function () {
+ // Persist every dirty object...
+ Object.keys(dirty).forEach(doPersist);
+ // Clear out the dirty list
+ dirty = {};
+ },
+ /**
+ * Get the start time for a specific domain object. The domain
+ * object may be specified by its identifier, or passed as a
+ * domain object instance. If a second, numeric argument is
+ * passed, this functions as a setter.
+ * @returns {number} the start time
+ * @param {string|DomainObject} id the domain object to modify
+ * @param {number} [value] the new value
+ */
+ start: function (id, value) {
+ // Convert to domain object id, look up timespan
+ var timespan = timespans[toId(id)];
+ // Use as setter if argument is present
+ if ((typeof value === 'number') && timespan) {
+ // Set the start (ensuring that it's non-negative,
+ // and not after the end time.)
+ timespan.setStart(
+ Math.min(Math.max(value, 0), timespan.getEnd())
+ );
+ // Mark as dirty for subsequent persistence
+ dirty[toId(id)] = true;
+ }
+ // Return value from the timespan
+ return timespan && timespan.getStart();
+ },
+ /**
+ * Get the end time for a specific domain object. The domain
+ * object may be specified by its identifier, or passed as a
+ * domain object instance. If a second, numeric argument is
+ * passed, this functions as a setter.
+ * @returns {number} the end time
+ * @param {string|DomainObject} id the domain object to modify
+ * @param {number} [value] the new value
+ */
+ end: function (id, value) {
+ // Convert to domain object id, look up timespan
+ var timespan = timespans[toId(id)];
+ // Use as setter if argument is present
+ if ((typeof value === 'number') && timespan) {
+ // Set the end (ensuring it doesn't preceed start)
+ timespan.setEnd(
+ Math.max(value, timespan.getStart())
+ );
+ // Mark as dirty for subsequent persistence
+ dirty[toId(id)] = true;
+ }
+ // Return value from the timespan
+ return timespan && timespan.getEnd();
+ },
+ /**
+ * Get the duration for a specific domain object. The domain
+ * object may be specified by its identifier, or passed as a
+ * domain object instance. If a second, numeric argument is
+ * passed, this functions as a setter.
+ * @returns {number} the duration
+ * @param {string|DomainObject} id the domain object to modify
+ * @param {number} [value] the new value
+ */
+ duration: function (id, value) {
+ // Convert to domain object id, look up timespan
+ var timespan = timespans[toId(id)];
+ // Use as setter if argument is present
+ if ((typeof value === 'number') && timespan) {
+ // Set duration (ensure that it's non-negative)
+ timespan.setDuration(
+ Math.max(value, 0)
+ );
+ // Mark as dirty for subsequent persistence
+ dirty[toId(id)] = true;
+ }
+ // Return value from the timespan
+ return timespan && timespan.getDuration();
+ },
+ /**
+ * Move the start and end of this domain object by the
+ * specified delta. Contained objects will move as well.
+ * @param {string|DomainObject} id the domain object to modify
+ * @param {number} delta the amount by which to change
+ */
+ move: function (id, delta) {
+ // Overview of algorithm used here:
+ // - Build up list of ids to actually move
+ // - Find the minimum start time
+ // - Change delta so it cannot move minimum past 0
+ // - Update start, then end time
+ var ids = {},
+ queue = [toId(id)],
+ minStart;
+
+ // Update start & end, in that order
+ function updateStartEnd(id) {
+ var timespan = timespans[id], start, end;
+ if (timespan) {
+ // Get start/end so we don't get fooled by our
+ // own adjustments
+ start = timespan.getStart();
+ end = timespan.getEnd();
+ // Update start, then end
+ timespan.setStart(start + delta);
+ timespan.setEnd(end + delta);
+ // Mark as dirty for subsequent persistence
+ dirty[toId(id)] = true;
+ }
+ }
+
+ // Build up set of ids
+ while (queue.length > 0) {
+ // Get the next id to consider
+ id = queue.shift();
+ // If we haven't already considered this...
+ if (!ids[id]) {
+ // Add it to the set
+ ids[id] = true;
+ // And queue up its composition
+ queue = queue.concat(compositions[id] || []);
+ }
+ }
+
+ // Find the minimum start time
+ minStart = Object.keys(ids).map(function (id) {
+ // Get the start time; default to +Inf if not
+ // found, since this will not survive a min
+ // test if any real timespans are present
+ return timespans[id] ?
+ timespans[id].getStart() :
+ Number.POSITIVE_INFINITY;
+ }).reduce(function (a, b) {
+ // Reduce with a minimum test
+ return Math.min(a, b);
+ }, Number.POSITIVE_INFINITY);
+
+ // Ensure delta doesn't exceed bounds
+ delta = Math.max(delta, -minStart);
+
+ // Update start/end times
+ if (delta !== 0) {
+ Object.keys(ids).forEach(updateStartEnd);
+ }
+ }
+ };
+ }
+
+ return TimelineDragHandler;
+ }
+);
\ No newline at end of file
diff --git a/platform/features/timeline/src/controllers/drag/TimelineDragPopulator.js b/platform/features/timeline/src/controllers/drag/TimelineDragPopulator.js
new file mode 100644
index 0000000000..e21080b003
--- /dev/null
+++ b/platform/features/timeline/src/controllers/drag/TimelineDragPopulator.js
@@ -0,0 +1,97 @@
+/*****************************************************************************
+ * Open MCT Web, Copyright (c) 2009-2015, United States Government
+ * as represented by the Administrator of the National Aeronautics and Space
+ * Administration. All rights reserved.
+ *
+ * Open MCT Web is licensed under the Apache License, Version 2.0 (the
+ * "License"); you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ * http://www.apache.org/licenses/LICENSE-2.0.
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
+ * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
+ * License for the specific language governing permissions and limitations
+ * under the License.
+ *
+ * Open MCT Web includes source code licensed under additional open source
+ * licenses. See the Open Source Licenses file (LICENSES.md) included with
+ * this source code distribution or the Licensing information page available
+ * at runtime from the About dialog for additional information.
+ *****************************************************************************/
+/*global define*/
+
+define(
+ ['./TimelineDragHandler', './TimelineSnapHandler', './TimelineDragHandleFactory'],
+ function (TimelineDragHandler, TimelineSnapHandler, TimelineDragHandleFactory) {
+ "use strict";
+
+ /**
+ * Provides drag handles for the active selection in a timeline view.
+ * @constructor
+ */
+ function TimelineDragPopulator(objectLoader) {
+ var handles = [],
+ factory,
+ selectedObject;
+
+ // Refresh active set of drag handles
+ function refreshHandles() {
+ handles = (factory && selectedObject) ?
+ factory.handles(selectedObject) :
+ [];
+ }
+
+ // Create a new factory for handles, based on root object in view
+ function populateForObject(domainObject) {
+ var dragHandler = domainObject && new TimelineDragHandler(
+ domainObject,
+ objectLoader
+ );
+
+ // Reinstantiate the factory
+ factory = dragHandler && new TimelineDragHandleFactory(
+ dragHandler,
+ new TimelineSnapHandler(dragHandler)
+ );
+
+ // If there's a selected object, restore the handles
+ refreshHandles();
+ }
+
+ // Change the current selection
+ function select(swimlane) {
+ // Cache selection to restore handles if other changes occur
+ selectedObject = swimlane && swimlane.domainObject;
+
+ // Provide handles for this selection, if it's defined
+ refreshHandles();
+ }
+
+ return {
+ /**
+ * Get the currently-applicable set of drag handles.
+ * @returns {Array} drag handles
+ */
+ get: function () {
+ return handles;
+ },
+ /**
+ * Set the root object in view. Drag interactions consider
+ * the full graph for snapping behavior, so this is needed.
+ * @param {DomainObject} domainObject the timeline object
+ * being viewed
+ */
+ populate: populateForObject,
+ /**
+ * Update selection state. Passing undefined means there
+ * is no selection.
+ * @param {TimelineSwimlane} swimlane the selected swimlane
+ */
+ select: select
+ };
+ }
+
+ return TimelineDragPopulator;
+ }
+);
\ No newline at end of file
diff --git a/platform/features/timeline/src/controllers/drag/TimelineEndHandle.js b/platform/features/timeline/src/controllers/drag/TimelineEndHandle.js
new file mode 100644
index 0000000000..060e690fa9
--- /dev/null
+++ b/platform/features/timeline/src/controllers/drag/TimelineEndHandle.js
@@ -0,0 +1,98 @@
+/*****************************************************************************
+ * Open MCT Web, Copyright (c) 2009-2015, United States Government
+ * as represented by the Administrator of the National Aeronautics and Space
+ * Administration. All rights reserved.
+ *
+ * Open MCT Web is licensed under the Apache License, Version 2.0 (the
+ * "License"); you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ * http://www.apache.org/licenses/LICENSE-2.0.
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
+ * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
+ * License for the specific language governing permissions and limitations
+ * under the License.
+ *
+ * Open MCT Web includes source code licensed under additional open source
+ * licenses. See the Open Source Licenses file (LICENSES.md) included with
+ * this source code distribution or the Licensing information page available
+ * at runtime from the About dialog for additional information.
+ *****************************************************************************/
+/*global define*/
+
+define(
+ ['../../TimelineConstants'],
+ function (Constants) {
+ "use strict";
+
+ /**
+ * Handle for changing the end time of a timeline or
+ * activity in the Timeline view.
+ * @constructor
+ * @param {string} id identifier of the domain object
+ * @param {TimelineDragHandler} dragHandler the handler which
+ * will update object state
+ * @param {TimelineSnapHandler} snapHandler the handler which
+ * provides candidate snap-to locations.
+ */
+ function TimelineEndHandle(id, dragHandler, snapHandler) {
+ var initialEnd;
+
+ // Get the snap-to location for a timestamp
+ function snap(timestamp, zoom) {
+ return snapHandler.snap(
+ timestamp,
+ zoom.toMillis(Constants.SNAP_WIDTH),
+ id
+ );
+ }
+
+ return {
+ /**
+ * Start dragging this handle.
+ */
+ begin: function () {
+ // Cache the initial state
+ initialEnd = dragHandler.end(id);
+ },
+ /**
+ * Drag this handle.
+ * @param {number} delta pixel delta from start
+ * @param {TimelineZoomController} zoom provider of zoom state
+ */
+ drag: function (delta, zoom) {
+ if (initialEnd !== undefined) {
+ // Update the state
+ dragHandler.end(
+ id,
+ snap(initialEnd + zoom.toMillis(delta), zoom)
+ );
+ }
+ },
+ /**
+ * Finish dragging this handle.
+ */
+ finish: function () {
+ // Clear initial state
+ initialEnd = undefined;
+ // Persist changes
+ dragHandler.persist();
+ },
+ /**
+ * Get a style object (suitable for passing into `ng-style`)
+ * for this handle.
+ * @param {TimelineZoomController} zoom provider of zoom state
+ */
+ style: function (zoom) {
+ return {
+ left: zoom.toPixels(dragHandler.end(id)) - Constants.HANDLE_WIDTH + 'px',
+ width: Constants.HANDLE_WIDTH + 'px'
+ };
+ }
+ };
+ }
+
+ return TimelineEndHandle;
+ }
+);
\ No newline at end of file
diff --git a/platform/features/timeline/src/controllers/drag/TimelineMoveHandle.js b/platform/features/timeline/src/controllers/drag/TimelineMoveHandle.js
new file mode 100644
index 0000000000..f2f2d082f0
--- /dev/null
+++ b/platform/features/timeline/src/controllers/drag/TimelineMoveHandle.js
@@ -0,0 +1,136 @@
+/*****************************************************************************
+ * Open MCT Web, Copyright (c) 2009-2015, United States Government
+ * as represented by the Administrator of the National Aeronautics and Space
+ * Administration. All rights reserved.
+ *
+ * Open MCT Web is licensed under the Apache License, Version 2.0 (the
+ * "License"); you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ * http://www.apache.org/licenses/LICENSE-2.0.
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
+ * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
+ * License for the specific language governing permissions and limitations
+ * under the License.
+ *
+ * Open MCT Web includes source code licensed under additional open source
+ * licenses. See the Open Source Licenses file (LICENSES.md) included with
+ * this source code distribution or the Licensing information page available
+ * at runtime from the About dialog for additional information.
+ *****************************************************************************/
+/*global define*/
+
+define(
+ ['../../TimelineConstants'],
+ function (Constants) {
+ "use strict";
+
+ /**
+ * Handle for moving (by drag) a timeline or
+ * activity in the Timeline view.
+ * @constructor
+ * @param {string} id identifier of the domain object
+ * @param {TimelineDragHandler} dragHandler the handler which
+ * will update object state
+ * @param {TimelineSnapHandler} snapHandler the handler which
+ * provides candidate snap-to locations.
+ */
+ function TimelineMoveHandle(id, dragHandler, snapHandler) {
+ var initialStart,
+ initialEnd;
+
+ // Get the snap-to location for a timestamp
+ function snap(timestamp, zoom) {
+ return snapHandler.snap(
+ timestamp,
+ zoom.toMillis(Constants.SNAP_WIDTH),
+ id
+ );
+ }
+
+ // Convert a pixel delta to a millisecond delta that will align
+ // with some useful snap location
+ function snapDelta(delta, zoom) {
+ var timeDelta = zoom.toMillis(delta),
+ desiredStart = initialStart + timeDelta,
+ desiredEnd = initialEnd + timeDelta,
+ snappedStart = snap(desiredStart, zoom),
+ snappedEnd = snap(desiredEnd, zoom),
+ diffStart = Math.abs(snappedStart - desiredStart),
+ diffEnd = Math.abs(snappedEnd - desiredEnd),
+ chooseEnd = false;
+
+ // First, check for case where both changed...
+ if ((diffStart > 0) && (diffEnd > 0)) {
+ // ...and choose the smallest change that snaps.
+ chooseEnd = diffEnd < diffStart;
+ } else {
+ // ...otherwise, snap toward the end if it changed.
+ chooseEnd = diffEnd > 0;
+ }
+ // Start is chosen if diffEnd didn't snap, or nothing snapped
+
+ // Our delta is relative to our initial state, but
+ // dragHandler.move is relative to current state, so whichever
+ // end we're snapping to, we need to compute a delta
+ // relative to the current state to get the desired result.
+ return chooseEnd ?
+ (snappedEnd - dragHandler.end(id)) :
+ (snappedStart - dragHandler.start(id));
+ }
+
+ return {
+ /**
+ * Start dragging this handle.
+ */
+ begin: function () {
+ // Cache the initial state
+ initialStart = dragHandler.start(id);
+ initialEnd = dragHandler.end(id);
+ },
+ /**
+ * Drag this handle.
+ * @param {number} delta pixel delta from start
+ * @param {TimelineZoomController} zoom provider of zoom state
+ */
+ drag: function (delta, zoom) {
+ if (initialStart !== undefined && initialEnd !== undefined) {
+ if (delta !== 0) {
+ dragHandler.move(id, snapDelta(delta, zoom));
+ }
+ }
+ },
+ /**
+ * Finish dragging this handle.
+ */
+ finish: function () {
+ // Clear initial state
+ initialStart = undefined;
+ initialEnd = undefined;
+ // Persist changes
+ dragHandler.persist();
+ },
+ /**
+ * Get a style object (suitable for passing into `ng-style`)
+ * for this handle.
+ * @param {TimelineZoomController} zoom provider of zoom state
+ */
+
+ style: function (zoom) {
+ return {
+ left: zoom.toPixels(dragHandler.start(id)) +
+ Constants.HANDLE_WIDTH +
+ 'px',
+ width: zoom.toPixels(dragHandler.duration(id)) -
+ Constants.HANDLE_WIDTH * 2
+ + 'px'
+ //cursor: initialStart === undefined ? 'grab' : 'grabbing'
+ };
+ }
+ };
+ }
+
+ return TimelineMoveHandle;
+ }
+);
\ No newline at end of file
diff --git a/platform/features/timeline/src/controllers/drag/TimelineSnapHandler.js b/platform/features/timeline/src/controllers/drag/TimelineSnapHandler.js
new file mode 100644
index 0000000000..5d2085f795
--- /dev/null
+++ b/platform/features/timeline/src/controllers/drag/TimelineSnapHandler.js
@@ -0,0 +1,106 @@
+/*****************************************************************************
+ * Open MCT Web, Copyright (c) 2009-2015, United States Government
+ * as represented by the Administrator of the National Aeronautics and Space
+ * Administration. All rights reserved.
+ *
+ * Open MCT Web is licensed under the Apache License, Version 2.0 (the
+ * "License"); you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ * http://www.apache.org/licenses/LICENSE-2.0.
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
+ * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
+ * License for the specific language governing permissions and limitations
+ * under the License.
+ *
+ * Open MCT Web includes source code licensed under additional open source
+ * licenses. See the Open Source Licenses file (LICENSES.md) included with
+ * this source code distribution or the Licensing information page available
+ * at runtime from the About dialog for additional information.
+ *****************************************************************************/
+/*global define*/
+
+define(
+ [],
+ function () {
+ "use strict";
+
+ /**
+ * Snaps timestamps to match other timestamps within a
+ * certain tolerance, to support the snap-to-start-and-end
+ * behavior of drag interactions in a timeline view.
+ * @constructor
+ * @param {TimelineDragHandler} dragHandler the handler
+ * for drag interactions, which maintains start/end
+ * information for timelines in this view.
+ */
+ function TimelineSnapHandler(dragHandler) {
+ // Snap to other end points
+ function snap(timestamp, tolerance, exclude) {
+ var result = timestamp,
+ closest = tolerance,
+ ids,
+ candidates;
+
+ // Filter an id for inclustion
+ function include(id) { return id !== exclude; }
+
+ // Evaluate a candidate timestamp as a snap-to location
+ function evaluate(candidate) {
+ var difference = Math.abs(candidate - timestamp);
+ // Is this closer than anything else we've found?
+ if (difference < closest) {
+ // ...then this is our new result
+ result = candidate;
+ // Track how close it was, for subsequent comparison.
+ closest = difference;
+ }
+ }
+
+ // Look up start time; for mapping below
+ function getStart(id) {
+ return dragHandler.start(id);
+ }
+
+ // Look up end time; for mapping below
+ function getEnd(id) {
+ return dragHandler.end(id);
+ }
+
+ // Get list of candidate ids
+ ids = dragHandler.ids().filter(include);
+
+ // Get candidate timestamps
+ candidates = ids.map(getStart).concat(ids.map(getEnd));
+
+ // ...and find the best one
+ candidates.forEach(evaluate);
+
+ // Closest candidate (or original timestamp) is our result
+ // now, so return it.
+ return result;
+ }
+
+ return {
+ /**
+ * Get a timestamp location that is near this
+ * timestamp (or simply return the provided
+ * timestamp if none are near enough, according
+ * to the specified tolerance.)
+ * Start/end times associated with the domain object
+ * with the specified identifier will be excluded
+ * from consideration (to avoid an undesired snap-to-self
+ * behavior.)
+ * @param {number} timestamp the timestamp to snap
+ * @param {number} tolerance the difference within which
+ * to snap
+ * @param {string} id the identifier to exclude
+ */
+ snap: snap
+ };
+ }
+
+ return TimelineSnapHandler;
+ }
+);
\ No newline at end of file
diff --git a/platform/features/timeline/src/controllers/drag/TimelineStartHandle.js b/platform/features/timeline/src/controllers/drag/TimelineStartHandle.js
new file mode 100644
index 0000000000..65d132e9d4
--- /dev/null
+++ b/platform/features/timeline/src/controllers/drag/TimelineStartHandle.js
@@ -0,0 +1,98 @@
+/*****************************************************************************
+ * Open MCT Web, Copyright (c) 2009-2015, United States Government
+ * as represented by the Administrator of the National Aeronautics and Space
+ * Administration. All rights reserved.
+ *
+ * Open MCT Web is licensed under the Apache License, Version 2.0 (the
+ * "License"); you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ * http://www.apache.org/licenses/LICENSE-2.0.
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
+ * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
+ * License for the specific language governing permissions and limitations
+ * under the License.
+ *
+ * Open MCT Web includes source code licensed under additional open source
+ * licenses. See the Open Source Licenses file (LICENSES.md) included with
+ * this source code distribution or the Licensing information page available
+ * at runtime from the About dialog for additional information.
+ *****************************************************************************/
+/*global define*/
+
+define(
+ ['../../TimelineConstants'],
+ function (Constants) {
+ "use strict";
+
+ /**
+ * Handle for changing the start time of a timeline or
+ * activity in the Timeline view.
+ * @constructor
+ * @param {string} id identifier of the domain object
+ * @param {TimelineDragHandler} dragHandler the handler which
+ * will update object state
+ * @param {TimelineSnapHandler} snapHandler the handler which
+ * provides candidate snap-to locations.
+ */
+ function TimelineStartHandle(id, dragHandler, snapHandler) {
+ var initialStart;
+
+ // Get the snap-to location for a timestamp
+ function snap(timestamp, zoom) {
+ return snapHandler.snap(
+ timestamp,
+ zoom.toMillis(Constants.SNAP_WIDTH),
+ id
+ );
+ }
+
+ return {
+ /**
+ * Start dragging this handle.
+ */
+ begin: function () {
+ // Cache the initial state
+ initialStart = dragHandler.start(id);
+ },
+ /**
+ * Drag this handle.
+ * @param {number} delta pixel delta from start
+ * @param {TimelineZoomController} zoom provider of zoom state
+ */
+ drag: function (delta, zoom) {
+ if (initialStart !== undefined) {
+ // Update the state
+ dragHandler.start(
+ id,
+ snap(initialStart + zoom.toMillis(delta), zoom)
+ );
+ }
+ },
+ /**
+ * Finish dragging this handle.
+ */
+ finish: function () {
+ // Clear initial state
+ initialStart = undefined;
+ // Persist changes
+ dragHandler.persist();
+ },
+ /**
+ * Get a style object (suitable for passing into `ng-style`)
+ * for this handle.
+ * @param {TimelineZoomController} zoom provider of zoom state
+ */
+ style: function (zoom) {
+ return {
+ left: zoom.toPixels(dragHandler.start(id)) + 'px',
+ width: Constants.HANDLE_WIDTH + 'px'
+ };
+ }
+ };
+ }
+
+ return TimelineStartHandle;
+ }
+);
\ No newline at end of file
diff --git a/platform/features/timeline/src/controllers/graph/TimelineGraph.js b/platform/features/timeline/src/controllers/graph/TimelineGraph.js
new file mode 100644
index 0000000000..7268020263
--- /dev/null
+++ b/platform/features/timeline/src/controllers/graph/TimelineGraph.js
@@ -0,0 +1,193 @@
+/*****************************************************************************
+ * Open MCT Web, Copyright (c) 2009-2015, United States Government
+ * as represented by the Administrator of the National Aeronautics and Space
+ * Administration. All rights reserved.
+ *
+ * Open MCT Web is licensed under the Apache License, Version 2.0 (the
+ * "License"); you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ * http://www.apache.org/licenses/LICENSE-2.0.
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
+ * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
+ * License for the specific language governing permissions and limitations
+ * under the License.
+ *
+ * Open MCT Web includes source code licensed under additional open source
+ * licenses. See the Open Source Licenses file (LICENSES.md) included with
+ * this source code distribution or the Licensing information page available
+ * at runtime from the About dialog for additional information.
+ *****************************************************************************/
+/*global define*/
+
+define(
+ [],
+ function () {
+ 'use strict';
+
+ /**
+ * Provides data to populate a graph in a timeline view.
+ * @constructor
+ * @param {string} key the resource's identifying key
+ * @param {Object.} domainObjects and object
+ * containing key-value pairs where keys are colors, and
+ * values are DomainObject instances to be drawn in that
+ * color
+ * @param {TimelineGraphRenderer} renderer a renderer which
+ * can be used to prepare Float32Array instances
+ */
+ function TimelineGraph(key, domainObjects, renderer) {
+ var drawingObject = { origin: [0, 0], dimensions: [0, 0], modified: 0},
+ // lines for the drawing object, by swimlane index
+ lines = [],
+ // min/max seen for a given swimlane, by swimlane index
+ extrema = [],
+ // current minimum
+ min = 0,
+ // current maximum
+ max = 0,
+ // current displayed time span
+ duration = 1000,
+ // line colors to display
+ colors = Object.keys(domainObjects);
+
+ // Get minimum value, ensure there's some room
+ function minimum() {
+ return (min >= max) ? (max - 1) : min;
+ }
+
+ // Get maximum value, ensure there's some room
+ function maximum() {
+ return (min >= max) ? (min + 1) : max;
+ }
+
+ // Update minimum and maximum values
+ function updateMinMax() {
+ // Find the minimum among plot lines
+ min = extrema.map(function (ex) {
+ return ex.min;
+ }).reduce(function (a, b) {
+ return Math.min(a, b);
+ }, Number.POSITIVE_INFINITY);
+
+ // Do the same for the maximum
+ max = extrema.map(function (ex) {
+ return ex.max;
+ }).reduce(function (a, b) {
+ return Math.max(a, b);
+ }, Number.NEGATIVE_INFINITY);
+
+ // Ensure the infinities don't survive
+ min = min === Number.POSITIVE_INFINITY ? max : min;
+ min = min === Number.NEGATIVE_INFINITY ? 0 : min;
+ max = max === Number.NEGATIVE_INFINITY ? min : max;
+ }
+
+ // Change contents of the drawing object (to trigger redraw)
+ function updateDrawingObject() {
+ // Update drawing object to include non-empty lines
+ drawingObject.lines = lines.filter(function (line) {
+ return line.points > 1;
+ });
+
+ // Update drawing bounds to fit data
+ drawingObject.origin[1] = minimum();
+ drawingObject.dimensions[1] = maximum() - minimum();
+ }
+
+ // Update a specific line, by index
+ function updateLine(graph, index) {
+ var buffer = renderer.render(graph),
+ line = lines[index],
+ ex = extrema[index],
+ i;
+
+ // Track minimum/maximum; note we skip x values
+ for (i = 1; i < buffer.length; i += 2) {
+ ex.min = Math.min(buffer[i], ex.min);
+ ex.max = Math.max(buffer[i], ex.max);
+ }
+
+ // Update line in drawing object
+ line.buffer = buffer;
+ line.points = graph.getPointCount();
+ line.color = renderer.decode(colors[index]);
+
+ // Update the graph's total min/max
+ if (line.points > 0) {
+ updateMinMax();
+ }
+
+ // Update the drawing object (used to draw the graph)
+ updateDrawingObject();
+ }
+
+ // Request initialization for a line's contents
+ function populateLine(color, index) {
+ var domainObject = domainObjects[color],
+ graphPromise = domainObject.useCapability('graph');
+
+ if (graphPromise) {
+ graphPromise.then(function (g) {
+ if (g[key]) {
+ updateLine(g[key], index);
+ }
+ });
+ }
+ }
+
+ // Create empty lines
+ lines = colors.map(function () {
+ // Sentinel value to exclude these lines
+ return { points: 0 };
+ });
+
+ // Specify initial min/max state per-line
+ extrema = colors.map(function () {
+ return {
+ min: Number.POSITIVE_INFINITY,
+ max: Number.NEGATIVE_INFINITY
+ };
+ });
+
+ // Start creating lines for all swimlanes
+ colors.forEach(populateLine);
+
+ return {
+ /**
+ * Get the minimum resource value that appears in this graph.
+ * @returns {number} the minimum value
+ */
+ minimum: minimum,
+ /**
+ * Get the maximum resource value that appears in this graph.
+ * @returns {number} the maximum value
+ */
+ maximum: maximum,
+ /**
+ * Set the displayed origin and duration, in milliseconds.
+ * @param {number} [value] value to set, if setting
+ */
+ setBounds: function (offset, duration) {
+ // We don't update in-place, because we need the change
+ // to trigger a watch in mct-chart.
+ drawingObject.origin = [ offset, drawingObject.origin[1] ];
+ drawingObject.dimensions = [ duration, drawingObject.dimensions[1] ];
+ },
+ /**
+ * Redraw lines in this graph.
+ */
+ refresh: function () {
+ colors.forEach(populateLine);
+ },
+ // Expose key, drawing object directly for use in templates
+ key: key,
+ drawingObject: drawingObject
+ };
+
+ }
+
+ return TimelineGraph;
+ }
+);
\ No newline at end of file
diff --git a/platform/features/timeline/src/controllers/graph/TimelineGraphPopulator.js b/platform/features/timeline/src/controllers/graph/TimelineGraphPopulator.js
new file mode 100644
index 0000000000..019f128525
--- /dev/null
+++ b/platform/features/timeline/src/controllers/graph/TimelineGraphPopulator.js
@@ -0,0 +1,157 @@
+/*****************************************************************************
+ * Open MCT Web, Copyright (c) 2009-2015, United States Government
+ * as represented by the Administrator of the National Aeronautics and Space
+ * Administration. All rights reserved.
+ *
+ * Open MCT Web is licensed under the Apache License, Version 2.0 (the
+ * "License"); you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ * http://www.apache.org/licenses/LICENSE-2.0.
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
+ * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
+ * License for the specific language governing permissions and limitations
+ * under the License.
+ *
+ * Open MCT Web includes source code licensed under additional open source
+ * licenses. See the Open Source Licenses file (LICENSES.md) included with
+ * this source code distribution or the Licensing information page available
+ * at runtime from the About dialog for additional information.
+ *****************************************************************************/
+/*global define*/
+define(
+ ['./TimelineGraph', './TimelineGraphRenderer'],
+ function (TimelineGraph, TimelineGraphRenderer) {
+ 'use strict';
+
+ /**
+ * Responsible for determining which resource graphs
+ * to display (based on capabilities exposed by included
+ * domain objects) and allocating data to those different
+ * graphs.
+ * @constructor
+ */
+ function TimelineGraphPopulator($q) {
+ var graphs = [],
+ cachedAssignments = {},
+ renderer = new TimelineGraphRenderer();
+
+ // Compare two domain objects
+ function idsMatch(objA, objB) {
+ return (objA && objA.getId && objA.getId()) ===
+ (objB && objB.getId && objB.getId());
+ }
+
+ // Compare two object sets for equality, to detect
+ // when graph updates are truly needed.
+ function deepEquals(objA, objB) {
+ var keysA, keysB;
+
+ // Check if all keys in both objects match
+ function keysMatch(keys) {
+ return keys.map(function (k) {
+ return deepEquals(objA[k], objB[k]);
+ }).reduce(function (a, b) {
+ return a && b;
+ }, true);
+ }
+
+ // First, check if they're matching domain objects
+ if (typeof (objA && objA.getId) === 'function') {
+ return idsMatch(objA, objB);
+ }
+
+ // Otherwise, assume key-value pairs
+ keysA = Object.keys(objA || {}).sort();
+ keysB = Object.keys(objB || {}).sort();
+
+ return (keysA.length === keysB.length) && keysMatch(keysA);
+ }
+
+ // Populate the graphs for these swimlanes
+ function populate(swimlanes) {
+ // Somewhere to store resource assignments
+ // (as key -> swimlane[])
+ var assignments = {};
+
+ // Look up resources for a domain object
+ function lookupResources(swimlane) {
+ var graphs = swimlane.domainObject.useCapability('graph');
+ function getKeys(obj) {
+ return Object.keys(obj);
+ }
+ return $q.when(graphs ? (graphs.then(getKeys)) : []);
+ }
+
+ // Add all graph assignments appropriate for this swimlane
+ function buildAssignments(swimlane) {
+ // Assign this swimlane to graphs for its resource keys
+ return lookupResources(swimlane).then(function (resources) {
+ resources.forEach(function (key) {
+ assignments[key] = assignments[key] || {};
+ assignments[key][swimlane.color()] =
+ swimlane.domainObject;
+ });
+ });
+ }
+
+ // Make a graph for this resource (after assigning)
+ function makeGraph(key) {
+ return new TimelineGraph(
+ key,
+ assignments[key],
+ renderer
+ );
+ }
+
+ // Used to filter down to swimlanes which need graphs
+ function needsGraph(swimlane) {
+ // Only show swimlanes with graphs & resources to graph
+ return swimlane.graph() &&
+ swimlane.domainObject.hasCapability('graph');
+ }
+
+ // Create graphs according to assignments that have been built
+ function createGraphs() {
+ // Only refresh graphs if our assignments actually changed
+ if (!deepEquals(cachedAssignments, assignments)) {
+ // Make new graphs
+ graphs = Object.keys(assignments).sort().map(makeGraph);
+ // Save resource->color->object assignments
+ cachedAssignments = assignments;
+ } else {
+ // Just refresh the existing graphs
+ graphs.forEach(function (graph) {
+ graph.refresh();
+ });
+ }
+ }
+
+ // Build up list of assignments, then create graphs
+ $q.all(swimlanes.filter(needsGraph).map(buildAssignments))
+ .then(createGraphs);
+ }
+
+ return {
+ /**
+ * Populate (or re-populate) the list of available resource
+ * graphs, based on the provided list of swimlanes (and their
+ * current state.)
+ * @param {TimelineSwimlane[]} swimlanes the swimlanes to use
+ */
+ populate: populate,
+ /**
+ * Get the current list of displayable resource graphs.
+ * @returns {TimelineGraph[]} the resource graphs
+ */
+ get: function () {
+ return graphs;
+ }
+ };
+ }
+
+ return TimelineGraphPopulator;
+
+ }
+);
\ No newline at end of file
diff --git a/platform/features/timeline/src/controllers/graph/TimelineGraphRenderer.js b/platform/features/timeline/src/controllers/graph/TimelineGraphRenderer.js
new file mode 100644
index 0000000000..72add2f315
--- /dev/null
+++ b/platform/features/timeline/src/controllers/graph/TimelineGraphRenderer.js
@@ -0,0 +1,83 @@
+/*****************************************************************************
+ * Open MCT Web, Copyright (c) 2009-2015, United States Government
+ * as represented by the Administrator of the National Aeronautics and Space
+ * Administration. All rights reserved.
+ *
+ * Open MCT Web is licensed under the Apache License, Version 2.0 (the
+ * "License"); you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ * http://www.apache.org/licenses/LICENSE-2.0.
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
+ * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
+ * License for the specific language governing permissions and limitations
+ * under the License.
+ *
+ * Open MCT Web includes source code licensed under additional open source
+ * licenses. See the Open Source Licenses file (LICENSES.md) included with
+ * this source code distribution or the Licensing information page available
+ * at runtime from the About dialog for additional information.
+ *****************************************************************************/
+/*global define,Float32Array*/
+
+define(
+ [],
+ function () {
+ 'use strict';
+
+ /**
+ * Responsible for preparing data for display by
+ * `mct-chart` in a timeline's resource graph.
+ * @constructor
+ */
+ function TimelineGraphRenderer() {
+ return {
+ /**
+ * Render a resource utilization to a Float32Array,
+ * to be passed to WebGL for display.
+ * @param {ResourceGraph} graph the resource utilization
+ * @returns {Float32Array} the rendered buffer
+ */
+ render: function (graph) {
+ var count = graph.getPointCount(),
+ buffer = new Float32Array(count * 2),
+ i;
+
+ // Populate the buffer
+ for (i = 0; i < count; i += 1) {
+ buffer[i * 2] = graph.getDomainValue(i);
+ buffer[i * 2 + 1] = graph.getRangeValue(i);
+ }
+
+ return buffer;
+ },
+ /**
+ * Convert an HTML color (in #-prefixed 6-digit hexadecimal)
+ * to an array of floating point values in a range of 0.0-1.0.
+ * An alpha element is included to facilitate display in an
+ * `mct-chart` (which uses WebGL.)
+ * @param {string} the color
+ * @returns {number[]} the same color, in floating-point format
+ */
+ decode: function (color) {
+ // Check for bad input, default to black if needed
+ color = /^#[A-Fa-f0-9]{6}$/.test(color) ? color : "#000000";
+
+ // Pull out R, G, B hex values
+ return [
+ color.substring(1, 3),
+ color.substring(3, 5),
+ color.substring(5, 7)
+ ].map(function (c) {
+ // Hex -> number
+ return parseInt(c, 16) / 255;
+ }).concat([1]); // Add the alpha channel
+ }
+ };
+ }
+
+ return TimelineGraphRenderer;
+
+ }
+);
\ No newline at end of file
diff --git a/platform/features/timeline/src/controllers/swimlane/TimelineColorAssigner.js b/platform/features/timeline/src/controllers/swimlane/TimelineColorAssigner.js
new file mode 100644
index 0000000000..b3335ec236
--- /dev/null
+++ b/platform/features/timeline/src/controllers/swimlane/TimelineColorAssigner.js
@@ -0,0 +1,122 @@
+/*****************************************************************************
+ * Open MCT Web, Copyright (c) 2009-2015, United States Government
+ * as represented by the Administrator of the National Aeronautics and Space
+ * Administration. All rights reserved.
+ *
+ * Open MCT Web is licensed under the Apache License, Version 2.0 (the
+ * "License"); you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ * http://www.apache.org/licenses/LICENSE-2.0.
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
+ * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
+ * License for the specific language governing permissions and limitations
+ * under the License.
+ *
+ * Open MCT Web includes source code licensed under additional open source
+ * licenses. See the Open Source Licenses file (LICENSES.md) included with
+ * this source code distribution or the Licensing information page available
+ * at runtime from the About dialog for additional information.
+ *****************************************************************************/
+/*global define*/
+define(
+ [],
+ function () {
+ "use strict";
+
+ var COLOR_OPTIONS = [
+ "#20b2aa",
+ "#9acd32",
+ "#ff8c00",
+ "#d2b48c",
+ "#40e0d0",
+ "#4169ff",
+ "#ffd700",
+ "#6a5acd",
+ "#ee82ee",
+ "#cc9966",
+ "#99cccc",
+ "#66cc33",
+ "#ffcc00",
+ "#ff6633",
+ "#cc66ff",
+ "#ff0066",
+ "#ffff00",
+ "#800080",
+ "#00868b",
+ "#008a00",
+ "#ff0000",
+ "#0000ff",
+ "#f5deb3",
+ "#bc8f8f",
+ "#4682b4",
+ "#ffafaf",
+ "#43cd80",
+ "#cdc1c5",
+ "#a0522d",
+ "#6495ed"
+ ],
+ // Fall back to black, as "no more colors available"
+ FALLBACK_COLOR = "#000000";
+
+ /**
+ * Responsible for choosing unique colors for the resource
+ * graph listing of a timeline view. Supports TimelineController.
+ * @constructor
+ * @param colors an object to store color configuration into;
+ * typically, this should be a property from the view's
+ * configuration, but TimelineSwimlane manages this.
+ */
+ function TimelineColorAssigner(colors) {
+ // Find an unused color
+ function freeColor() {
+ // Set of used colors
+ var set = {}, found;
+
+ // Build up a set of used colors
+ Object.keys(colors).forEach(function (id) {
+ set[colors[id]] = true;
+ });
+
+ // Find an unused color
+ COLOR_OPTIONS.forEach(function (c) {
+ found = (!set[c] && !found) ? c : found;
+ });
+
+ // Provide the color
+ return found || FALLBACK_COLOR;
+ }
+
+ return {
+ /**
+ * Get the current color assignment.
+ * @param {string} id the id to which the color is assigned
+ */
+ get: function (id) {
+ return colors[id];
+ },
+ /**
+ * Assign a new color to this id. If no color is specified,
+ * an unused color will be chosen.
+ * @param {string} id the id to which the color is assigned
+ * @param {string} [color] the new color to assign
+ */
+ assign: function (id, color) {
+ colors[id] = typeof color === 'string' ? color : freeColor();
+ },
+ /**
+ * Release the color assignment for this id. That id will
+ * no longer have a color associated with it, and its color
+ * will be free to use in subsequent calls.
+ * @param {string} id the id whose color should be released
+ */
+ release: function (id) {
+ delete colors[id];
+ }
+ };
+ }
+
+ return TimelineColorAssigner;
+ }
+);
\ No newline at end of file
diff --git a/platform/features/timeline/src/controllers/swimlane/TimelineProxy.js b/platform/features/timeline/src/controllers/swimlane/TimelineProxy.js
new file mode 100644
index 0000000000..3174659a63
--- /dev/null
+++ b/platform/features/timeline/src/controllers/swimlane/TimelineProxy.js
@@ -0,0 +1,79 @@
+/*****************************************************************************
+ * Open MCT Web, Copyright (c) 2009-2015, United States Government
+ * as represented by the Administrator of the National Aeronautics and Space
+ * Administration. All rights reserved.
+ *
+ * Open MCT Web is licensed under the Apache License, Version 2.0 (the
+ * "License"); you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ * http://www.apache.org/licenses/LICENSE-2.0.
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
+ * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
+ * License for the specific language governing permissions and limitations
+ * under the License.
+ *
+ * Open MCT Web includes source code licensed under additional open source
+ * licenses. See the Open Source Licenses file (LICENSES.md) included with
+ * this source code distribution or the Licensing information page available
+ * at runtime from the About dialog for additional information.
+ *****************************************************************************/
+/*global define*/
+
+define(
+ [],
+ function () {
+ "use strict";
+
+ /**
+ * Selection proxy for the Timeline view. Implements
+ * behavior associated with the Add button in the
+ * timeline's toolbar.
+ * @constructor
+ */
+ function TimelineProxy(domainObject, selection) {
+ var actionMap = {};
+
+ // Populate available Create actions for this domain object
+ function populateActionMap(domainObject) {
+ var actionCapability = domainObject.getCapability('action'),
+ actions = actionCapability ?
+ actionCapability.getActions('create') : [];
+ actions.forEach(function (action) {
+ actionMap[action.getMetadata().type] = action;
+ });
+ }
+
+ // Populate available actions based on current selection
+ // (defaulting to object-in-view if there is none.)
+ function populateForSelection() {
+ var swimlane = selection && selection.get(),
+ selectedObject = swimlane && swimlane.domainObject;
+ populateActionMap(selectedObject || domainObject);
+ }
+
+ populateActionMap(domainObject);
+
+ return {
+ /**
+ * Add a domain object of the specified type.
+ * @param {string} type the type of domain object to add
+ */
+ add: function (type) {
+ // Update list of create actions; this needs to reflect
+ // the current selection so that Save in defaults
+ // appropriately.
+ populateForSelection();
+
+ // Create an object of that type
+ if (actionMap[type]) {
+ return actionMap[type].perform();
+ }
+ }
+ };
+ }
+
+ return TimelineProxy;
+ }
+);
\ No newline at end of file
diff --git a/platform/features/timeline/src/controllers/swimlane/TimelineSwimlane.js b/platform/features/timeline/src/controllers/swimlane/TimelineSwimlane.js
new file mode 100644
index 0000000000..23f2c49a2e
--- /dev/null
+++ b/platform/features/timeline/src/controllers/swimlane/TimelineSwimlane.js
@@ -0,0 +1,177 @@
+/*****************************************************************************
+ * Open MCT Web, Copyright (c) 2009-2015, United States Government
+ * as represented by the Administrator of the National Aeronautics and Space
+ * Administration. All rights reserved.
+ *
+ * Open MCT Web is licensed under the Apache License, Version 2.0 (the
+ * "License"); you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ * http://www.apache.org/licenses/LICENSE-2.0.
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
+ * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
+ * License for the specific language governing permissions and limitations
+ * under the License.
+ *
+ * Open MCT Web includes source code licensed under additional open source
+ * licenses. See the Open Source Licenses file (LICENSES.md) included with
+ * this source code distribution or the Licensing information page available
+ * at runtime from the About dialog for additional information.
+ *****************************************************************************/
+/*global define*/
+
+define(
+ [],
+ function () {
+ "use strict";
+
+ /**
+ * Describes a swimlane in a timeline view. This will be
+ * used directly from timeline view.
+ *
+ * Only general properties of swimlanes are included here.
+ * Since swimlanes are also directly selected and exposed to the
+ * toolbar, the TimelineSwimlaneDecorator should also be used
+ * to add additional properties to specific swimlanes.
+ *
+ * @constructor
+ * @param {DomainObject} domainObject the represented object
+ * @param {TimelineColorAssigner} assigner color assignment handler
+ * @param configuration the view's configuration object
+ * @param {TimelineSwimlane} parent the parent swim lane (if any)
+ */
+ function TimelineSwimlane(domainObject, assigner, configuration, parent, index) {
+ var id = domainObject.getId(),
+ highlight = false, // Drop highlight (middle)
+ highlightBottom = false, // Drop highlight (lower)
+ idPath = (parent ? parent.idPath : []).concat([domainObject.getId()]),
+ depth = parent ? (parent.depth + 1) : 0,
+ timespan,
+ path = (!parent || !parent.parent) ? "" : parent.path +
+ //(parent.path.length > 0 ? " / " : "") +
+ parent.domainObject.getModel().name +
+ " > ";
+
+ // Look up timespan for this object
+ domainObject.useCapability('timespan').then(function (t) {
+ timespan = t;
+ });
+
+ return {
+ /**
+ * Check if this swimlane is currently visible. (That is,
+ * check to see if its parents are expanded.)
+ * @returns {boolean} true if it is visible
+ */
+ visible: function () {
+ return !parent || (parent.expanded && parent.visible());
+ },
+ /**
+ * Show the Edit Properties dialog.
+ */
+ properties: function () {
+ return domainObject.getCapability("action").perform("properties");
+ },
+ /**
+ * Toggle inclusion of this swimlane's represented object in
+ * the resource graph area.
+ */
+ toggleGraph: function () {
+ configuration.graph = configuration.graph || {};
+ configuration.graph[id] = !configuration.graph[id];
+ // Assign or release legend color
+ assigner[configuration.graph[id] ? 'assign' : 'release'](id);
+ },
+ /**
+ * Get (or set, if an argument is provided) the flag which
+ * determines if the object in this swimlane is included in
+ * the set of active resource graphs.
+ * @param {boolean} [value] the state to set (if setting)
+ * @returns {boolean} true if included; otherwise false
+ */
+ graph: function (value) {
+ // Set if an argument was provided
+ if (arguments.length > 0) {
+ configuration.graph = configuration.graph || {};
+ configuration.graph[id] = !!value;
+ // Assign or release the legend color
+ assigner[value ? 'assign' : 'release'](id);
+ }
+ // Provide the current state
+ return (configuration.graph || {})[id];
+ },
+ /**
+ * Get (or set, if an argument is provided) the color
+ * associated with this swimlane when its contents are
+ * included in the set of active resource graphs.
+ * @param {string} [value] the color to set (if setting)
+ * @returns {string} the color for resource graphing
+ */
+ color: function (value) {
+ // Set if an argument was provided
+ if (arguments.length > 0) {
+ // Defer to the color assigner
+ assigner.assign(id, value);
+ }
+ // Provide the current value
+ return assigner.get(id);
+ },
+ /**
+ * Get (or set, if an argument is provided) the drag
+ * highlight state for this swimlane. True means the body
+ * of the swimlane should be highlighted for drop into.
+ */
+ highlight: function (value) {
+ // Set if an argument was provided
+ if (arguments.length > 0) {
+ highlight = value;
+ }
+ // Provide current value
+ return highlight;
+ },
+ /**
+ * Get (or set, if an argument is provided) the drag
+ * highlight state for this swimlane. True means the body
+ * of the swimlane should be highlighted for drop after.
+ */
+ highlightBottom: function (value) {
+ // Set if an argument was provided
+ if (arguments.length > 0) {
+ highlightBottom = value;
+ }
+ // Provide current value
+ return highlightBottom;
+ },
+ /**
+ * Check if a swimlane exceeds the bounds of its parent.
+ * @returns {boolean} true if there is a bounds violation
+ */
+ exceeded: function () {
+ var parentTimespan = parent && parent.timespan();
+ return timespan && parentTimespan &&
+ (timespan.getStart() < parentTimespan.getStart() ||
+ timespan.getEnd() > parentTimespan.getEnd());
+ },
+ /**
+ * Get the timespan associated with this swimlane
+ */
+ timespan: function () {
+ return timespan;
+ },
+ // Expose domain object, expansion state, indentation depth
+ domainObject: domainObject,
+ expanded: true,
+ depth: depth,
+ path: path,
+ id: id,
+ idPath: idPath,
+ parent: parent,
+ index: index,
+ children: [] // Populated by populator
+ };
+ }
+
+ return TimelineSwimlane;
+ }
+);
\ No newline at end of file
diff --git a/platform/features/timeline/src/controllers/swimlane/TimelineSwimlaneDecorator.js b/platform/features/timeline/src/controllers/swimlane/TimelineSwimlaneDecorator.js
new file mode 100644
index 0000000000..2aafdb1720
--- /dev/null
+++ b/platform/features/timeline/src/controllers/swimlane/TimelineSwimlaneDecorator.js
@@ -0,0 +1,114 @@
+/*****************************************************************************
+ * Open MCT Web, Copyright (c) 2009-2015, United States Government
+ * as represented by the Administrator of the National Aeronautics and Space
+ * Administration. All rights reserved.
+ *
+ * Open MCT Web is licensed under the Apache License, Version 2.0 (the
+ * "License"); you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ * http://www.apache.org/licenses/LICENSE-2.0.
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
+ * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
+ * License for the specific language governing permissions and limitations
+ * under the License.
+ *
+ * Open MCT Web includes source code licensed under additional open source
+ * licenses. See the Open Source Licenses file (LICENSES.md) included with
+ * this source code distribution or the Licensing information page available
+ * at runtime from the About dialog for additional information.
+ *****************************************************************************/
+/*global define*/
+
+define(
+ ['./TimelineSwimlaneDropHandler'],
+ function (TimelineSwimlaneDropHandler) {
+ "use strict";
+
+ var ACTIVITY_RELATIONSHIP = "modes";
+
+ /**
+ * Adds optional methods to TimelineSwimlanes, in order
+ * to conditionally make available options in the toolbar.
+ * @constructor
+ */
+ function TimelineSwimlaneDecorator(swimlane, selection) {
+ var domainObject = swimlane && swimlane.domainObject,
+ model = (domainObject && domainObject.getModel()) || {},
+ mutator = domainObject && domainObject.getCapability('mutation'),
+ persister = domainObject && domainObject.getCapability('persistence'),
+ type = domainObject && domainObject.getCapability('type'),
+ dropHandler = new TimelineSwimlaneDropHandler(swimlane);
+
+ // Activity Modes dialog
+ function modes(value) {
+ // Can be used as a setter...
+ if (arguments.length > 0 && Array.isArray(value)) {
+ // Update the relationships
+ mutator.mutate(function (model) {
+ model.relationships = model.relationships || {};
+ model.relationships[ACTIVITY_RELATIONSHIP] = value;
+ }).then(persister.persist);
+ }
+ // ...otherwise, use as a getter
+ return (model.relationships || {})[ACTIVITY_RELATIONSHIP] || [];
+ }
+
+ // Activity Link dialog
+ function link(value) {
+ // Can be used as a setter...
+ if (arguments.length > 0 && (typeof value === 'string') &&
+ value !== model.link) {
+ // Update the link
+ mutator.mutate(function (model) {
+ model.link = value;
+ }).then(persister.persist);
+ }
+ return model.link;
+ }
+
+ // Fire the Remove action
+ function remove() {
+ return domainObject.getCapability("action").perform("remove");
+ }
+
+ // Select the current swimlane
+ function select() {
+ selection.select(swimlane);
+ }
+
+ // Check if the swimlane is selected
+ function selected() {
+ return selection.get() === swimlane;
+ }
+
+ // Activities should have the Activity Modes and Activity Link dialog
+ if (type && type.instanceOf("activity") && mutator && persister) {
+ swimlane.modes = modes;
+ swimlane.link = link;
+ }
+
+ // Everything but the top-level object should have Remove
+ if (swimlane.parent) {
+ swimlane.remove = remove;
+ }
+
+ // We're in edit mode, if a selection is available
+ if (selection) {
+ // Add shorthands to select, and check for selection
+ swimlane.select = select;
+ swimlane.selected = selected;
+ }
+
+ // Expose drop handlers (which needed a reference to the swimlane)
+ swimlane.allowDropIn = dropHandler.allowDropIn;
+ swimlane.allowDropAfter = dropHandler.allowDropAfter;
+ swimlane.drop = dropHandler.drop;
+
+ return swimlane;
+ }
+
+ return TimelineSwimlaneDecorator;
+ }
+);
diff --git a/platform/features/timeline/src/controllers/swimlane/TimelineSwimlaneDropHandler.js b/platform/features/timeline/src/controllers/swimlane/TimelineSwimlaneDropHandler.js
new file mode 100644
index 0000000000..681fd695cf
--- /dev/null
+++ b/platform/features/timeline/src/controllers/swimlane/TimelineSwimlaneDropHandler.js
@@ -0,0 +1,207 @@
+/*****************************************************************************
+ * Open MCT Web, Copyright (c) 2009-2015, United States Government
+ * as represented by the Administrator of the National Aeronautics and Space
+ * Administration. All rights reserved.
+ *
+ * Open MCT Web is licensed under the Apache License, Version 2.0 (the
+ * "License"); you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ * http://www.apache.org/licenses/LICENSE-2.0.
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
+ * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
+ * License for the specific language governing permissions and limitations
+ * under the License.
+ *
+ * Open MCT Web includes source code licensed under additional open source
+ * licenses. See the Open Source Licenses file (LICENSES.md) included with
+ * this source code distribution or the Licensing information page available
+ * at runtime from the About dialog for additional information.
+ *****************************************************************************/
+/*global define*/
+
+define(
+ [],
+ function () {
+ "use strict";
+
+ /**
+ * Handles drop (from drag-and-drop) initiated changes to a swimlane.
+ * @constructor
+ */
+ function TimelineSwimlaneDropHandler(swimlane) {
+ // Utility function; like $q.when, but synchronous (to reduce
+ // performance impact when wrapping synchronous values)
+ function asPromise(value) {
+ return (value && value.then) ? value : {
+ then: function (callback) {
+ return asPromise(callback(value));
+ }
+ };
+ }
+
+ // Check if we are in edit mode
+ function inEditMode() {
+ return swimlane.domainObject.hasCapability("editor");
+ }
+
+ // Boolean and (for reduce below)
+ function or(a, b) {
+ return a || b;
+ }
+
+ // Check if pathA entirely contains pathB
+ function pathContains(swimlane, id) {
+ // Check if id at a specific index matches (for map below)
+ function matches(pathId) {
+ return pathId === id;
+ }
+
+ // Path A contains Path B if it is longer, and all of
+ // B's ids match the ids in A.
+ return swimlane.idPath.map(matches).reduce(or, false);
+ }
+
+ // Check if a swimlane contains a child with the specified id
+ function contains(swimlane, id) {
+ // Check if a child swimlane has a matching domain object id
+ function matches(child) {
+ return child.domainObject.getId() === id;
+ }
+
+ // Find any one child id that matches this id
+ return swimlane.children.map(matches).reduce(or, false);
+ }
+
+ // Remove a domain object from its current location
+ function remove(domainObject) {
+ return domainObject &&
+ domainObject.getCapability('action').perform('remove');
+ }
+
+ // Initiate mutation of a domain object
+ function doMutate(domainObject, mutator) {
+ return asPromise(
+ domainObject.useCapability("mutation", mutator)
+ ).then(function () {
+ // Persist the results of mutation
+ var persistence = domainObject.getCapability("persistence");
+ if (persistence) {
+ // Persist the changes
+ persistence.persist();
+ }
+ });
+ }
+
+ // Check if this swimlane is in a state where a drop-after will
+ // act as a drop-into-at-first position (expanded and non-empty)
+ function expandedForDropInto() {
+ return swimlane.expanded && swimlane.children.length > 0;
+ }
+
+ // Check if the swimlane is ready to accept a drop-into
+ // (instead of drop-after)
+ function isDropInto() {
+ return swimlane.highlight() || expandedForDropInto();
+ }
+
+ // Choose an index for insertion in a domain object's composition
+ function chooseTargetIndex(id, offset, composition) {
+ return Math.max(
+ Math.min(
+ (composition || []).indexOf(id) + offset,
+ (composition || []).length
+ ),
+ 0
+ );
+ }
+
+ // Insert an id into target's composition
+ function insert(id, target, indexOffset) {
+ var myId = swimlane.domainObject.getId();
+ return doMutate(target, function (model) {
+ model.composition.splice(
+ chooseTargetIndex(myId, indexOffset, model.composition),
+ 0,
+ id
+ );
+ });
+ }
+
+ // Check if a compose action is allowed for the object in this
+ // swimlane (we handle the link differently to set the index,
+ // but check for the existence of the action to invole the
+ // relevant policies.)
+ function allowsCompose(swimlane, domainObject) {
+ var actionCapability =
+ swimlane.domainObject.getCapability('action');
+ return actionCapability && actionCapability.getActions({
+ key: 'compose',
+ selectedObject: domainObject
+ }).length > 0;
+ }
+
+ return {
+ /**
+ * Check if a drop-into should be allowed for this swimlane,
+ * for the provided domain object identifier.
+ * @param {string} id identifier for the domain object to be
+ * dropped
+ * @returns {boolean} true if this should be allowed
+ */
+ allowDropIn: function (id, domainObject) {
+ return inEditMode() &&
+ !pathContains(swimlane, id) &&
+ !contains(swimlane, id) &&
+ allowsCompose(swimlane, domainObject);
+ },
+ /**
+ * Check if a drop-after should be allowed for this swimlane,
+ * for the provided domain object identifier.
+ * @param {string} id identifier for the domain object to be
+ * dropped
+ * @returns {boolean} true if this should be allowed
+ */
+ allowDropAfter: function (id, domainObject) {
+ var target = expandedForDropInto() ?
+ swimlane : swimlane.parent;
+ return inEditMode() &&
+ target &&
+ !pathContains(target, id) &&
+ allowsCompose(target, domainObject);
+ },
+ /**
+ * Drop the provided domain object into a timeline. This is
+ * provided as a mandatory id, and an optional domain object
+ * instance; if the latter is provided, it will be removed
+ * from its parent before being added. (This is specifically
+ * to support moving Activity objects around within a Timeline.)
+ * @param {string} id the identifier for the domain object
+ * @param {DomainObject} [domainObject] the object itself
+ */
+ drop: function (id, domainObject) {
+ // Get the desired drop object, and destination index
+ var dropInto = isDropInto(),
+ dropTarget = dropInto ?
+ swimlane.domainObject :
+ swimlane.parent.domainObject,
+ dropIndexOffset = (!dropInto) ? 1 :
+ (swimlane.expanded && swimlane.highlightBottom()) ?
+ Number.NEGATIVE_INFINITY :
+ Number.POSITIVE_INFINITY;
+
+ if (swimlane.highlight() || swimlane.highlightBottom()) {
+ // Remove the domain object from its original location...
+ return asPromise(remove(domainObject)).then(function () {
+ // ...then insert it at its new location.
+ insert(id, dropTarget, dropIndexOffset);
+ });
+ }
+ }
+ };
+ }
+
+ return TimelineSwimlaneDropHandler;
+ }
+);
\ No newline at end of file
diff --git a/platform/features/timeline/src/controllers/swimlane/TimelineSwimlanePopulator.js b/platform/features/timeline/src/controllers/swimlane/TimelineSwimlanePopulator.js
new file mode 100644
index 0000000000..a15d97196f
--- /dev/null
+++ b/platform/features/timeline/src/controllers/swimlane/TimelineSwimlanePopulator.js
@@ -0,0 +1,185 @@
+/*****************************************************************************
+ * Open MCT Web, Copyright (c) 2009-2015, United States Government
+ * as represented by the Administrator of the National Aeronautics and Space
+ * Administration. All rights reserved.
+ *
+ * Open MCT Web is licensed under the Apache License, Version 2.0 (the
+ * "License"); you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ * http://www.apache.org/licenses/LICENSE-2.0.
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
+ * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
+ * License for the specific language governing permissions and limitations
+ * under the License.
+ *
+ * Open MCT Web includes source code licensed under additional open source
+ * licenses. See the Open Source Licenses file (LICENSES.md) included with
+ * this source code distribution or the Licensing information page available
+ * at runtime from the About dialog for additional information.
+ *****************************************************************************/
+/*global define*/
+
+define(
+ [
+ './TimelineSwimlane',
+ './TimelineSwimlaneDecorator',
+ './TimelineColorAssigner',
+ './TimelineProxy'
+ ],
+ function (
+ TimelineSwimlane,
+ TimelineSwimlaneDecorator,
+ TimelineColorAssigner,
+ TimelineProxy
+ ) {
+ 'use strict';
+
+ /**
+ * Populates and maintains a list of swimlanes for a given
+ * timeline view.
+ * @constructor
+ */
+ function TimelineSwimlanePopulator(objectLoader, configuration, selection) {
+ var swimlanes = [],
+ start = Number.POSITIVE_INFINITY,
+ end = Number.NEGATIVE_INFINITY,
+ colors = (configuration.colors || {}),
+ assigner = new TimelineColorAssigner(colors);
+
+ // Track extremes of start/end times
+ function trackStartEnd(timespan) {
+ if (timespan) {
+ start = Math.min(start, timespan.getStart());
+ end = Math.max(end, timespan.getEnd());
+ }
+ }
+
+ // Add domain object (and its subgraph) in as swimlanes
+ function populateSwimlanes(subgraph, parent, index) {
+ var domainObject = subgraph.domainObject,
+ swimlane;
+
+ // For the recursive step
+ function populate(childSubgraph, index) {
+ populateSwimlanes(childSubgraph, swimlane, index);
+ }
+
+ // Make sure we have a valid object instance...
+ if (domainObject) {
+ // Create the new swimlane
+ swimlane = new TimelineSwimlaneDecorator(new TimelineSwimlane(
+ domainObject,
+ assigner,
+ configuration,
+ parent,
+ index || 0
+ ), selection);
+ // Track start & end times of this domain object
+ domainObject.useCapability('timespan').then(trackStartEnd);
+ // Add it to our list
+ swimlanes.push(swimlane);
+ // Fill in parent's children
+ ((parent || {}).children || []).push(swimlane);
+ // Add in children
+ subgraph.composition.forEach(populate);
+ }
+ }
+
+ // Restore a selection
+ function reselect(path, candidates, depth) {
+ // Next ID on the path
+ var next = path[depth || 0];
+
+ // Ensure a default
+ depth = depth || 0;
+
+ // Search through this layer of candidates to see
+ // if they might contain our selection (based on id path)
+ candidates.forEach(function (swimlane) {
+ // Check if we're on the right path...
+ if (swimlane.id === next) {
+ // Do we still have ids to check?
+ if (depth < path.length - 1) {
+ // Yes, so recursively explore that path
+ reselect(path, swimlane.children, depth + 1);
+ } else {
+ // Nope, we found the object to select
+ selection.select(swimlane);
+ }
+ }
+ });
+ }
+
+ // Handle population of swimlanes
+ function recalculateSwimlanes(domainObject) {
+ function populate(subgraph) {
+ // Cache current selection state during refresh
+ var selected = selection && selection.get(),
+ selectedIdPath = selected && selected.idPath;
+
+ // Clear existing swimlanes
+ swimlanes = [];
+
+ // Build new set of swimlanes
+ populateSwimlanes(subgraph);
+
+ // Restore selection, if there was one
+ if (selectedIdPath && swimlanes.length > 0) {
+ reselect(selectedIdPath, [swimlanes[0]]);
+ }
+ }
+
+ // Repopulate swimlanes for this object
+ if (!domainObject) {
+ populate({});
+ } else {
+ objectLoader.load(domainObject, 'timespan').then(populate);
+ }
+
+ // Set the selection proxy as well (for the Add button)
+ if (selection) {
+ selection.proxy(
+ domainObject && new TimelineProxy(domainObject, selection)
+ );
+ }
+ }
+
+ // Ensure colors are exposed in configuration
+ configuration.colors = colors;
+
+ return {
+ /**
+ * Update list of swimlanes to match those reachable from this
+ * object.
+ * @param {DomainObject} the timeline being viewed
+ */
+ populate: recalculateSwimlanes,
+ /**
+ * Get a list of swimlanes for this timeline view.
+ * @returns {TimelineSwimlane[]} current swimlanes
+ */
+ get: function () {
+ return swimlanes;
+ },
+ /**
+ * Get the first timestamp in the set of swimlanes.
+ * @returns {number} first timestamp
+ */
+ start: function () {
+ return start;
+ },
+ /**
+ * Get the last timestamp in the set of swimlanes.
+ * @returns {number} first timestamp
+ */
+ end: function () {
+ return end;
+ }
+ };
+ }
+
+ return TimelineSwimlanePopulator;
+ }
+);
\ No newline at end of file
diff --git a/platform/features/timeline/src/directives/MCTSwimlaneDrag.js b/platform/features/timeline/src/directives/MCTSwimlaneDrag.js
new file mode 100644
index 0000000000..8825cced8e
--- /dev/null
+++ b/platform/features/timeline/src/directives/MCTSwimlaneDrag.js
@@ -0,0 +1,68 @@
+/*****************************************************************************
+ * Open MCT Web, Copyright (c) 2009-2015, United States Government
+ * as represented by the Administrator of the National Aeronautics and Space
+ * Administration. All rights reserved.
+ *
+ * Open MCT Web is licensed under the Apache License, Version 2.0 (the
+ * "License"); you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ * http://www.apache.org/licenses/LICENSE-2.0.
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
+ * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
+ * License for the specific language governing permissions and limitations
+ * under the License.
+ *
+ * Open MCT Web includes source code licensed under additional open source
+ * licenses. See the Open Source Licenses file (LICENSES.md) included with
+ * this source code distribution or the Licensing information page available
+ * at runtime from the About dialog for additional information.
+ *****************************************************************************/
+/*global define*/
+
+define(
+ ['./SwimlaneDragConstants'],
+ function (SwimlaneDragConstants) {
+ "use strict";
+
+ /**
+ * Defines the `mct-swimlane-drag` directive. When a drag is initiated
+ * form an element with this attribute, the swimlane being dragged
+ * (identified by the value of this attribute, as an Angular expression)
+ * will be exported to the `dndService` as part of the active drag-drop
+ * state.
+ * @param {DndService} dndService drag-and-drop service
+ */
+ function MCTSwimlaneDrag(dndService) {
+ function link(scope, element, attrs) {
+ // Look up the swimlane from the provided expression
+ function swimlane() {
+ return scope.$eval(attrs.mctSwimlaneDrag);
+ }
+ // When drag starts, publish via dndService
+ element.on('dragstart', function () {
+ dndService.setData(
+ SwimlaneDragConstants.TIMELINE_SWIMLANE_DRAG_TYPE,
+ swimlane()
+ );
+ });
+ // When drag ends, clear via dndService
+ element.on('dragend', function () {
+ dndService.removeData(
+ SwimlaneDragConstants.TIMELINE_SWIMLANE_DRAG_TYPE
+ );
+ });
+ }
+
+ return {
+ // Applies to attributes
+ restrict: "A",
+ // Link using above function
+ link: link
+ };
+ }
+
+ return MCTSwimlaneDrag;
+ }
+);
diff --git a/platform/features/timeline/src/directives/MCTSwimlaneDrop.js b/platform/features/timeline/src/directives/MCTSwimlaneDrop.js
new file mode 100644
index 0000000000..d64f1a7831
--- /dev/null
+++ b/platform/features/timeline/src/directives/MCTSwimlaneDrop.js
@@ -0,0 +1,127 @@
+/*****************************************************************************
+ * Open MCT Web, Copyright (c) 2009-2015, United States Government
+ * as represented by the Administrator of the National Aeronautics and Space
+ * Administration. All rights reserved.
+ *
+ * Open MCT Web is licensed under the Apache License, Version 2.0 (the
+ * "License"); you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ * http://www.apache.org/licenses/LICENSE-2.0.
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
+ * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
+ * License for the specific language governing permissions and limitations
+ * under the License.
+ *
+ * Open MCT Web includes source code licensed under additional open source
+ * licenses. See the Open Source Licenses file (LICENSES.md) included with
+ * this source code distribution or the Licensing information page available
+ * at runtime from the About dialog for additional information.
+ *****************************************************************************/
+/*global define*/
+
+define(
+ ['./SwimlaneDragConstants'],
+ function (SwimlaneDragConstants) {
+ "use strict";
+
+ /**
+ * Defines the `mct-swimlane-drop` directive. When a drop occurs
+ * on an element with this attribute, the swimlane targeted by the drop
+ * (identified by the value of this attribute, as an Angular expression)
+ * will receive the dropped domain object (at which point it can handle
+ * the drop, typically by inserting/reordering.)
+ * @param {DndService} dndService drag-and-drop service
+ */
+ function MCTSwimlaneDrop(dndService) {
+
+ // Handle dragover events
+ function dragOver(e, element, swimlane) {
+ var event = (e || {}).originalEvent || e,
+ height = element[0].offsetHeight,
+ rect = element[0].getBoundingClientRect(),
+ offset = event.pageY - rect.top,
+ dataTransfer = event.dataTransfer,
+ id = dndService.getData(
+ SwimlaneDragConstants.MCT_DRAG_TYPE
+ ),
+ draggedObject = dndService.getData(
+ SwimlaneDragConstants.MCT_EXTENDED_DRAG_TYPE
+ );
+
+ if (id) {
+ // TODO: Vary this based on modifier keys
+ event.dataTransfer.dropEffect = 'move';
+
+ // Set the swimlane's drop highlight state; top 75% is
+ // for drop-into, bottom 25% is for drop-after.
+ swimlane.highlight(
+ offset < (height * 0.75) &&
+ swimlane.allowDropIn(id, draggedObject)
+ );
+ swimlane.highlightBottom(
+ offset >= (height * 0.75) &&
+ swimlane.allowDropAfter(id, draggedObject)
+ );
+
+ // Indicate that we will accept the drag
+ if (swimlane.highlight() || swimlane.highlightBottom()) {
+ event.preventDefault(); // Required in Chrome?
+ return false;
+ }
+ }
+ }
+
+ // Handle drop events
+ function drop(e, element, swimlane) {
+ var event = (e || {}).originalEvent || e,
+ dataTransfer = event.dataTransfer,
+ id = dataTransfer.getData(
+ SwimlaneDragConstants.MCT_DRAG_TYPE
+ ),
+ draggedSwimlane = dndService.getData(
+ SwimlaneDragConstants.TIMELINE_SWIMLANE_DRAG_TYPE
+ );
+
+ if (id) {
+ // Delegate the drop to the swimlane itself
+ swimlane.drop(id, (draggedSwimlane || {}).domainObject);
+ }
+
+ // Clear the swimlane highlights
+ swimlane.highlight(false);
+ swimlane.highlightBottom(false);
+ }
+
+ function link(scope, element, attrs) {
+ // Lookup swimlane by evaluating this attribute
+ function swimlane() {
+ return scope.$eval(attrs.mctSwimlaneDrop);
+ }
+ // Handle dragover
+ element.on('dragover', function (e) {
+ dragOver(e, element, swimlane());
+ });
+ // Handle drops
+ element.on('drop', function (e) {
+ drop(e, element, swimlane());
+ });
+ // Clear highlights when drag leaves this swimlane
+ element.on('dragleave', function () {
+ swimlane().highlight(false);
+ swimlane().highlightBottom(false);
+ });
+ }
+
+ return {
+ // Applies to attributes
+ restrict: "A",
+ // Link using above function
+ link: link
+ };
+ }
+
+ return MCTSwimlaneDrop;
+ }
+);
diff --git a/platform/features/timeline/src/directives/SwimlaneDragConstants.js b/platform/features/timeline/src/directives/SwimlaneDragConstants.js
new file mode 100644
index 0000000000..33ce4d79ea
--- /dev/null
+++ b/platform/features/timeline/src/directives/SwimlaneDragConstants.js
@@ -0,0 +1,41 @@
+/*****************************************************************************
+ * Open MCT Web, Copyright (c) 2009-2015, United States Government
+ * as represented by the Administrator of the National Aeronautics and Space
+ * Administration. All rights reserved.
+ *
+ * Open MCT Web is licensed under the Apache License, Version 2.0 (the
+ * "License"); you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ * http://www.apache.org/licenses/LICENSE-2.0.
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
+ * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
+ * License for the specific language governing permissions and limitations
+ * under the License.
+ *
+ * Open MCT Web includes source code licensed under additional open source
+ * licenses. See the Open Source Licenses file (LICENSES.md) included with
+ * this source code distribution or the Licensing information page available
+ * at runtime from the About dialog for additional information.
+ *****************************************************************************/
+/*global define*/
+
+define({
+ /**
+ * The string identifier for the data type used for drag-and-drop
+ * composition of domain objects. (e.g. in event.dataTransfer.setData
+ * calls.)
+ */
+ MCT_DRAG_TYPE: 'mct-domain-object-id',
+ /**
+ * The string identifier for the data type used for drag-and-drop
+ * composition of domain objects, by object instance (passed through
+ * the dndService)
+ */
+ MCT_EXTENDED_DRAG_TYPE: 'mct-domain-object',
+ /**
+ * String identifier for swimlanes being dragged.
+ */
+ TIMELINE_SWIMLANE_DRAG_TYPE: 'timeline-swimlane'
+});
diff --git a/platform/features/timeline/src/services/ObjectLoader.js b/platform/features/timeline/src/services/ObjectLoader.js
new file mode 100644
index 0000000000..ce15c721df
--- /dev/null
+++ b/platform/features/timeline/src/services/ObjectLoader.js
@@ -0,0 +1,135 @@
+/*****************************************************************************
+ * Open MCT Web, Copyright (c) 2009-2015, United States Government
+ * as represented by the Administrator of the National Aeronautics and Space
+ * Administration. All rights reserved.
+ *
+ * Open MCT Web is licensed under the Apache License, Version 2.0 (the
+ * "License"); you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ * http://www.apache.org/licenses/LICENSE-2.0.
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
+ * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
+ * License for the specific language governing permissions and limitations
+ * under the License.
+ *
+ * Open MCT Web includes source code licensed under additional open source
+ * licenses. See the Open Source Licenses file (LICENSES.md) included with
+ * this source code distribution or the Licensing information page available
+ * at runtime from the About dialog for additional information.
+ *****************************************************************************/
+/*global define*/
+
+define(
+ [],
+ function () {
+ 'use strict';
+
+ /**
+ * The ObjectLoader is a utility service for loading subgraphs
+ * of the composition hierarchy, starting at a provided object,
+ * and optionally filtering out objects which fail to meet certain
+ * criteria.
+ * @constructor
+ */
+ function ObjectLoader($q) {
+
+ // Build up an object containing id->object pairs
+ // for the subset of the graph that is relevant.
+ function loadSubGraph(domainObject, criterion) {
+ var result = { domainObject: domainObject, composition: [] },
+ visiting = {},
+ filter;
+
+ // Check object existence (for criterion-less filtering)
+ function exists(domainObject) {
+ return !!domainObject;
+ }
+
+ // Check for capability matching criterion
+ function hasCapability(domainObject) {
+ return domainObject && domainObject.hasCapability(criterion);
+ }
+
+ // For the recursive step...
+ function loadSubGraphFor(childObject) {
+ return loadSubGraph(childObject, filter);
+ }
+
+ // Store loaded subgraphs into the result
+ function storeSubgraphs(subgraphs) {
+ result.composition = subgraphs;
+ }
+
+ // Avoid infinite recursion
+ function notVisiting(domainObject) {
+ return !visiting[domainObject.getId()];
+ }
+
+ // Put the composition of this domain object into the result
+ function mapIntoResult(composition) {
+ return $q.all(
+ composition.filter(filter).filter(notVisiting)
+ .map(loadSubGraphFor)
+ ).then(storeSubgraphs);
+ }
+
+ // Used to give the final result after promise chaining
+ function giveResult() {
+ // Stop suppressing recursive visitation
+ visiting[domainObject.getId()] = true;
+ // And return the expecting result value
+ return result;
+ }
+
+ // Load composition for
+ function loadComposition() {
+ // First, record that we're looking at this domain
+ // object to detect cycles and avoid an infinite loop
+ visiting[domainObject.getId()] = true;
+ // Look up the composition, store it to the graph structure
+ return domainObject.useCapability('composition')
+ .then(mapIntoResult)
+ .then(giveResult);
+ }
+
+ // Choose the filter function to use
+ filter = typeof criterion === 'function' ? criterion :
+ (typeof criterion === 'string' ? hasCapability :
+ exists);
+
+ // Load child hierarchy, then provide the flat list
+ return domainObject.hasCapability('composition') ?
+ loadComposition() : $q.when(result);
+ }
+
+ return {
+ /**
+ * Load domain objects contained in the subgraph of the
+ * composition hierarchy which starts at the specified
+ * domain object, optionally pruning out objects (and their
+ * subgraphs) which match a certain criterion.
+ * The result is given as a promise for an object containing
+ * key-value pairs, where keys are domain object identifiers
+ * and values are domain objects in the subgraph.
+ * The criterion may be omitted (in which case no pruning is
+ * done) or specified as a string, in which case it will be
+ * treated as the name of a required capability, or specified
+ * as a function, which should return a truthy/falsy value
+ * when called with a domain object to indicate whether or
+ * not it should be included in the result set.
+ *
+ * @param {DomainObject} domainObject the domain object to
+ * start from
+ * @param {string|Function} [criterion] the criterion used
+ * to prune domain objects
+ * @returns {Promise} a promise for loaded domain objects
+ */
+ load: loadSubGraph
+ };
+ }
+
+ return ObjectLoader;
+ }
+);
\ No newline at end of file
diff --git a/platform/features/timeline/test/TimelineConstantsSpec.js b/platform/features/timeline/test/TimelineConstantsSpec.js
new file mode 100644
index 0000000000..3a0bb0866d
--- /dev/null
+++ b/platform/features/timeline/test/TimelineConstantsSpec.js
@@ -0,0 +1,35 @@
+/*****************************************************************************
+ * Open MCT Web, Copyright (c) 2009-2015, United States Government
+ * as represented by the Administrator of the National Aeronautics and Space
+ * Administration. All rights reserved.
+ *
+ * Open MCT Web is licensed under the Apache License, Version 2.0 (the
+ * "License"); you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ * http://www.apache.org/licenses/LICENSE-2.0.
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
+ * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
+ * License for the specific language governing permissions and limitations
+ * under the License.
+ *
+ * Open MCT Web includes source code licensed under additional open source
+ * licenses. See the Open Source Licenses file (LICENSES.md) included with
+ * this source code distribution or the Licensing information page available
+ * at runtime from the About dialog for additional information.
+ *****************************************************************************/
+/*global define,describe,it,expect,beforeEach,waitsFor,jasmine,window,afterEach*/
+
+define(
+ ['../src/TimelineConstants'],
+ function (TimelineConstants) {
+ "use strict";
+ describe("The set of Timeline constants", function () {
+ it("specifies a handle width", function () {
+ expect(TimelineConstants.HANDLE_WIDTH)
+ .toEqual(jasmine.any(Number));
+ });
+ });
+ }
+);
diff --git a/platform/features/timeline/test/TimelineFormatterSpec.js b/platform/features/timeline/test/TimelineFormatterSpec.js
new file mode 100644
index 0000000000..456e408514
--- /dev/null
+++ b/platform/features/timeline/test/TimelineFormatterSpec.js
@@ -0,0 +1,62 @@
+/*****************************************************************************
+ * Open MCT Web, Copyright (c) 2009-2015, United States Government
+ * as represented by the Administrator of the National Aeronautics and Space
+ * Administration. All rights reserved.
+ *
+ * Open MCT Web is licensed under the Apache License, Version 2.0 (the
+ * "License"); you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ * http://www.apache.org/licenses/LICENSE-2.0.
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
+ * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
+ * License for the specific language governing permissions and limitations
+ * under the License.
+ *
+ * Open MCT Web includes source code licensed under additional open source
+ * licenses. See the Open Source Licenses file (LICENSES.md) included with
+ * this source code distribution or the Licensing information page available
+ * at runtime from the About dialog for additional information.
+ *****************************************************************************/
+/*global define,describe,it,expect,beforeEach,waitsFor,jasmine,window,afterEach*/
+
+define(
+ ['../src/TimelineFormatter'],
+ function (TimelineFormatter) {
+ 'use strict';
+
+ var SECOND = 1000,
+ MINUTE = SECOND * 60,
+ HOUR = MINUTE * 60,
+ DAY = HOUR * 24;
+
+ describe("The timeline formatter", function () {
+ var formatter;
+
+ beforeEach(function () {
+ formatter = new TimelineFormatter();
+ });
+
+ it("formats durations with seconds", function () {
+ expect(formatter.format(SECOND)).toEqual("000 00:00:01.000");
+ });
+
+ it("formats durations with milliseconds", function () {
+ expect(formatter.format(SECOND + 42)).toEqual("000 00:00:01.042");
+ });
+
+ it("formats durations with days", function () {
+ expect(formatter.format(3 * DAY + SECOND)).toEqual("003 00:00:01.000");
+ });
+
+ it("formats durations with hours", function () {
+ expect(formatter.format(DAY + HOUR * 11 + SECOND)).toEqual("001 11:00:01.000");
+ });
+
+ it("formats durations with minutes", function () {
+ expect(formatter.format(HOUR + MINUTE * 21)).toEqual("000 01:21:00.000");
+ });
+ });
+ }
+);
\ No newline at end of file
diff --git a/platform/features/timeline/test/capabilities/ActivityTimespanCapabilitySpec.js b/platform/features/timeline/test/capabilities/ActivityTimespanCapabilitySpec.js
new file mode 100644
index 0000000000..a3feb78c15
--- /dev/null
+++ b/platform/features/timeline/test/capabilities/ActivityTimespanCapabilitySpec.js
@@ -0,0 +1,92 @@
+/*****************************************************************************
+ * Open MCT Web, Copyright (c) 2009-2015, United States Government
+ * as represented by the Administrator of the National Aeronautics and Space
+ * Administration. All rights reserved.
+ *
+ * Open MCT Web is licensed under the Apache License, Version 2.0 (the
+ * "License"); you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ * http://www.apache.org/licenses/LICENSE-2.0.
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
+ * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
+ * License for the specific language governing permissions and limitations
+ * under the License.
+ *
+ * Open MCT Web includes source code licensed under additional open source
+ * licenses. See the Open Source Licenses file (LICENSES.md) included with
+ * this source code distribution or the Licensing information page available
+ * at runtime from the About dialog for additional information.
+ *****************************************************************************/
+/*global define,describe,it,expect,beforeEach,waitsFor,jasmine,window,afterEach*/
+
+define(
+ ['../../src/capabilities/ActivityTimespanCapability'],
+ function (ActivityTimespanCapability) {
+ 'use strict';
+
+ describe("An Activity's timespan capability", function () {
+ var mockQ,
+ mockDomainObject,
+ capability;
+
+ function asPromise(v) {
+ return (v || {}).then ? v : {
+ then: function (callback) {
+ return asPromise(callback(v));
+ }
+ };
+ }
+
+ beforeEach(function () {
+ mockQ = jasmine.createSpyObj('$q', ['when']);
+ mockDomainObject = jasmine.createSpyObj(
+ 'domainObject',
+ [ 'getModel', 'getCapability' ]
+ );
+
+ mockQ.when.andCallFake(asPromise);
+ mockDomainObject.getModel.andReturn({
+ start: {
+ timestamp: 42000,
+ epoch: "TEST"
+ },
+ duration: {
+ timestamp: 12321
+ }
+ });
+
+ capability = new ActivityTimespanCapability(
+ mockQ,
+ mockDomainObject
+ );
+ });
+
+ it("applies only to activity objects", function () {
+ expect(ActivityTimespanCapability.appliesTo({
+ type: 'activity'
+ })).toBeTruthy();
+ expect(ActivityTimespanCapability.appliesTo({
+ type: 'folder'
+ })).toBeFalsy();
+ });
+
+ it("provides timespan based on model", function () {
+ var mockCallback = jasmine.createSpy('callback');
+ capability.invoke().then(mockCallback);
+ // We verify other methods in ActivityTimespanSpec,
+ // so just make sure we got something that looks right.
+ expect(mockCallback).toHaveBeenCalledWith({
+ getStart: jasmine.any(Function),
+ getEnd: jasmine.any(Function),
+ getDuration: jasmine.any(Function),
+ setStart: jasmine.any(Function),
+ setEnd: jasmine.any(Function),
+ setDuration: jasmine.any(Function),
+ getEpoch: jasmine.any(Function)
+ });
+ });
+ });
+ }
+);
diff --git a/platform/features/timeline/test/capabilities/ActivityTimespanSpec.js b/platform/features/timeline/test/capabilities/ActivityTimespanSpec.js
new file mode 100644
index 0000000000..2254fb29dd
--- /dev/null
+++ b/platform/features/timeline/test/capabilities/ActivityTimespanSpec.js
@@ -0,0 +1,101 @@
+/*****************************************************************************
+ * Open MCT Web, Copyright (c) 2009-2015, United States Government
+ * as represented by the Administrator of the National Aeronautics and Space
+ * Administration. All rights reserved.
+ *
+ * Open MCT Web is licensed under the Apache License, Version 2.0 (the
+ * "License"); you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ * http://www.apache.org/licenses/LICENSE-2.0.
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
+ * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
+ * License for the specific language governing permissions and limitations
+ * under the License.
+ *
+ * Open MCT Web includes source code licensed under additional open source
+ * licenses. See the Open Source Licenses file (LICENSES.md) included with
+ * this source code distribution or the Licensing information page available
+ * at runtime from the About dialog for additional information.
+ *****************************************************************************/
+/*global define,describe,it,expect,beforeEach,waitsFor,jasmine,window,afterEach*/
+
+define(
+ ['../../src/capabilities/ActivityTimespan'],
+ function (ActivityTimespan) {
+ 'use strict';
+
+ describe("An Activity's timespan", function () {
+ var testModel,
+ mutatorModel,
+ mockMutation,
+ timespan;
+
+ beforeEach(function () {
+ testModel = {
+ start: {
+ timestamp: 42000,
+ epoch: "TEST"
+ },
+ duration: {
+ timestamp: 12321
+ }
+ };
+ // Provide a cloned model for mutation purposes
+ // It is important to distinguish mutation made to
+ // the model provided via the mutation capability from
+ // changes made to the model directly (the latter is
+ // not intended usage.)
+ mutatorModel = JSON.parse(JSON.stringify(testModel));
+ mockMutation = jasmine.createSpyObj("mutation", ["mutate"]);
+ mockMutation.mutate.andCallFake(function (mutator) {
+ mutator(mutatorModel);
+ });
+ timespan = new ActivityTimespan(testModel, mockMutation);
+ });
+
+ it("provides a start time", function () {
+ expect(timespan.getStart()).toEqual(42000);
+ });
+
+ it("provides an end time", function () {
+ expect(timespan.getEnd()).toEqual(54321);
+ });
+
+ it("provides duration", function () {
+ expect(timespan.getDuration()).toEqual(12321);
+ });
+
+ it("provides an epoch", function () {
+ expect(timespan.getEpoch()).toEqual("TEST");
+ });
+
+ it("sets start time using mutation capability", function () {
+ timespan.setStart(52000);
+ expect(mutatorModel.start.timestamp).toEqual(52000);
+ // Should have also changed duration to preserve end
+ expect(mutatorModel.duration.timestamp).toEqual(2321);
+ // Original model should still be the same
+ expect(testModel.start.timestamp).toEqual(42000);
+ });
+
+ it("sets end time using mutation capability", function () {
+ timespan.setEnd(44000);
+ // Should have also changed duration to preserve end
+ expect(mutatorModel.duration.timestamp).toEqual(2000);
+ // Original model should still be the same
+ expect(testModel.duration.timestamp).toEqual(12321);
+ });
+
+ it("sets duration using mutation capability", function () {
+ timespan.setDuration(8000);
+ // Should have also changed duration to preserve end
+ expect(mutatorModel.duration.timestamp).toEqual(8000);
+ // Original model should still be the same
+ expect(testModel.duration.timestamp).toEqual(12321);
+ });
+
+ });
+ }
+);
\ No newline at end of file
diff --git a/platform/features/timeline/test/capabilities/ActivityUtilizationSpec.js b/platform/features/timeline/test/capabilities/ActivityUtilizationSpec.js
new file mode 100644
index 0000000000..ec453b9953
--- /dev/null
+++ b/platform/features/timeline/test/capabilities/ActivityUtilizationSpec.js
@@ -0,0 +1,41 @@
+/*****************************************************************************
+ * Open MCT Web, Copyright (c) 2009-2015, United States Government
+ * as represented by the Administrator of the National Aeronautics and Space
+ * Administration. All rights reserved.
+ *
+ * Open MCT Web is licensed under the Apache License, Version 2.0 (the
+ * "License"); you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ * http://www.apache.org/licenses/LICENSE-2.0.
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
+ * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
+ * License for the specific language governing permissions and limitations
+ * under the License.
+ *
+ * Open MCT Web includes source code licensed under additional open source
+ * licenses. See the Open Source Licenses file (LICENSES.md) included with
+ * this source code distribution or the Licensing information page available
+ * at runtime from the About dialog for additional information.
+ *****************************************************************************/
+/*global define,describe,it,expect,beforeEach,waitsFor,jasmine,window,afterEach*/
+
+define(
+ ['../../src/capabilities/ActivityUtilization'],
+ function (ActivityUtilization) {
+ 'use strict';
+
+ describe("An Activity's resource utilization", function () {
+
+ // Placeholder; WTD-918 will implement
+ it("has the expected interface", function () {
+ var utilization = new ActivityUtilization();
+ expect(utilization.getPointCount()).toEqual(jasmine.any(Number));
+ expect(utilization.getDomainValue()).toEqual(jasmine.any(Number));
+ expect(utilization.getRangeValue()).toEqual(jasmine.any(Number));
+ });
+
+ });
+ }
+);
\ No newline at end of file
diff --git a/platform/features/timeline/test/capabilities/CostCapabilitySpec.js b/platform/features/timeline/test/capabilities/CostCapabilitySpec.js
new file mode 100644
index 0000000000..ca1db9c74f
--- /dev/null
+++ b/platform/features/timeline/test/capabilities/CostCapabilitySpec.js
@@ -0,0 +1,81 @@
+/*****************************************************************************
+ * Open MCT Web, Copyright (c) 2009-2015, United States Government
+ * as represented by the Administrator of the National Aeronautics and Space
+ * Administration. All rights reserved.
+ *
+ * Open MCT Web is licensed under the Apache License, Version 2.0 (the
+ * "License"); you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ * http://www.apache.org/licenses/LICENSE-2.0.
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
+ * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
+ * License for the specific language governing permissions and limitations
+ * under the License.
+ *
+ * Open MCT Web includes source code licensed under additional open source
+ * licenses. See the Open Source Licenses file (LICENSES.md) included with
+ * this source code distribution or the Licensing information page available
+ * at runtime from the About dialog for additional information.
+ *****************************************************************************/
+/*global define,describe,it,expect,beforeEach,waitsFor,jasmine,window,afterEach*/
+
+define(
+ ['../../src/capabilities/CostCapability'],
+ function (CostCapability) {
+ 'use strict';
+
+ describe("A subsystem mode's cost capability", function () {
+ var testModel,
+ capability;
+
+ beforeEach(function () {
+ var mockDomainObject = jasmine.createSpyObj(
+ 'domainObject',
+ [ 'getModel', 'getId' ]
+ );
+
+ testModel = {
+ resources: {
+ abc: -1,
+ power: 12321,
+ comms: 42
+ }
+ };
+
+ mockDomainObject.getModel.andReturn(testModel);
+
+ capability = new CostCapability(mockDomainObject);
+ });
+
+ it("provides a list of resource types", function () {
+ expect(capability.resources())
+ .toEqual(['abc', 'comms', 'power']);
+ });
+
+ it("provides resource costs", function () {
+ expect(capability.cost('abc')).toEqual(-1);
+ expect(capability.cost('power')).toEqual(12321);
+ expect(capability.cost('comms')).toEqual(42);
+ });
+
+ it("provides all resources in a group", function () {
+ expect(capability.invoke()).toEqual(testModel.resources);
+ });
+
+ it("applies to subsystem modes", function () {
+ expect(CostCapability.appliesTo({
+ type: "mode"
+ })).toBeTruthy();
+ expect(CostCapability.appliesTo({
+ type: "activity"
+ })).toBeFalsy();
+ expect(CostCapability.appliesTo({
+ type: "other"
+ })).toBeFalsy();
+ });
+
+ });
+ }
+);
diff --git a/platform/features/timeline/test/capabilities/CumulativeGraphSpec.js b/platform/features/timeline/test/capabilities/CumulativeGraphSpec.js
new file mode 100644
index 0000000000..3f19161414
--- /dev/null
+++ b/platform/features/timeline/test/capabilities/CumulativeGraphSpec.js
@@ -0,0 +1,88 @@
+/*****************************************************************************
+ * Open MCT Web, Copyright (c) 2009-2015, United States Government
+ * as represented by the Administrator of the National Aeronautics and Space
+ * Administration. All rights reserved.
+ *
+ * Open MCT Web is licensed under the Apache License, Version 2.0 (the
+ * "License"); you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ * http://www.apache.org/licenses/LICENSE-2.0.
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
+ * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
+ * License for the specific language governing permissions and limitations
+ * under the License.
+ *
+ * Open MCT Web includes source code licensed under additional open source
+ * licenses. See the Open Source Licenses file (LICENSES.md) included with
+ * this source code distribution or the Licensing information page available
+ * at runtime from the About dialog for additional information.
+ *****************************************************************************/
+/*global define,describe,it,expect,beforeEach,waitsFor,jasmine,window,afterEach*/
+
+define(
+ ['../../src/capabilities/CumulativeGraph'],
+ function (CumulativeGraph) {
+ 'use strict';
+
+ describe("A cumulative resource graph", function () {
+ var mockGraph,
+ points,
+ graph;
+
+ beforeEach(function () {
+ points = [ 0, 10, -10, -100, 20, 100, 0 ];
+
+ mockGraph = jasmine.createSpyObj(
+ 'graph',
+ [ 'getPointCount', 'getDomainValue', 'getRangeValue' ]
+ );
+
+ mockGraph.getPointCount.andReturn(points.length * 2);
+ mockGraph.getDomainValue.andCallFake(function (i) {
+ return Math.floor(i / 2) * 100 + 25;
+ });
+ mockGraph.getRangeValue.andCallFake(function (i) {
+ return points[Math.floor(i / 2) + i % 2];
+ });
+
+ graph = new CumulativeGraph(
+ mockGraph,
+ 1000,
+ 2000,
+ 1500,
+ 1 / 10
+ );
+ });
+
+ it("accumulates its wrapped instantaneous graph", function () {
+ // Note that range values are percentages
+ expect(graph.getDomainValue(0)).toEqual(0);
+ expect(graph.getRangeValue(0)).toEqual(50); // initial state
+ expect(graph.getDomainValue(1)).toEqual(25);
+ expect(graph.getRangeValue(1)).toEqual(50); // initial state
+ expect(graph.getDomainValue(2)).toEqual(125);
+ expect(graph.getRangeValue(2)).toEqual(60); // +10
+ expect(graph.getDomainValue(3)).toEqual(225);
+ expect(graph.getRangeValue(3)).toEqual(50); // -10
+ expect(graph.getDomainValue(4)).toEqual(275);
+ expect(graph.getRangeValue(4)).toEqual(0); // -100 (hit bottom)
+ expect(graph.getDomainValue(5)).toEqual(325);
+ expect(graph.getRangeValue(5)).toEqual(0); // still at 0...
+ expect(graph.getDomainValue(6)).toEqual(425);
+ expect(graph.getRangeValue(6)).toEqual(20); // +20
+ expect(graph.getDomainValue(7)).toEqual(505);
+ expect(graph.getRangeValue(7)).toEqual(100); // +100
+ expect(graph.getDomainValue(8)).toEqual(525);
+ expect(graph.getRangeValue(8)).toEqual(100); // still full
+ expect(graph.getDomainValue(9)).toEqual(625);
+ expect(graph.getRangeValue(9)).toEqual(100); // still full
+ expect(graph.getPointCount()).toEqual(10);
+ });
+
+
+
+ });
+ }
+);
\ No newline at end of file
diff --git a/platform/features/timeline/test/capabilities/GraphCapabilitySpec.js b/platform/features/timeline/test/capabilities/GraphCapabilitySpec.js
new file mode 100644
index 0000000000..fc880cbd94
--- /dev/null
+++ b/platform/features/timeline/test/capabilities/GraphCapabilitySpec.js
@@ -0,0 +1,119 @@
+/*****************************************************************************
+ * Open MCT Web, Copyright (c) 2009-2015, United States Government
+ * as represented by the Administrator of the National Aeronautics and Space
+ * Administration. All rights reserved.
+ *
+ * Open MCT Web is licensed under the Apache License, Version 2.0 (the
+ * "License"); you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ * http://www.apache.org/licenses/LICENSE-2.0.
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
+ * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
+ * License for the specific language governing permissions and limitations
+ * under the License.
+ *
+ * Open MCT Web includes source code licensed under additional open source
+ * licenses. See the Open Source Licenses file (LICENSES.md) included with
+ * this source code distribution or the Licensing information page available
+ * at runtime from the About dialog for additional information.
+ *****************************************************************************/
+/*global define,describe,it,expect,beforeEach,waitsFor,jasmine,window,afterEach*/
+
+define(
+ ['../../src/capabilities/GraphCapability'],
+ function (GraphCapability) {
+ 'use strict';
+
+ describe("A Timeline's graph capability", function () {
+ var mockQ,
+ mockDomainObject,
+ testModel,
+ capability;
+
+ function asPromise(v) {
+ return (v || {}).then ? v : {
+ then: function (cb) {
+ return asPromise(cb(v));
+ }
+ };
+ }
+
+ beforeEach(function () {
+ mockQ = jasmine.createSpyObj('$q', ['when']);
+ mockDomainObject = jasmine.createSpyObj(
+ 'domainObject',
+ [ 'getId', 'getModel', 'useCapability' ]
+ );
+
+ testModel = {
+ type: "activity",
+ resources: {
+ abc: 100,
+ xyz: 42
+ }
+ };
+
+ mockQ.when.andCallFake(asPromise);
+ mockDomainObject.getModel.andReturn(testModel);
+
+ capability = new GraphCapability(
+ mockQ,
+ mockDomainObject
+ );
+ });
+
+ it("is applicable to timelines", function () {
+ expect(GraphCapability.appliesTo({
+ type: "timeline"
+ })).toBeTruthy();
+ });
+
+ it("is applicable to activities", function () {
+ expect(GraphCapability.appliesTo(testModel))
+ .toBeTruthy();
+ });
+
+ it("is not applicable to other objects", function () {
+ expect(GraphCapability.appliesTo({
+ type: "something"
+ })).toBeFalsy();
+ });
+
+ it("provides one graph per resource type", function () {
+ var mockCallback = jasmine.createSpy('callback');
+
+ mockDomainObject.useCapability.andReturn(asPromise([
+ { key: "abc", start: 0, end: 15 },
+ { key: "abc", start: 0, end: 15 },
+ { key: "def", start: 4, end: 15 },
+ { key: "xyz", start: 0, end: 20 }
+ ]));
+
+ capability.invoke().then(mockCallback);
+
+ expect(mockCallback).toHaveBeenCalledWith({
+ abc: jasmine.any(Object),
+ def: jasmine.any(Object),
+ xyz: jasmine.any(Object)
+ });
+ });
+
+ it("provides a battery graph for timelines with capacity", function () {
+ var mockCallback = jasmine.createSpy('callback');
+ testModel.capacity = 1000;
+ testModel.type = "timeline";
+ mockDomainObject.useCapability.andReturn(asPromise([
+ { key: "power", start: 0, end: 15 }
+ ]));
+ capability.invoke().then(mockCallback);
+ expect(mockCallback).toHaveBeenCalledWith({
+ power: jasmine.any(Object),
+ battery: jasmine.any(Object)
+ });
+ });
+
+ });
+ }
+);
diff --git a/platform/features/timeline/test/capabilities/ResourceGraphSpec.js b/platform/features/timeline/test/capabilities/ResourceGraphSpec.js
new file mode 100644
index 0000000000..16744704ad
--- /dev/null
+++ b/platform/features/timeline/test/capabilities/ResourceGraphSpec.js
@@ -0,0 +1,77 @@
+/*****************************************************************************
+ * Open MCT Web, Copyright (c) 2009-2015, United States Government
+ * as represented by the Administrator of the National Aeronautics and Space
+ * Administration. All rights reserved.
+ *
+ * Open MCT Web is licensed under the Apache License, Version 2.0 (the
+ * "License"); you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ * http://www.apache.org/licenses/LICENSE-2.0.
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
+ * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
+ * License for the specific language governing permissions and limitations
+ * under the License.
+ *
+ * Open MCT Web includes source code licensed under additional open source
+ * licenses. See the Open Source Licenses file (LICENSES.md) included with
+ * this source code distribution or the Licensing information page available
+ * at runtime from the About dialog for additional information.
+ *****************************************************************************/
+/*global define,describe,it,expect,beforeEach,waitsFor,jasmine,window,afterEach*/
+
+define(
+ ['../../src/capabilities/ResourceGraph'],
+ function (ResourceGraph) {
+ 'use strict';
+
+ describe("A resource graph capability", function () {
+
+ // Placeholder; WTD-918 will implement
+ it("has zero points for zero utilization changes", function () {
+ var graph = new ResourceGraph([]);
+ expect(graph.getPointCount()).toEqual(0);
+ });
+
+ it("creates steps based on resource utilizations", function () {
+ var graph = new ResourceGraph([
+ { start: 5, end: 100, value: 42 },
+ { start: 50, end: 120, value: -22 },
+ { start: 15, end: 40, value: 30 },
+ { start: 150, end: 180, value: -10 }
+ ]);
+
+ expect(graph.getPointCount()).toEqual(16);
+
+ // Should get two values at every time stamp, for step-like appearance
+ [ 5, 15, 40, 50, 100, 120, 150, 180].forEach(function (v, i) {
+ expect(graph.getDomainValue(i * 2)).toEqual(v);
+ expect(graph.getDomainValue(i * 2 + 1)).toEqual(v);
+ });
+
+ // Should also repeat values at subsequent indexes, but offset differently,
+ // for horizontal spans between steps
+ [ 0, 42, 72, 42, 20, -22, 0, -10].forEach(function (v, i) {
+ expect(graph.getRangeValue(i * 2)).toEqual(v);
+ // Offset backwards; wrap around end of the series
+ expect(graph.getRangeValue((16 + i * 2 - 1) % 16)).toEqual(v);
+ });
+ });
+
+ it("filters out zero-duration spikes", function () {
+ var graph = new ResourceGraph([
+ { start: 5, end: 100, value: 42 },
+ { start: 100, end: 120, value: -22 },
+ { start: 100, end: 180, value: 30 },
+ { start: 130, end: 180, value: -10 }
+ ]);
+
+ // There are only 5 unique timestamps there, so there should
+ // be 5 steps, for 10 total points
+ expect(graph.getPointCount()).toEqual(10);
+ });
+
+ });
+ }
+);
\ No newline at end of file
diff --git a/platform/features/timeline/test/capabilities/TimelineTimespanCapabilitySpec.js b/platform/features/timeline/test/capabilities/TimelineTimespanCapabilitySpec.js
new file mode 100644
index 0000000000..2de3186889
--- /dev/null
+++ b/platform/features/timeline/test/capabilities/TimelineTimespanCapabilitySpec.js
@@ -0,0 +1,136 @@
+/*****************************************************************************
+ * Open MCT Web, Copyright (c) 2009-2015, United States Government
+ * as represented by the Administrator of the National Aeronautics and Space
+ * Administration. All rights reserved.
+ *
+ * Open MCT Web is licensed under the Apache License, Version 2.0 (the
+ * "License"); you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ * http://www.apache.org/licenses/LICENSE-2.0.
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
+ * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
+ * License for the specific language governing permissions and limitations
+ * under the License.
+ *
+ * Open MCT Web includes source code licensed under additional open source
+ * licenses. See the Open Source Licenses file (LICENSES.md) included with
+ * this source code distribution or the Licensing information page available
+ * at runtime from the About dialog for additional information.
+ *****************************************************************************/
+/*global define,describe,it,expect,beforeEach,waitsFor,jasmine,window,afterEach*/
+
+define(
+ ['../../src/capabilities/TimelineTimespanCapability'],
+ function (TimelineTimespanCapability) {
+ 'use strict';
+
+ describe("A Timeline's timespan capability", function () {
+ var mockQ,
+ mockDomainObject,
+ mockChildA,
+ mockChildB,
+ mockTimespanA,
+ mockTimespanB,
+ capability;
+
+ function asPromise(v) {
+ return (v || {}).then ? v : {
+ then: function (callback) {
+ return asPromise(callback(v));
+ }
+ };
+ }
+
+ beforeEach(function () {
+ mockQ = jasmine.createSpyObj('$q', ['when', 'all']);
+ mockDomainObject = jasmine.createSpyObj(
+ 'domainObject',
+ [ 'getModel', 'getCapability', 'useCapability' ]
+ );
+ mockChildA = jasmine.createSpyObj(
+ 'childA',
+ [ 'getModel', 'useCapability', 'hasCapability' ]
+ );
+ mockChildB = jasmine.createSpyObj(
+ 'childA',
+ [ 'getModel', 'useCapability', 'hasCapability' ]
+ );
+ mockTimespanA = jasmine.createSpyObj(
+ 'timespanA',
+ [ 'getEnd' ]
+ );
+ mockTimespanB = jasmine.createSpyObj(
+ 'timespanB',
+ [ 'getEnd' ]
+ );
+
+ mockQ.when.andCallFake(asPromise);
+ mockQ.all.andCallFake(function (values) {
+ var result = [];
+ function addResult(v) { result.push(v); }
+ function promiseResult(v) { asPromise(v).then(addResult); }
+ values.forEach(promiseResult);
+ return asPromise(result);
+ });
+ mockDomainObject.getModel.andReturn({
+ start: {
+ timestamp: 42000,
+ epoch: "TEST"
+ },
+ duration: {
+ timestamp: 12321
+ }
+ });
+ mockDomainObject.useCapability.andCallFake(function (c) {
+ if (c === 'composition') {
+ return asPromise([ mockChildA, mockChildB ]);
+ }
+ });
+ mockChildA.hasCapability.andReturn(true);
+ mockChildB.hasCapability.andReturn(true);
+ mockChildA.useCapability.andCallFake(function (c) {
+ return c === 'timespan' && mockTimespanA;
+ });
+ mockChildB.useCapability.andCallFake(function (c) {
+ return c === 'timespan' && mockTimespanB;
+ });
+
+ capability = new TimelineTimespanCapability(
+ mockQ,
+ mockDomainObject
+ );
+ });
+
+ it("applies only to timeline objects", function () {
+ expect(TimelineTimespanCapability.appliesTo({
+ type: 'timeline'
+ })).toBeTruthy();
+ expect(TimelineTimespanCapability.appliesTo({
+ type: 'folder'
+ })).toBeFalsy();
+ });
+
+ it("provides timespan based on model", function () {
+ var mockCallback = jasmine.createSpy('callback');
+ capability.invoke().then(mockCallback);
+ // We verify other methods in ActivityTimespanSpec,
+ // so just make sure we got something that looks right.
+ expect(mockCallback).toHaveBeenCalledWith({
+ getStart: jasmine.any(Function),
+ getEnd: jasmine.any(Function),
+ getDuration: jasmine.any(Function),
+ setStart: jasmine.any(Function),
+ setEnd: jasmine.any(Function),
+ setDuration: jasmine.any(Function),
+ getEpoch: jasmine.any(Function)
+ });
+ // Finally, verify that getEnd recurses
+ mockCallback.mostRecentCall.args[0].getEnd();
+ expect(mockTimespanA.getEnd).toHaveBeenCalled();
+ expect(mockTimespanB.getEnd).toHaveBeenCalled();
+ });
+ });
+ }
+);
diff --git a/platform/features/timeline/test/capabilities/TimelineTimespanSpec.js b/platform/features/timeline/test/capabilities/TimelineTimespanSpec.js
new file mode 100644
index 0000000000..41c46c53a7
--- /dev/null
+++ b/platform/features/timeline/test/capabilities/TimelineTimespanSpec.js
@@ -0,0 +1,112 @@
+/*****************************************************************************
+ * Open MCT Web, Copyright (c) 2009-2015, United States Government
+ * as represented by the Administrator of the National Aeronautics and Space
+ * Administration. All rights reserved.
+ *
+ * Open MCT Web is licensed under the Apache License, Version 2.0 (the
+ * "License"); you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ * http://www.apache.org/licenses/LICENSE-2.0.
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
+ * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
+ * License for the specific language governing permissions and limitations
+ * under the License.
+ *
+ * Open MCT Web includes source code licensed under additional open source
+ * licenses. See the Open Source Licenses file (LICENSES.md) included with
+ * this source code distribution or the Licensing information page available
+ * at runtime from the About dialog for additional information.
+ *****************************************************************************/
+/*global define,describe,it,expect,beforeEach,waitsFor,jasmine,window,afterEach*/
+
+define(
+ ['../../src/capabilities/TimelineTimespan'],
+ function (TimelineTimespan) {
+ 'use strict';
+
+ describe("A Timeline's timespan", function () {
+ var testModel,
+ mockTimespans,
+ mockMutation,
+ mutationModel,
+ timespan;
+
+ function makeMockTimespan(end) {
+ var mockTimespan = jasmine.createSpyObj(
+ 'timespan-' + end,
+ ['getEnd']
+ );
+ mockTimespan.getEnd.andReturn(end);
+ return mockTimespan;
+ }
+
+ beforeEach(function () {
+ testModel = {
+ start: {
+ timestamp: 42000,
+ epoch: "TEST"
+ }
+ };
+
+ mutationModel = JSON.parse(JSON.stringify(testModel));
+
+ mockMutation = jasmine.createSpyObj("mutation", ["mutate"]);
+ mockTimespans = [ 44000, 65000, 1100 ].map(makeMockTimespan);
+
+ mockMutation.mutate.andCallFake(function (mutator) {
+ mutator(mutationModel);
+ });
+
+ timespan = new TimelineTimespan(
+ testModel,
+ mockMutation,
+ mockTimespans
+ );
+ });
+
+ it("provides a start time", function () {
+ expect(timespan.getStart()).toEqual(42000);
+ });
+
+ it("provides an end time", function () {
+ expect(timespan.getEnd()).toEqual(65000);
+ });
+
+ it("provides duration", function () {
+ expect(timespan.getDuration()).toEqual(65000 - 42000);
+ });
+
+ it("provides an epoch", function () {
+ expect(timespan.getEpoch()).toEqual("TEST");
+ });
+
+
+ it("sets start time using mutation capability", function () {
+ timespan.setStart(52000);
+ expect(mutationModel.start.timestamp).toEqual(52000);
+ // Original model should still be the same
+ expect(testModel.start.timestamp).toEqual(42000);
+ });
+
+ it("makes no changes with setEnd", function () {
+ // Copy initial state to verify that it doesn't change
+ var initialModel = JSON.parse(JSON.stringify(testModel));
+ timespan.setEnd(123454321);
+ // Neither model should have changed
+ expect(testModel).toEqual(initialModel);
+ expect(mutationModel).toEqual(initialModel);
+ });
+
+ it("makes no changes with setDuration", function () {
+ // Copy initial state to verify that it doesn't change
+ var initialModel = JSON.parse(JSON.stringify(testModel));
+ timespan.setDuration(123454321);
+ // Neither model should have changed
+ expect(testModel).toEqual(initialModel);
+ expect(mutationModel).toEqual(initialModel);
+ });
+ });
+ }
+);
\ No newline at end of file
diff --git a/platform/features/timeline/test/capabilities/TimelineUtilizationSpec.js b/platform/features/timeline/test/capabilities/TimelineUtilizationSpec.js
new file mode 100644
index 0000000000..4282e1ab8d
--- /dev/null
+++ b/platform/features/timeline/test/capabilities/TimelineUtilizationSpec.js
@@ -0,0 +1,41 @@
+/*****************************************************************************
+ * Open MCT Web, Copyright (c) 2009-2015, United States Government
+ * as represented by the Administrator of the National Aeronautics and Space
+ * Administration. All rights reserved.
+ *
+ * Open MCT Web is licensed under the Apache License, Version 2.0 (the
+ * "License"); you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ * http://www.apache.org/licenses/LICENSE-2.0.
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
+ * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
+ * License for the specific language governing permissions and limitations
+ * under the License.
+ *
+ * Open MCT Web includes source code licensed under additional open source
+ * licenses. See the Open Source Licenses file (LICENSES.md) included with
+ * this source code distribution or the Licensing information page available
+ * at runtime from the About dialog for additional information.
+ *****************************************************************************/
+/*global define,describe,it,expect,beforeEach,waitsFor,jasmine,window,afterEach*/
+
+define(
+ ['../../src/capabilities/TimelineUtilization'],
+ function (TimelineUtilization) {
+ 'use strict';
+
+ describe("A Timeline's resource utilization", function () {
+
+ // Placeholder; WTD-918 will implement
+ it("has the expected interface", function () {
+ var utilization = new TimelineUtilization();
+ expect(utilization.getPointCount()).toEqual(jasmine.any(Number));
+ expect(utilization.getDomainValue()).toEqual(jasmine.any(Number));
+ expect(utilization.getRangeValue()).toEqual(jasmine.any(Number));
+ });
+
+ });
+ }
+);
\ No newline at end of file
diff --git a/platform/features/timeline/test/capabilities/UtilizationCapabilitySpec.js b/platform/features/timeline/test/capabilities/UtilizationCapabilitySpec.js
new file mode 100644
index 0000000000..eda895bb60
--- /dev/null
+++ b/platform/features/timeline/test/capabilities/UtilizationCapabilitySpec.js
@@ -0,0 +1,216 @@
+/*****************************************************************************
+ * Open MCT Web, Copyright (c) 2009-2015, United States Government
+ * as represented by the Administrator of the National Aeronautics and Space
+ * Administration. All rights reserved.
+ *
+ * Open MCT Web is licensed under the Apache License, Version 2.0 (the
+ * "License"); you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ * http://www.apache.org/licenses/LICENSE-2.0.
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
+ * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
+ * License for the specific language governing permissions and limitations
+ * under the License.
+ *
+ * Open MCT Web includes source code licensed under additional open source
+ * licenses. See the Open Source Licenses file (LICENSES.md) included with
+ * this source code distribution or the Licensing information page available
+ * at runtime from the About dialog for additional information.
+ *****************************************************************************/
+/*global define,describe,it,expect,beforeEach,waitsFor,jasmine,window,afterEach*/
+
+define(
+ ['../../src/capabilities/UtilizationCapability'],
+ function (UtilizationCapability) {
+ 'use strict';
+
+ describe("A Timeline's utilization capability", function () {
+ var mockQ,
+ mockDomainObject,
+ testModel,
+ testCapabilities,
+ mockRelationship,
+ mockComposition,
+ mockCallback,
+ capability;
+
+ function asPromise(v) {
+ return (v || {}).then ? v : {
+ then: function (callback) {
+ return asPromise(callback(v));
+ },
+ testValue: v
+ };
+ }
+
+ function allPromises(promises) {
+ return asPromise(promises.map(function (p) {
+ return (p || {}).then ? p.testValue : p;
+ }));
+ }
+
+ // Utility function for making domain objects with utilization
+ // and/or cost capabilities
+ function fakeDomainObject(resources, start, end, costs) {
+ return {
+ getCapability: function (c) {
+ return ((c === 'utilization') && {
+ // Utilization capability
+ resources: function () {
+ return asPromise(resources);
+ },
+ invoke: function () {
+ return asPromise(resources.map(function (k) {
+ return { key: k, start: start, end: end };
+ }));
+ }
+ }) || ((c === 'cost') && {
+ // Cost capability
+ resources: function () {
+ return Object.keys(costs).sort();
+ },
+ cost: function (c) {
+ return costs[c];
+ }
+ });
+ },
+ useCapability: function (c) {
+ return this.getCapability(c).invoke();
+ }
+ };
+ }
+
+ beforeEach(function () {
+ mockQ = jasmine.createSpyObj('$q', ['when', 'all']);
+ mockDomainObject = jasmine.createSpyObj(
+ 'domainObject',
+ [ 'getId', 'getModel', 'getCapability', 'useCapability' ]
+ );
+ mockRelationship = jasmine.createSpyObj(
+ 'relationship',
+ [ 'getRelatedObjects' ]
+ );
+ mockComposition = jasmine.createSpyObj(
+ 'composition',
+ [ 'invoke' ]
+ );
+ mockCallback = jasmine.createSpy('callback');
+
+ testModel = {
+ type: "activity",
+ resources: {
+ abc: 100,
+ xyz: 42
+ }
+ };
+ testCapabilities = {
+ composition: mockComposition,
+ relationship: mockRelationship
+ };
+
+ mockQ.when.andCallFake(asPromise);
+ mockQ.all.andCallFake(allPromises);
+ mockDomainObject.getModel.andReturn(testModel);
+ mockDomainObject.getCapability.andCallFake(function (c) {
+ return testCapabilities[c];
+ });
+ mockDomainObject.useCapability.andCallFake(function (c) {
+ return testCapabilities[c] && testCapabilities[c].invoke();
+ });
+
+ capability = new UtilizationCapability(
+ mockQ,
+ mockDomainObject
+ );
+ });
+
+ it("is applicable to timelines", function () {
+ expect(UtilizationCapability.appliesTo({
+ type: "timeline"
+ })).toBeTruthy();
+ });
+
+ it("is applicable to activities", function () {
+ expect(UtilizationCapability.appliesTo(testModel))
+ .toBeTruthy();
+ });
+
+ it("is not applicable to other objects", function () {
+ expect(UtilizationCapability.appliesTo({
+ type: "something"
+ })).toBeFalsy();
+ });
+
+ it("accumulates resources from composition", function () {
+ mockComposition.invoke.andReturn(asPromise([
+ fakeDomainObject(['abc', 'def']),
+ fakeDomainObject(['def', 'xyz']),
+ fakeDomainObject(['abc', 'xyz'])
+ ]));
+
+ capability.resources().then(mockCallback);
+
+ expect(mockCallback)
+ .toHaveBeenCalledWith(['abc', 'def', 'xyz']);
+ });
+
+ it("accumulates utilizations from composition", function () {
+ mockComposition.invoke.andReturn(asPromise([
+ fakeDomainObject(['abc', 'def'], 10, 100),
+ fakeDomainObject(['def', 'xyz'], 50, 90)
+ ]));
+
+ capability.invoke().then(mockCallback);
+
+ expect(mockCallback).toHaveBeenCalledWith([
+ { key: 'abc', start: 10, end: 100 },
+ { key: 'def', start: 10, end: 100 },
+ { key: 'def', start: 50, end: 90 },
+ { key: 'xyz', start: 50, end: 90 }
+ ]);
+ });
+
+ it("provides intrinsic utilization from related objects", function () {
+ var mockTimespan = jasmine.createSpyObj(
+ 'timespan',
+ ['getStart', 'getEnd', 'getEpoch']
+ ),
+ mockTimespanCapability = jasmine.createSpyObj(
+ 'timespanCapability',
+ ['invoke']
+ );
+ mockComposition.invoke.andReturn(asPromise([]));
+ mockRelationship.getRelatedObjects.andReturn(asPromise([
+ fakeDomainObject([], 0, 0, { abc: 5, xyz: 15 })
+ ]));
+
+ testCapabilities.timespan = mockTimespanCapability;
+ mockTimespanCapability.invoke.andReturn(asPromise(mockTimespan));
+ mockTimespan.getStart.andReturn(42);
+ mockTimespan.getEnd.andReturn(12321);
+ mockTimespan.getEpoch.andReturn("TEST");
+
+ capability.invoke().then(mockCallback);
+
+ expect(mockCallback).toHaveBeenCalledWith([
+ { key: 'abc', start: 42, end: 12321, value: 5, epoch: "TEST" },
+ { key: 'xyz', start: 42, end: 12321, value: 15, epoch: "TEST" }
+ ]);
+ });
+
+ it("provides resource keys from related objects", function () {
+ mockComposition.invoke.andReturn(asPromise([]));
+ mockRelationship.getRelatedObjects.andReturn(asPromise([
+ fakeDomainObject([], 0, 0, { abc: 5, xyz: 15 })
+ ]));
+
+ capability.resources().then(mockCallback);
+
+ expect(mockCallback).toHaveBeenCalledWith(['abc', 'xyz']);
+ });
+
+ });
+ }
+);
diff --git a/platform/features/timeline/test/controllers/ActivityModeValuesControllerSpec.js b/platform/features/timeline/test/controllers/ActivityModeValuesControllerSpec.js
new file mode 100644
index 0000000000..b731c414ac
--- /dev/null
+++ b/platform/features/timeline/test/controllers/ActivityModeValuesControllerSpec.js
@@ -0,0 +1,53 @@
+/*****************************************************************************
+ * Open MCT Web, Copyright (c) 2009-2015, United States Government
+ * as represented by the Administrator of the National Aeronautics and Space
+ * Administration. All rights reserved.
+ *
+ * Open MCT Web is licensed under the Apache License, Version 2.0 (the
+ * "License"); you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ * http://www.apache.org/licenses/LICENSE-2.0.
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
+ * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
+ * License for the specific language governing permissions and limitations
+ * under the License.
+ *
+ * Open MCT Web includes source code licensed under additional open source
+ * licenses. See the Open Source Licenses file (LICENSES.md) included with
+ * this source code distribution or the Licensing information page available
+ * at runtime from the About dialog for additional information.
+ *****************************************************************************/
+/*global define,describe,it,expect,beforeEach,waitsFor,jasmine,window,afterEach*/
+
+define(
+ ['../../src/controllers/ActivityModeValuesController'],
+ function (ActivityModeValuesController) {
+ 'use strict';
+
+ describe("An Activity Mode's Values view controller", function () {
+ var testResources,
+ controller;
+
+ beforeEach(function () {
+ testResources = [
+ { key: 'abc', name: "Some name" },
+ { key: 'def', name: "Test type", units: "Test units" },
+ { key: 'xyz', name: "Something else" }
+ ];
+ controller = new ActivityModeValuesController(testResources);
+ });
+
+ it("exposes resource metadata by key", function () {
+ expect(controller.metadata('abc')).toEqual(testResources[0]);
+ expect(controller.metadata('def')).toEqual(testResources[1]);
+ expect(controller.metadata('xyz')).toEqual(testResources[2]);
+ });
+
+ it("exposes no metadata for unknown keys", function () {
+ expect(controller.metadata('???')).toBeUndefined();
+ });
+ });
+ }
+);
\ No newline at end of file
diff --git a/platform/features/timeline/test/controllers/TimelineControllerSpec.js b/platform/features/timeline/test/controllers/TimelineControllerSpec.js
new file mode 100644
index 0000000000..0f20165ae8
--- /dev/null
+++ b/platform/features/timeline/test/controllers/TimelineControllerSpec.js
@@ -0,0 +1,250 @@
+/*****************************************************************************
+ * Open MCT Web, Copyright (c) 2009-2015, United States Government
+ * as represented by the Administrator of the National Aeronautics and Space
+ * Administration. All rights reserved.
+ *
+ * Open MCT Web is licensed under the Apache License, Version 2.0 (the
+ * "License"); you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ * http://www.apache.org/licenses/LICENSE-2.0.
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
+ * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
+ * License for the specific language governing permissions and limitations
+ * under the License.
+ *
+ * Open MCT Web includes source code licensed under additional open source
+ * licenses. See the Open Source Licenses file (LICENSES.md) included with
+ * this source code distribution or the Licensing information page available
+ * at runtime from the About dialog for additional information.
+ *****************************************************************************/
+/*global define,describe,it,expect,beforeEach,waitsFor,jasmine,window,afterEach*/
+
+define(
+ ['../../src/controllers/TimelineController'],
+ function (TimelineController) {
+ 'use strict';
+
+ var DOMAIN_OBJECT_METHODS = [
+ 'getModel',
+ 'getId',
+ 'useCapability',
+ 'hasCapability',
+ 'getCapability'
+ ];
+
+ describe("The timeline controller", function () {
+ var mockScope,
+ mockQ,
+ mockLoader,
+ mockDomainObject,
+ mockSpan,
+ testModels,
+ testConfiguration,
+ controller;
+
+ function asPromise(v) {
+ return (v || {}).then ? v : {
+ then: function (callback) {
+ return asPromise(callback(v));
+ },
+ testValue: v
+ };
+ }
+
+ function allPromises(promises) {
+ return asPromise(promises.map(function (p) {
+ return (p || {}).then ? p.testValue : p;
+ }));
+ }
+
+ function subgraph(domainObject, objects) {
+ function lookupSubgraph(id) {
+ return subgraph(objects[id], objects);
+ }
+ return {
+ domainObject: domainObject,
+ composition: (domainObject.getModel().composition || [])
+ .map(lookupSubgraph)
+ };
+ }
+
+
+ beforeEach(function () {
+ var mockA, mockB, mockUtilization, mockPromise, mockGraph, testCapabilities;
+
+ function getCapability(c) {
+ return testCapabilities[c];
+ }
+
+ function useCapability(c) {
+ return c === 'timespan' ? asPromise(mockSpan) :
+ c === 'graph' ? asPromise({ abc: mockGraph, xyz: mockGraph }) :
+ undefined;
+ }
+
+ testModels = {
+ a: { modified: 40, composition: ['b'] },
+ b: { modified: 2 }
+ };
+
+ testConfiguration = {};
+
+ mockQ = jasmine.createSpyObj('$q', ['when', 'all']);
+ mockA = jasmine.createSpyObj('a', DOMAIN_OBJECT_METHODS);
+ mockB = jasmine.createSpyObj('b', DOMAIN_OBJECT_METHODS);
+ mockSpan = jasmine.createSpyObj('span', ['getStart', 'getEnd']);
+ mockUtilization = jasmine.createSpyObj('utilization', ['resources', 'utilization']);
+ mockGraph = jasmine.createSpyObj('graph', ['getPointCount']);
+ mockPromise = jasmine.createSpyObj('promise', ['then']);
+
+ mockScope = jasmine.createSpyObj(
+ "$scope",
+ [ '$watch', '$on' ]
+ );
+ mockLoader = jasmine.createSpyObj('objectLoader', ['load']);
+ mockDomainObject = mockA;
+
+ mockScope.domainObject = mockDomainObject;
+ mockScope.configuration = testConfiguration;
+ mockQ.when.andCallFake(asPromise);
+ mockQ.all.andCallFake(allPromises);
+ mockA.getId.andReturn('a');
+ mockA.getModel.andReturn(testModels.a);
+ mockB.getId.andReturn('b');
+ mockB.getModel.andReturn(testModels.b);
+ mockA.useCapability.andCallFake(useCapability);
+ mockB.useCapability.andCallFake(useCapability);
+ mockA.hasCapability.andReturn(true);
+ mockB.hasCapability.andReturn(true);
+ mockA.getCapability.andCallFake(getCapability);
+ mockB.getCapability.andCallFake(getCapability);
+ mockSpan.getStart.andReturn(42);
+ mockSpan.getEnd.andReturn(12321);
+ mockUtilization.resources.andReturn(['abc', 'xyz']);
+ mockUtilization.utilization.andReturn(mockPromise);
+ mockLoader.load.andCallFake(function () {
+ return asPromise(subgraph(mockA, {
+ a: mockA,
+ b: mockB
+ }));
+ });
+
+ testCapabilities = {
+ "utilization": mockUtilization
+ };
+
+ controller = new TimelineController(mockScope, mockQ, mockLoader, 0);
+ });
+
+ it("exposes scroll state tracker in scope", function () {
+ expect(mockScope.scroll.x).toEqual(0);
+ expect(mockScope.scroll.y).toEqual(0);
+ });
+
+ it("repopulates when modifications are made", function () {
+ var fnWatchCall,
+ strWatchCall;
+
+ // Find the $watch that was given a function
+ mockScope.$watch.calls.forEach(function (call) {
+ if (typeof call.args[0] === 'function') {
+ // white-box: we know the first call is
+ // the one we're looking for
+ fnWatchCall = fnWatchCall || call;
+ } else if (typeof call.args[0] === 'string') {
+ strWatchCall = strWatchCall || call;
+ }
+ });
+
+ // Make sure string watch was for domainObject
+ expect(strWatchCall.args[0]).toEqual('domainObject');
+ // Initially populate
+ strWatchCall.args[1](mockDomainObject);
+
+ // There should be to swimlanes
+ expect(controller.swimlanes().length).toEqual(2);
+
+ // Watch should be for sum of modified flags...
+ expect(fnWatchCall.args[0]()).toEqual(42);
+
+ // Remove the child, then fire the watch
+ testModels.a.composition = [];
+ fnWatchCall.args[1]();
+
+ // Swimlanes should have updated
+ expect(controller.swimlanes().length).toEqual(1);
+ });
+
+ it("repopulates graphs when graph choices change", function () {
+ var tmp;
+
+ // Note that this test is brittle; it relies upon the
+ // order of $watch calls in TimelineController.
+
+ // Initially populate
+ mockScope.$watch.calls[0].args[1](mockDomainObject);
+
+ // Verify precondition - no graphs
+ expect(controller.graphs().length).toEqual(0);
+
+ // Execute the watch function for graph state
+ tmp = mockScope.$watch.calls[2].args[0]();
+
+ // Change graph state
+ testConfiguration.graph = { a: true, b: true };
+
+ // Verify that this would have triggered a watch
+ expect(mockScope.$watch.calls[2].args[0]())
+ .not.toEqual(tmp);
+
+ // Run the function the watch would have triggered
+ mockScope.$watch.calls[2].args[1]();
+
+ // Should have some graphs now
+ expect(controller.graphs().length).toEqual(2);
+
+ });
+
+ it("reports full scrollable width using zoom controller", function () {
+ var mockZoom = jasmine.createSpyObj('zoom', ['toPixels', 'duration']);
+ mockZoom.toPixels.andReturn(54321);
+ mockZoom.duration.andReturn(12345);
+
+ // Initially populate
+ mockScope.$watch.calls[0].args[1](mockDomainObject);
+
+ expect(controller.width(mockZoom)).toEqual(54321);
+ // Verify interactions; we took zoom's duration for our start/end,
+ // and converted it to pixels.
+ // First, check that we used the start/end (from above)
+ expect(mockZoom.duration).toHaveBeenCalledWith(12321 - 42);
+ // Next, verify that the result was passed to toPixels
+ expect(mockZoom.toPixels).toHaveBeenCalledWith(12345);
+ });
+
+ it("provides drag handles", function () {
+ // TimelineDragPopulator et al are tested for these,
+ // so just verify that handles are indeed exposed.
+ expect(controller.handles()).toEqual(jasmine.any(Array));
+ });
+
+ it("refreshes graphs on request", function () {
+ var mockGraph = jasmine.createSpyObj('graph', ['refresh']);
+
+ // Sneak a mock graph into the graph populator...
+ // This is whiteboxy and will have to change if
+ // GraphPopulator changes
+ controller.graphs().push(mockGraph);
+
+ // Refresh
+ controller.refresh();
+
+ // Should have refreshed the graph
+ expect(mockGraph.refresh).toHaveBeenCalled();
+ });
+ });
+
+ }
+);
\ No newline at end of file
diff --git a/platform/features/timeline/test/controllers/TimelineDateTimeControllerSpec.js b/platform/features/timeline/test/controllers/TimelineDateTimeControllerSpec.js
new file mode 100644
index 0000000000..d39b47caf5
--- /dev/null
+++ b/platform/features/timeline/test/controllers/TimelineDateTimeControllerSpec.js
@@ -0,0 +1,78 @@
+/*****************************************************************************
+ * Open MCT Web, Copyright (c) 2009-2015, United States Government
+ * as represented by the Administrator of the National Aeronautics and Space
+ * Administration. All rights reserved.
+ *
+ * Open MCT Web is licensed under the Apache License, Version 2.0 (the
+ * "License"); you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ * http://www.apache.org/licenses/LICENSE-2.0.
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
+ * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
+ * License for the specific language governing permissions and limitations
+ * under the License.
+ *
+ * Open MCT Web includes source code licensed under additional open source
+ * licenses. See the Open Source Licenses file (LICENSES.md) included with
+ * this source code distribution or the Licensing information page available
+ * at runtime from the About dialog for additional information.
+ *****************************************************************************/
+
+/*global define,describe,it,expect,beforeEach,waitsFor,jasmine,window,afterEach*/
+
+define(
+ ["../../src/controllers/TimelineDateTimeController"],
+ function (TimelineDateTimeController) {
+ "use strict";
+
+ describe("The date-time controller for timeline creation", function () {
+ var mockScope,
+ controller;
+
+ beforeEach(function () {
+ mockScope = jasmine.createSpyObj('$scope', ['$watchCollection']);
+ mockScope.field = 'testField';
+ mockScope.ngModel = { testField: { timestamp: 0, epoch: "SET" } };
+ controller = new TimelineDateTimeController(mockScope);
+ });
+
+
+ // Verify two-way binding support
+ it("updates model on changes to entry fields", function () {
+ // Make sure we're looking at the right watch
+ expect(mockScope.$watchCollection.calls[0].args[0])
+ .toEqual("datetime");
+ mockScope.$watchCollection.calls[0].args[1]({
+ days: 4,
+ hours: 12,
+ minutes: 30,
+ seconds: 11
+ });
+ expect(mockScope.ngModel.testField.timestamp).toEqual(
+ ((((((4 * 24) + 12) * 60) + 30) * 60) + 11) * 1000
+ );
+ });
+
+ it("updates form when model changes", function () {
+ // Make sure we're looking at the right watch
+ expect(mockScope.$watchCollection.calls[1].args[0])
+ .toEqual(jasmine.any(Function));
+ // ...and that it's really looking at the field in ngModel
+ expect(mockScope.$watchCollection.calls[1].args[0]())
+ .toBe(mockScope.ngModel.testField);
+ mockScope.$watchCollection.calls[1].args[1]({
+ timestamp: ((((((4 * 24) + 12) * 60) + 30) * 60) + 11) * 1000
+ });
+ expect(mockScope.datetime).toEqual({
+ days: 4,
+ hours: 12,
+ minutes: 30,
+ seconds: 11
+ });
+ });
+
+ });
+ }
+);
diff --git a/platform/features/timeline/test/controllers/TimelineGanttControllerSpec.js b/platform/features/timeline/test/controllers/TimelineGanttControllerSpec.js
new file mode 100644
index 0000000000..c573d02ed5
--- /dev/null
+++ b/platform/features/timeline/test/controllers/TimelineGanttControllerSpec.js
@@ -0,0 +1,101 @@
+/*****************************************************************************
+ * Open MCT Web, Copyright (c) 2009-2015, United States Government
+ * as represented by the Administrator of the National Aeronautics and Space
+ * Administration. All rights reserved.
+ *
+ * Open MCT Web is licensed under the Apache License, Version 2.0 (the
+ * "License"); you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ * http://www.apache.org/licenses/LICENSE-2.0.
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
+ * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
+ * License for the specific language governing permissions and limitations
+ * under the License.
+ *
+ * Open MCT Web includes source code licensed under additional open source
+ * licenses. See the Open Source Licenses file (LICENSES.md) included with
+ * this source code distribution or the Licensing information page available
+ * at runtime from the About dialog for additional information.
+ *****************************************************************************/
+/*global define,describe,it,expect,beforeEach,waitsFor,jasmine,window,afterEach*/
+
+define(
+ ['../../src/controllers/TimelineGanttController'],
+ function (TimelineGanttController) {
+ "use strict";
+
+ var TEST_MAX_OFFSCREEN = 50;
+
+ describe("The timeline Gantt bar controller", function () {
+ var mockTimespan,
+ testScroll,
+ mockToPixels,
+ controller;
+
+ // Shorthands for passing these arguments to the controller
+ function width() {
+ return controller.width(
+ mockTimespan,
+ testScroll,
+ mockToPixels
+ );
+ }
+ function left() {
+ return controller.left(
+ mockTimespan,
+ testScroll,
+ mockToPixels
+ );
+ }
+
+
+ beforeEach(function () {
+ mockTimespan = jasmine.createSpyObj(
+ 'timespan',
+ ['getStart', 'getEnd', 'getDuration']
+ );
+ testScroll = { x: 0, width: 2000 };
+ mockToPixels = jasmine.createSpy('toPixels');
+
+ mockTimespan.getStart.andReturn(100);
+ mockTimespan.getDuration.andReturn(50);
+ mockTimespan.getEnd.andReturn(150);
+
+ mockToPixels.andCallFake(function (t) { return t * 10; });
+
+ controller = new TimelineGanttController(TEST_MAX_OFFSCREEN);
+ });
+
+ it("positions start and end points correctly on-screen", function () {
+ // Test's initial conditions are nominal, so should have
+ // the same return value as mockToPixels
+ expect(left()).toEqual(1000);
+ expect(width()).toEqual(500);
+ });
+
+ it("prevents excessive off screen values to the left", function () {
+ testScroll.x = 1200;
+ expect(left()).toEqual(1150);
+ expect(width()).toEqual(350); // ...such that right edge is 1500
+ });
+
+ it("prevents excessive off screen values to the right", function () {
+ testScroll.width = 1200;
+ expect(left()).toEqual(1000);
+ expect(width()).toEqual(250); // ...such that right edge is 1250
+ });
+
+ it("prevents excessive off screen values on both edges", function () {
+ testScroll.x = 1100;
+ testScroll.width = 200; // Visible right edge is now 1300
+ expect(left()).toEqual(1050);
+ expect(width()).toEqual(300); // ...such that right edge is 1350
+ });
+
+
+
+ });
+ }
+);
\ No newline at end of file
diff --git a/platform/features/timeline/test/controllers/TimelineGraphControllerSpec.js b/platform/features/timeline/test/controllers/TimelineGraphControllerSpec.js
new file mode 100644
index 0000000000..9d74832cdb
--- /dev/null
+++ b/platform/features/timeline/test/controllers/TimelineGraphControllerSpec.js
@@ -0,0 +1,89 @@
+/*****************************************************************************
+ * Open MCT Web, Copyright (c) 2009-2015, United States Government
+ * as represented by the Administrator of the National Aeronautics and Space
+ * Administration. All rights reserved.
+ *
+ * Open MCT Web is licensed under the Apache License, Version 2.0 (the
+ * "License"); you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ * http://www.apache.org/licenses/LICENSE-2.0.
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
+ * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
+ * License for the specific language governing permissions and limitations
+ * under the License.
+ *
+ * Open MCT Web includes source code licensed under additional open source
+ * licenses. See the Open Source Licenses file (LICENSES.md) included with
+ * this source code distribution or the Licensing information page available
+ * at runtime from the About dialog for additional information.
+ *****************************************************************************/
+/*global define,describe,it,expect,beforeEach,waitsFor,jasmine,window,afterEach*/
+
+define(
+ ['../../src/controllers/TimelineGraphController'],
+ function (TimelineGraphController) {
+ 'use strict';
+
+ describe("The Timeline graph controller", function () {
+ var mockScope,
+ testResources,
+ controller;
+
+ beforeEach(function () {
+ mockScope = jasmine.createSpyObj(
+ '$scope',
+ [ '$watchCollection' ]
+ );
+ testResources = [
+ { key: 'abc', name: "Some name" },
+ { key: 'def', name: "Test type", units: "Test units" },
+ { key: 'xyz', name: "Something else" }
+ ];
+ controller = new TimelineGraphController(
+ mockScope,
+ testResources
+ );
+ });
+
+ it("watches for parameter changes", function () {
+ expect(mockScope.$watchCollection).toHaveBeenCalledWith(
+ 'parameters',
+ jasmine.any(Function)
+ );
+ });
+
+ it("updates graphs when parameters change", function () {
+ var mockGraphA = jasmine.createSpyObj('graph-a', ['setBounds']),
+ mockGraphB = jasmine.createSpyObj('graph-b', ['setBounds']);
+
+ // Supply new parameters
+ mockScope.$watchCollection.mostRecentCall.args[1]({
+ graphs: [ mockGraphA, mockGraphB ],
+ origin: 9,
+ duration: 144
+ });
+
+ // Graphs should have both been updated
+ expect(mockGraphA.setBounds).toHaveBeenCalledWith(9, 144);
+ expect(mockGraphB.setBounds).toHaveBeenCalledWith(9, 144);
+ });
+
+ it("provides labels for graphs", function () {
+ var mockGraph = jasmine.createSpyObj('graph', ['minimum', 'maximum']);
+
+ mockGraph.minimum.andReturn(12.3412121);
+ mockGraph.maximum.andReturn(88.7555555);
+ mockGraph.key = "def";
+
+ expect(controller.label(mockGraph)).toEqual({
+ title: "Test type (Test units)",
+ low: "12.341",
+ middle: "50.548",
+ high: "88.756"
+ });
+ });
+ });
+ }
+);
diff --git a/platform/features/timeline/test/controllers/TimelineTableControllerSpec.js b/platform/features/timeline/test/controllers/TimelineTableControllerSpec.js
new file mode 100644
index 0000000000..f97390bdb4
--- /dev/null
+++ b/platform/features/timeline/test/controllers/TimelineTableControllerSpec.js
@@ -0,0 +1,52 @@
+/*****************************************************************************
+ * Open MCT Web, Copyright (c) 2009-2015, United States Government
+ * as represented by the Administrator of the National Aeronautics and Space
+ * Administration. All rights reserved.
+ *
+ * Open MCT Web is licensed under the Apache License, Version 2.0 (the
+ * "License"); you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ * http://www.apache.org/licenses/LICENSE-2.0.
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
+ * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
+ * License for the specific language governing permissions and limitations
+ * under the License.
+ *
+ * Open MCT Web includes source code licensed under additional open source
+ * licenses. See the Open Source Licenses file (LICENSES.md) included with
+ * this source code distribution or the Licensing information page available
+ * at runtime from the About dialog for additional information.
+ *****************************************************************************/
+/*global define,describe,it,expect,beforeEach,waitsFor,jasmine,window,afterEach*/
+
+define(
+ [
+ '../../src/controllers/TimelineTableController',
+ '../../src/TimelineFormatter'
+ ],
+ function (TimelineTableController, TimelineFormatter) {
+ "use strict";
+
+ describe("The timeline table controller", function () {
+ var formatter, controller;
+
+ beforeEach(function () {
+ controller = new TimelineTableController();
+ formatter = new TimelineFormatter();
+ });
+
+ // This controller's job is just to expose the formatter
+ // in scope, so simply verify that the two agree.
+ it("formats durations", function () {
+ [ 0, 100, 4123, 93600, 748801230012].forEach(function (n) {
+ expect(controller.niceTime(n))
+ .toEqual(formatter.format(n));
+ });
+ });
+
+
+ });
+ }
+);
diff --git a/platform/features/timeline/test/controllers/TimelineTickControllerSpec.js b/platform/features/timeline/test/controllers/TimelineTickControllerSpec.js
new file mode 100644
index 0000000000..ca3063a0d9
--- /dev/null
+++ b/platform/features/timeline/test/controllers/TimelineTickControllerSpec.js
@@ -0,0 +1,88 @@
+/*****************************************************************************
+ * Open MCT Web, Copyright (c) 2009-2015, United States Government
+ * as represented by the Administrator of the National Aeronautics and Space
+ * Administration. All rights reserved.
+ *
+ * Open MCT Web is licensed under the Apache License, Version 2.0 (the
+ * "License"); you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ * http://www.apache.org/licenses/LICENSE-2.0.
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
+ * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
+ * License for the specific language governing permissions and limitations
+ * under the License.
+ *
+ * Open MCT Web includes source code licensed under additional open source
+ * licenses. See the Open Source Licenses file (LICENSES.md) included with
+ * this source code distribution or the Licensing information page available
+ * at runtime from the About dialog for additional information.
+ *****************************************************************************/
+/*global define,describe,it,expect,beforeEach,waitsFor,jasmine,window,afterEach*/
+
+define(
+ ['../../src/controllers/TimelineTickController', '../../src/TimelineFormatter'],
+ function (TimelineTickController, TimelineFormatter) {
+ 'use strict';
+
+ var BILLION = 1000000000,
+ FORMATTER = new TimelineFormatter();
+
+ describe("The timeline tick controller", function () {
+ var mockToMillis,
+ controller;
+
+ function expectedTick(pixelValue) {
+ return {
+ left: pixelValue,
+ text: FORMATTER.format(pixelValue * 2 + BILLION)
+ };
+ }
+
+ beforeEach(function () {
+ mockToMillis = jasmine.createSpy('toMillis');
+ mockToMillis.andCallFake(function (v) {
+ return v * 2 + BILLION;
+ });
+ controller = new TimelineTickController();
+ });
+
+ it("exposes tick marks within a requested pixel span", function () {
+ // Simple case
+ expect(controller.labels(8000, 300, 100, mockToMillis))
+ .toEqual([8000, 8100, 8200, 8300].map(expectedTick));
+
+ // Slightly more complicated case
+ expect(controller.labels(7480, 4500, 1000, mockToMillis))
+ .toEqual([7000, 8000, 9000, 10000, 11000, 12000].map(expectedTick));
+ });
+
+ it("does not rebuild arrays for same inputs", function () {
+ var firstValue = controller.labels(800, 300, 100, mockToMillis);
+
+ expect(controller.labels(800, 300, 100, mockToMillis))
+ .toEqual(firstValue);
+
+ expect(controller.labels(800, 300, 100, mockToMillis))
+ .toBe(firstValue);
+ });
+
+ it("does rebuild arrays when zoom changes", function () {
+ var firstValue = controller.labels(800, 300, 100, mockToMillis);
+
+ mockToMillis.andCallFake(function (v) {
+ return BILLION * 2 + v;
+ });
+
+ expect(controller.labels(800, 300, 100, mockToMillis))
+ .not.toEqual(firstValue);
+
+ expect(controller.labels(800, 300, 100, mockToMillis))
+ .not.toBe(firstValue);
+ });
+
+ });
+
+ }
+);
\ No newline at end of file
diff --git a/platform/features/timeline/test/controllers/TimelineZoomControllerSpec.js b/platform/features/timeline/test/controllers/TimelineZoomControllerSpec.js
new file mode 100644
index 0000000000..1c86d75599
--- /dev/null
+++ b/platform/features/timeline/test/controllers/TimelineZoomControllerSpec.js
@@ -0,0 +1,101 @@
+/*****************************************************************************
+ * Open MCT Web, Copyright (c) 2009-2015, United States Government
+ * as represented by the Administrator of the National Aeronautics and Space
+ * Administration. All rights reserved.
+ *
+ * Open MCT Web is licensed under the Apache License, Version 2.0 (the
+ * "License"); you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ * http://www.apache.org/licenses/LICENSE-2.0.
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
+ * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
+ * License for the specific language governing permissions and limitations
+ * under the License.
+ *
+ * Open MCT Web includes source code licensed under additional open source
+ * licenses. See the Open Source Licenses file (LICENSES.md) included with
+ * this source code distribution or the Licensing information page available
+ * at runtime from the About dialog for additional information.
+ *****************************************************************************/
+/*global define,describe,it,expect,beforeEach,waitsFor,jasmine,window,afterEach*/
+
+
+define(
+ ['../../src/controllers/TimelineZoomController'],
+ function (TimelineZoomController) {
+ 'use strict';
+
+ describe("The timeline zoom state controller", function () {
+ var testConfiguration,
+ mockScope,
+ controller;
+
+ beforeEach(function () {
+ testConfiguration = {
+ levels: [
+ 1000,
+ 2000,
+ 3500
+ ],
+ width: 12321
+ };
+ mockScope = jasmine.createSpyObj("$scope", ['$watch']);
+ mockScope.commit = jasmine.createSpy('commit');
+ controller = new TimelineZoomController(
+ mockScope,
+ testConfiguration
+ );
+ });
+
+ it("starts off at a middle zoom level", function () {
+ expect(controller.zoom()).toEqual(2000);
+ });
+
+ it("allows duration to be changed", function () {
+ var initial = controller.duration();
+ controller.duration(initial * 3.33);
+ expect(controller.duration() > initial).toBeTruthy();
+ });
+
+ it("handles time-to-pixel conversions", function () {
+ var zoomLevel = controller.zoom();
+ expect(controller.toPixels(zoomLevel)).toEqual(12321);
+ expect(controller.toPixels(zoomLevel * 2)).toEqual(24642);
+ });
+
+ it("handles pixel-to-time conversions", function () {
+ var zoomLevel = controller.zoom();
+ expect(controller.toMillis(12321)).toEqual(zoomLevel);
+ expect(controller.toMillis(24642)).toEqual(zoomLevel * 2);
+ });
+
+ it("allows zoom to be changed", function () {
+ controller.zoom(1);
+ expect(controller.zoom()).toEqual(3500);
+ });
+
+ it("does not normally persist zoom changes", function () {
+ controller.zoom(1);
+ expect(mockScope.commit).not.toHaveBeenCalled();
+ });
+
+ it("persists zoom changes in Edit mode", function () {
+ mockScope.domainObject = jasmine.createSpyObj(
+ 'domainObject',
+ ['hasCapability']
+ );
+ mockScope.domainObject.hasCapability.andCallFake(function (c) {
+ return c === 'editor';
+ });
+ controller.zoom(1);
+ expect(mockScope.commit).toHaveBeenCalled();
+ expect(mockScope.configuration.zoomLevel)
+ .toEqual(jasmine.any(Number));
+ });
+
+ });
+
+ }
+);
diff --git a/platform/features/timeline/test/controllers/drag/TimelineDragHandleFactorySpec.js b/platform/features/timeline/test/controllers/drag/TimelineDragHandleFactorySpec.js
new file mode 100644
index 0000000000..8528b3c68d
--- /dev/null
+++ b/platform/features/timeline/test/controllers/drag/TimelineDragHandleFactorySpec.js
@@ -0,0 +1,87 @@
+/*****************************************************************************
+ * Open MCT Web, Copyright (c) 2009-2015, United States Government
+ * as represented by the Administrator of the National Aeronautics and Space
+ * Administration. All rights reserved.
+ *
+ * Open MCT Web is licensed under the Apache License, Version 2.0 (the
+ * "License"); you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ * http://www.apache.org/licenses/LICENSE-2.0.
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
+ * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
+ * License for the specific language governing permissions and limitations
+ * under the License.
+ *
+ * Open MCT Web includes source code licensed under additional open source
+ * licenses. See the Open Source Licenses file (LICENSES.md) included with
+ * this source code distribution or the Licensing information page available
+ * at runtime from the About dialog for additional information.
+ *****************************************************************************/
+/*global define,describe,it,expect,beforeEach,waitsFor,jasmine,window,afterEach*/
+
+define(
+ ['../../../src/controllers/drag/TimelineDragHandleFactory'],
+ function (TimelineDragHandleFactory) {
+ 'use strict';
+
+ describe("A Timeline drag handle factory", function () {
+ var mockDragHandler,
+ mockSnapHandler,
+ mockDomainObject,
+ mockType,
+ testType,
+ factory;
+
+ beforeEach(function () {
+ mockDragHandler = jasmine.createSpyObj(
+ 'dragHandler',
+ [ 'start' ]
+ );
+ mockSnapHandler = jasmine.createSpyObj(
+ 'snapHandler',
+ [ 'snap' ]
+ );
+ mockDomainObject = jasmine.createSpyObj(
+ 'domainObject',
+ [ 'getCapability', 'getId' ]
+ );
+ mockType = jasmine.createSpyObj(
+ 'type',
+ [ 'instanceOf' ]
+ );
+
+ mockDomainObject.getId.andReturn('test-id');
+ mockDomainObject.getCapability.andReturn(mockType);
+ mockType.instanceOf.andCallFake(function (t) {
+ return t === testType;
+ });
+
+ factory = new TimelineDragHandleFactory(
+ mockDragHandler,
+ mockSnapHandler
+ );
+ });
+
+ it("inspects an object's type capability", function () {
+ factory.handles(mockDomainObject);
+ expect(mockDomainObject.getCapability)
+ .toHaveBeenCalledWith('type');
+ });
+
+ it("provides three handles for activities", function () {
+ testType = "activity";
+ expect(factory.handles(mockDomainObject).length)
+ .toEqual(3);
+ });
+
+ it("provides two handles for timelines", function () {
+ testType = "timeline";
+ expect(factory.handles(mockDomainObject).length)
+ .toEqual(2);
+ });
+
+ });
+ }
+);
diff --git a/platform/features/timeline/test/controllers/drag/TimelineDragHandlerSpec.js b/platform/features/timeline/test/controllers/drag/TimelineDragHandlerSpec.js
new file mode 100644
index 0000000000..b6ddd364c0
--- /dev/null
+++ b/platform/features/timeline/test/controllers/drag/TimelineDragHandlerSpec.js
@@ -0,0 +1,230 @@
+/*****************************************************************************
+ * Open MCT Web, Copyright (c) 2009-2015, United States Government
+ * as represented by the Administrator of the National Aeronautics and Space
+ * Administration. All rights reserved.
+ *
+ * Open MCT Web is licensed under the Apache License, Version 2.0 (the
+ * "License"); you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ * http://www.apache.org/licenses/LICENSE-2.0.
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
+ * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
+ * License for the specific language governing permissions and limitations
+ * under the License.
+ *
+ * Open MCT Web includes source code licensed under additional open source
+ * licenses. See the Open Source Licenses file (LICENSES.md) included with
+ * this source code distribution or the Licensing information page available
+ * at runtime from the About dialog for additional information.
+ *****************************************************************************/
+/*global define,describe,it,expect,beforeEach,waitsFor,jasmine,window,afterEach*/
+
+define(
+ ['../../../src/controllers/drag/TimelineDragHandler'],
+ function (TimelineDragHandler) {
+ 'use strict';
+
+ describe("A Timeline drag handler", function () {
+ var mockLoader,
+ mockSelection,
+ testConfiguration,
+ mockDomainObject,
+ mockDomainObjects,
+ mockTimespans,
+ mockMutations,
+ mockPersists,
+ mockCallback,
+ handler;
+
+ function asPromise(value) {
+ return (value || {}).then ? value : {
+ then: function (callback) {
+ return asPromise(callback(value));
+ }
+ };
+ }
+
+ function subgraph(domainObject, objects) {
+ function lookupSubgraph(id) {
+ return subgraph(objects[id], objects);
+ }
+ return {
+ domainObject: domainObject,
+ composition: (domainObject.getModel().composition || [])
+ .map(lookupSubgraph)
+ };
+ }
+
+ function makeMockDomainObject(id, composition) {
+ var mockDomainObject = jasmine.createSpyObj(
+ 'domainObject-' + id,
+ ['getId', 'getModel', 'getCapability', 'useCapability']
+ );
+
+ mockDomainObject.getId.andReturn(id);
+ mockDomainObject.getModel.andReturn({ composition: composition });
+ mockDomainObject.useCapability.andReturn(asPromise(mockTimespans[id]));
+ mockDomainObject.getCapability.andCallFake(function (c) {
+ return {
+ persistence: mockPersists[id],
+ mutation: mockMutations[id]
+ }[c];
+ });
+
+ return mockDomainObject;
+ }
+
+ beforeEach(function () {
+ mockTimespans = {};
+ mockPersists = {};
+ mockMutations = {};
+ ['a', 'b', 'c', 'd', 'e', 'f'].forEach(function (id, index) {
+ mockTimespans[id] = jasmine.createSpyObj(
+ 'timespan-' + id,
+ [ 'getStart', 'getEnd', 'getDuration', 'setStart', 'setEnd', 'setDuration' ]
+ );
+ mockPersists[id] = jasmine.createSpyObj(
+ 'persistence-' + id,
+ [ 'persist' ]
+ );
+ mockMutations[id] = jasmine.createSpyObj(
+ 'mutation-' + id,
+ [ 'mutate' ]
+ );
+ mockTimespans[id].getStart.andReturn(index * 1000);
+ mockTimespans[id].getDuration.andReturn(4000 + index);
+ mockTimespans[id].getEnd.andReturn(4000 + index + index * 1000);
+ });
+
+ mockLoader = jasmine.createSpyObj('objectLoader', ['load']);
+ mockDomainObject = makeMockDomainObject('a', ['b', 'c']);
+ mockDomainObjects = {
+ a: mockDomainObject,
+ b: makeMockDomainObject('b', ['d']),
+ c: makeMockDomainObject('c', ['e', 'f']),
+ d: makeMockDomainObject('d', []),
+ e: makeMockDomainObject('e', []),
+ f: makeMockDomainObject('f', [])
+ };
+ mockSelection = jasmine.createSpyObj('selection', ['get', 'select']);
+ mockCallback = jasmine.createSpy('callback');
+
+ testConfiguration = {};
+
+ mockLoader.load.andReturn(asPromise(
+ subgraph(mockDomainObject, mockDomainObjects)
+ ));
+
+ handler = new TimelineDragHandler(
+ mockDomainObject,
+ mockLoader
+ );
+ });
+
+ it("uses the loader to find subgraph", function () {
+ expect(mockLoader.load).toHaveBeenCalledWith(
+ mockDomainObject,
+ 'timespan'
+ );
+ });
+
+ it("reports available object identifiers", function () {
+ expect(handler.ids())
+ .toEqual(Object.keys(mockDomainObjects).sort());
+ });
+
+ it("exposes start/end/duration from timespan capabilities", function () {
+ expect(handler.start('a')).toEqual(0);
+ expect(handler.start('b')).toEqual(1000);
+ expect(handler.start('c')).toEqual(2000);
+ expect(handler.duration('a')).toEqual(4000);
+ expect(handler.duration('b')).toEqual(4001);
+ expect(handler.duration('c')).toEqual(4002);
+ expect(handler.end('a')).toEqual(4000);
+ expect(handler.end('b')).toEqual(5001);
+ expect(handler.end('c')).toEqual(6002);
+ });
+
+ it("accepts objects instead of identifiers for start/end/duration calls", function () {
+ Object.keys(mockDomainObjects).forEach(function (id) {
+ expect(handler.start(mockDomainObjects[id])).toEqual(handler.start(id));
+ expect(handler.duration(mockDomainObjects[id])).toEqual(handler.duration(id));
+ expect(handler.end(mockDomainObjects[id])).toEqual(handler.end(id));
+ });
+ });
+
+ it("mutates objects", function () {
+ handler.start('a', 123);
+ expect(mockTimespans.a.setStart).toHaveBeenCalledWith(123);
+ handler.duration('b', 42);
+ expect(mockTimespans.b.setDuration).toHaveBeenCalledWith(42);
+ handler.end('c', 12321);
+ expect(mockTimespans.c.setEnd).toHaveBeenCalledWith(12321);
+ });
+
+ it("disallows negative starts, durations", function () {
+ handler.start('a', -100);
+ handler.duration('b', -1000);
+ expect(mockTimespans.a.setStart).toHaveBeenCalledWith(0);
+ expect(mockTimespans.b.setDuration).toHaveBeenCalledWith(0);
+ });
+
+ it("disallows starts greater than ends violations", function () {
+ handler.start('a', 5000);
+ handler.end('b', 500);
+ expect(mockTimespans.a.setStart).toHaveBeenCalledWith(4000); // end time
+ expect(mockTimespans.b.setEnd).toHaveBeenCalledWith(1000); // start time
+ });
+
+ it("moves objects in groups", function () {
+ handler.move('b', 42);
+ expect(mockTimespans.b.setStart).toHaveBeenCalledWith(1042);
+ expect(mockTimespans.b.setEnd).toHaveBeenCalledWith(5043);
+ expect(mockTimespans.d.setStart).toHaveBeenCalledWith(3042);
+ expect(mockTimespans.d.setEnd).toHaveBeenCalledWith(7045);
+ // Verify no other interactions
+ ['a', 'c', 'e', 'f'].forEach(function (id) {
+ expect(mockTimespans[id].setStart).not.toHaveBeenCalled();
+ expect(mockTimespans[id].setEnd).not.toHaveBeenCalled();
+ });
+ });
+
+ it("moves whole subtrees", function () {
+ handler.move('a', 12321);
+ // We verify the math in the previous test, so just verify
+ // that the whole tree is effected here.
+ Object.keys(mockTimespans).forEach(function (id) {
+ expect(mockTimespans[id].setStart).toHaveBeenCalled();
+ });
+ });
+
+ it("prevents bulk moves past 0", function () {
+ // Have a start later; new lowest start is b, at 1000
+ mockTimespans.a.getStart.andReturn(10000);
+ handler.move('a', -10000);
+ // Verify that move was stopped at 0, for b, even though
+ // move was initiated at a
+ expect(mockTimespans.a.setStart).toHaveBeenCalledWith(9000);
+ expect(mockTimespans.b.setStart).toHaveBeenCalledWith(0);
+ expect(mockTimespans.c.setStart).toHaveBeenCalledWith(1000);
+ });
+
+ it("persists mutated objects", function () {
+ handler.start('a', 20);
+ handler.end('b', 50);
+ handler.duration('c', 30);
+ handler.persist();
+ expect(mockPersists.a.persist).toHaveBeenCalled();
+ expect(mockPersists.b.persist).toHaveBeenCalled();
+ expect(mockPersists.c.persist).toHaveBeenCalled();
+ expect(mockPersists.d.persist).not.toHaveBeenCalled();
+ expect(mockPersists.e.persist).not.toHaveBeenCalled();
+ expect(mockPersists.f.persist).not.toHaveBeenCalled();
+ });
+
+
+ });
+ }
+);
\ No newline at end of file
diff --git a/platform/features/timeline/test/controllers/drag/TimelineDragPopulatorSpec.js b/platform/features/timeline/test/controllers/drag/TimelineDragPopulatorSpec.js
new file mode 100644
index 0000000000..1a120a8fbb
--- /dev/null
+++ b/platform/features/timeline/test/controllers/drag/TimelineDragPopulatorSpec.js
@@ -0,0 +1,74 @@
+/*****************************************************************************
+ * Open MCT Web, Copyright (c) 2009-2015, United States Government
+ * as represented by the Administrator of the National Aeronautics and Space
+ * Administration. All rights reserved.
+ *
+ * Open MCT Web is licensed under the Apache License, Version 2.0 (the
+ * "License"); you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ * http://www.apache.org/licenses/LICENSE-2.0.
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
+ * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
+ * License for the specific language governing permissions and limitations
+ * under the License.
+ *
+ * Open MCT Web includes source code licensed under additional open source
+ * licenses. See the Open Source Licenses file (LICENSES.md) included with
+ * this source code distribution or the Licensing information page available
+ * at runtime from the About dialog for additional information.
+ *****************************************************************************/
+/*global define,describe,it,expect,beforeEach,waitsFor,jasmine,window,afterEach*/
+
+
+define(
+ ['../../../src/controllers/drag/TimelineDragPopulator'],
+ function (TimelineDragPopulator) {
+ "use strict";
+
+ describe("The timeline drag populator", function () {
+ var mockObjectLoader,
+ mockPromise,
+ mockSwimlane,
+ mockDomainObject,
+ populator;
+
+ beforeEach(function () {
+ mockObjectLoader = jasmine.createSpyObj("objectLoader", ["load"]);
+ mockPromise = jasmine.createSpyObj("promise", ["then"]);
+ mockSwimlane = jasmine.createSpyObj("swimlane", ["color"]);
+ mockDomainObject = jasmine.createSpyObj(
+ "domainObject",
+ ["getCapability", "getId"]
+ );
+
+ mockSwimlane.domainObject = mockDomainObject;
+ mockObjectLoader.load.andReturn(mockPromise);
+
+ populator = new TimelineDragPopulator(mockObjectLoader);
+ });
+
+ it("loads timespans for the represented object's subgraph", function () {
+ populator.populate(mockDomainObject);
+ expect(mockObjectLoader.load).toHaveBeenCalledWith(
+ mockDomainObject,
+ 'timespan'
+ );
+ });
+
+ it("updates handles for selections", function () {
+ // Ensure we have a represented object context
+ populator.populate(mockDomainObject);
+ // Initially, no selection and no handles
+ expect(populator.get()).toEqual([]);
+ // Select the swimlane
+ populator.select(mockSwimlane);
+ // We should have handles now
+ expect(populator.get().length).toEqual(3);
+ });
+
+ });
+
+ }
+);
\ No newline at end of file
diff --git a/platform/features/timeline/test/controllers/drag/TimelineEndHandleSpec.js b/platform/features/timeline/test/controllers/drag/TimelineEndHandleSpec.js
new file mode 100644
index 0000000000..85c06f22cb
--- /dev/null
+++ b/platform/features/timeline/test/controllers/drag/TimelineEndHandleSpec.js
@@ -0,0 +1,117 @@
+/*****************************************************************************
+ * Open MCT Web, Copyright (c) 2009-2015, United States Government
+ * as represented by the Administrator of the National Aeronautics and Space
+ * Administration. All rights reserved.
+ *
+ * Open MCT Web is licensed under the Apache License, Version 2.0 (the
+ * "License"); you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ * http://www.apache.org/licenses/LICENSE-2.0.
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
+ * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
+ * License for the specific language governing permissions and limitations
+ * under the License.
+ *
+ * Open MCT Web includes source code licensed under additional open source
+ * licenses. See the Open Source Licenses file (LICENSES.md) included with
+ * this source code distribution or the Licensing information page available
+ * at runtime from the About dialog for additional information.
+ *****************************************************************************/
+/*global define,describe,it,expect,beforeEach,waitsFor,jasmine,window,afterEach*/
+
+define(
+ ['../../../src/controllers/drag/TimelineEndHandle', '../../../src/TimelineConstants'],
+ function (TimelineEndHandle, TimelineConstants) {
+ 'use strict';
+
+ describe("A Timeline end drag handle", function () {
+ var mockDragHandler,
+ mockSnapHandler,
+ mockZoomController,
+ handle;
+
+ beforeEach(function () {
+ mockDragHandler = jasmine.createSpyObj(
+ 'dragHandler',
+ [ 'end', 'persist' ]
+ );
+ mockSnapHandler = jasmine.createSpyObj(
+ 'snapHandler',
+ [ 'snap' ]
+ );
+ mockZoomController = jasmine.createSpyObj(
+ 'zoom',
+ [ 'toMillis', 'toPixels' ]
+ );
+
+ mockDragHandler.end.andReturn(12321);
+
+ // Echo back the value from snapper for most tests
+ mockSnapHandler.snap.andCallFake(function (ts) {
+ return ts;
+ });
+
+ // Double pixels to get millis, for test purposes
+ mockZoomController.toMillis.andCallFake(function (px) {
+ return px * 2;
+ });
+
+ mockZoomController.toPixels.andCallFake(function (ms) {
+ return ms / 2;
+ });
+
+ handle = new TimelineEndHandle(
+ 'test-id',
+ mockDragHandler,
+ mockSnapHandler
+ );
+ });
+
+ it("provides a style for templates", function () {
+ var w = TimelineConstants.HANDLE_WIDTH;
+ expect(handle.style(mockZoomController)).toEqual({
+ // Left should be adjusted by zoom controller
+ left: (12321 / 2) - w + 'px',
+ // Width should match the defined constant
+ width: w + 'px'
+ });
+ });
+
+ it("forwards drags to the drag handler", function () {
+ handle.begin();
+ handle.drag(100, mockZoomController);
+ // Should have been interpreted as a +200 ms change
+ expect(mockDragHandler.end).toHaveBeenCalledWith(
+ "test-id",
+ 12521
+ );
+ });
+
+ it("snaps drags to other end points", function () {
+ mockSnapHandler.snap.andReturn(42);
+ handle.begin();
+ handle.drag(-10, mockZoomController);
+ // Should have used snap-to timestamp
+ expect(mockDragHandler.end).toHaveBeenCalledWith(
+ "test-id",
+ 42
+ );
+ });
+
+ it("persists when a move is complete", function () {
+ // Simulate normal drag cycle
+ handle.begin();
+ handle.drag(100, mockZoomController);
+ // Should not have persisted yet
+ expect(mockDragHandler.persist).not.toHaveBeenCalled();
+ // Finish the drag
+ handle.finish();
+ // Now it should have persisted
+ expect(mockDragHandler.persist).toHaveBeenCalled();
+ });
+
+ });
+ }
+);
\ No newline at end of file
diff --git a/platform/features/timeline/test/controllers/drag/TimelineMoveHandleSpec.js b/platform/features/timeline/test/controllers/drag/TimelineMoveHandleSpec.js
new file mode 100644
index 0000000000..2f49bfb8bf
--- /dev/null
+++ b/platform/features/timeline/test/controllers/drag/TimelineMoveHandleSpec.js
@@ -0,0 +1,184 @@
+/*****************************************************************************
+ * Open MCT Web, Copyright (c) 2009-2015, United States Government
+ * as represented by the Administrator of the National Aeronautics and Space
+ * Administration. All rights reserved.
+ *
+ * Open MCT Web is licensed under the Apache License, Version 2.0 (the
+ * "License"); you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ * http://www.apache.org/licenses/LICENSE-2.0.
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
+ * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
+ * License for the specific language governing permissions and limitations
+ * under the License.
+ *
+ * Open MCT Web includes source code licensed under additional open source
+ * licenses. See the Open Source Licenses file (LICENSES.md) included with
+ * this source code distribution or the Licensing information page available
+ * at runtime from the About dialog for additional information.
+ *****************************************************************************/
+/*global define,describe,it,expect,beforeEach,waitsFor,jasmine,window,afterEach*/
+
+define(
+ ['../../../src/controllers/drag/TimelineMoveHandle', '../../../src/TimelineConstants'],
+ function (TimelineMoveHandle, TimelineConstants) {
+ 'use strict';
+
+ describe("A Timeline move drag handle", function () {
+ var mockDragHandler,
+ mockSnapHandler,
+ mockZoomController,
+ handle;
+
+ beforeEach(function () {
+ mockDragHandler = jasmine.createSpyObj(
+ 'dragHandler',
+ [ 'start', 'duration', 'end', 'move', 'persist' ]
+ );
+ mockSnapHandler = jasmine.createSpyObj(
+ 'snapHandler',
+ [ 'snap' ]
+ );
+ mockZoomController = jasmine.createSpyObj(
+ 'zoom',
+ [ 'toMillis', 'toPixels' ]
+ );
+
+ mockDragHandler.start.andReturn(12321);
+ mockDragHandler.duration.andReturn(4200);
+ mockDragHandler.end.andReturn(12321 + 4200);
+
+ // Echo back the value from snapper for most tests
+ mockSnapHandler.snap.andCallFake(function (ts) {
+ return ts;
+ });
+
+ // Double pixels to get millis, for test purposes
+ mockZoomController.toMillis.andCallFake(function (px) {
+ return px * 2;
+ });
+
+ mockZoomController.toPixels.andCallFake(function (ms) {
+ return ms / 2;
+ });
+
+ handle = new TimelineMoveHandle(
+ 'test-id',
+ mockDragHandler,
+ mockSnapHandler
+ );
+ });
+
+ it("provides a style for templates", function () {
+ var w = TimelineConstants.HANDLE_WIDTH;
+ expect(handle.style(mockZoomController)).toEqual({
+ // Left should be adjusted by zoom controller
+ left: (12321 / 2) + w + 'px',
+ // Width should be duration minus end points
+ width: 2100 - (w * 2) + 'px'
+ });
+ });
+
+ it("forwards drags to the drag handler", function () {
+ handle.begin();
+ handle.drag(100, mockZoomController);
+ // Should have been interpreted as a +200 ms change
+ expect(mockDragHandler.move).toHaveBeenCalledWith(
+ "test-id",
+ 200
+ );
+ });
+
+ it("tracks drags incrementally", function () {
+ handle.begin();
+
+ handle.drag(100, mockZoomController);
+ // Should have been interpreted as a +200 ms change...
+ expect(mockDragHandler.move).toHaveBeenCalledWith(
+ "test-id",
+ 200
+ );
+
+ // Reflect the change from the drag handler
+ mockDragHandler.start.andReturn(12521);
+ mockDragHandler.end.andReturn(12521 + 4200);
+
+ // ....followed by a +100 ms change.
+ handle.drag(150, mockZoomController);
+ expect(mockDragHandler.move).toHaveBeenCalledWith(
+ "test-id",
+ 100
+ );
+ });
+
+ it("snaps drags to other end points", function () {
+ mockSnapHandler.snap.andCallFake(function (ts) {
+ return ts + 10;
+ });
+ handle.begin();
+ handle.drag(100, mockZoomController);
+ // Should have used snap-to timestamp, which was 10
+ // ms greater than the provided one
+ expect(mockDragHandler.move).toHaveBeenCalledWith(
+ "test-id",
+ 210
+ );
+ });
+
+ it("considers snaps for both endpoints", function () {
+ handle.begin();
+ expect(mockSnapHandler.snap).not.toHaveBeenCalled();
+ handle.drag(100, mockZoomController);
+ expect(mockSnapHandler.snap.calls.length).toEqual(2);
+ });
+
+ it("chooses the closest snap-to location", function () {
+ // Use a toggle to give snapped timestamps that are
+ // different distances away from the original.
+ // The move handle needs to choose the closest snap-to,
+ // regardless of whether it is the start/end (which
+ // will vary based on the initial state of this toggle.)
+ var toggle = false;
+ mockSnapHandler.snap.andCallFake(function (ts) {
+ toggle = !toggle;
+ return ts + (toggle ? -5 : 10);
+ });
+ handle.begin();
+ handle.drag(100, mockZoomController);
+ expect(mockDragHandler.move).toHaveBeenCalledWith(
+ "test-id",
+ 195 // Chose the -5
+ );
+
+ // Reflect the change from the drag handler
+ mockDragHandler.start.andReturn(12521 - 5);
+ mockDragHandler.end.andReturn(12521 + 4200 - 5);
+
+ toggle = true; // Change going-in state
+ handle.drag(300, mockZoomController);
+ // Note that the -5 offset is shown in the current state,
+ // so snapping to the -5 implies that the full 400ms will
+ // be moved (again, relative to dragHandler's reported state)
+ expect(mockDragHandler.move).toHaveBeenCalledWith(
+ "test-id",
+ 400 // Still chose the -5
+ );
+ });
+
+ it("persists when a move is complete", function () {
+ // Simulate normal drag cycle
+ handle.begin();
+ handle.drag(100, mockZoomController);
+ // Should not have persisted yet
+ expect(mockDragHandler.persist).not.toHaveBeenCalled();
+ // Finish the drag
+ handle.finish();
+ // Now it should have persisted
+ expect(mockDragHandler.persist).toHaveBeenCalled();
+ });
+
+ });
+ }
+);
\ No newline at end of file
diff --git a/platform/features/timeline/test/controllers/drag/TimelineSnapHandlerSpec.js b/platform/features/timeline/test/controllers/drag/TimelineSnapHandlerSpec.js
new file mode 100644
index 0000000000..780bd7c999
--- /dev/null
+++ b/platform/features/timeline/test/controllers/drag/TimelineSnapHandlerSpec.js
@@ -0,0 +1,81 @@
+/*****************************************************************************
+ * Open MCT Web, Copyright (c) 2009-2015, United States Government
+ * as represented by the Administrator of the National Aeronautics and Space
+ * Administration. All rights reserved.
+ *
+ * Open MCT Web is licensed under the Apache License, Version 2.0 (the
+ * "License"); you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ * http://www.apache.org/licenses/LICENSE-2.0.
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
+ * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
+ * License for the specific language governing permissions and limitations
+ * under the License.
+ *
+ * Open MCT Web includes source code licensed under additional open source
+ * licenses. See the Open Source Licenses file (LICENSES.md) included with
+ * this source code distribution or the Licensing information page available
+ * at runtime from the About dialog for additional information.
+ *****************************************************************************/
+/*global define,describe,it,expect,beforeEach,waitsFor,jasmine,window,afterEach*/
+
+define(
+ ['../../../src/controllers/drag/TimelineSnapHandler'],
+ function (TimelineSnapHandler) {
+ 'use strict';
+
+ describe("A Timeline snap handler", function () {
+ var mockDragHandler,
+ handler;
+
+ beforeEach(function () {
+ var starts = { a: 1000, b: 2000, c: 2500, d: 2600 },
+ ends = { a: 2050, b: 3000, c: 2700, d: 10000 };
+
+ mockDragHandler = jasmine.createSpyObj(
+ 'dragHandler',
+ [ 'start', 'end', 'ids' ]
+ );
+
+ mockDragHandler.ids.andReturn(['a', 'b', 'c', 'd']);
+ mockDragHandler.start.andCallFake(function (id) {
+ return starts[id];
+ });
+ mockDragHandler.end.andCallFake(function (id) {
+ return ends[id];
+ });
+
+ handler = new TimelineSnapHandler(mockDragHandler);
+ });
+
+ it("provides a preferred snap location within tolerance", function () {
+ expect(handler.snap(2511, 15, 'a')).toEqual(2500); // c's start
+ expect(handler.snap(2488, 15, 'a')).toEqual(2500); // c's start
+ expect(handler.snap(10, 1000, 'b')).toEqual(1000); // a's start
+ expect(handler.snap(2711, 20, 'd')).toEqual(2700); // c's end
+ });
+
+ it("excludes provided id from snapping", function () {
+ // Don't want objects to snap to themselves, so we need
+ // this exclusion.
+ expect(handler.snap(2010, 50, 'b')).toEqual(2050); // a's end
+ // Verify that b's start would have been used had the
+ // id not been provided
+ expect(handler.snap(2010, 50, 'd')).toEqual(2000);
+ });
+
+ it("snaps to the closest point, when multiple match", function () {
+ // 2600 and 2700 (plus others) are both in range here
+ expect(handler.snap(2651, 1000, 'a')).toEqual(2700);
+ });
+
+ it("does not snap if no points are within tolerance", function () {
+ // Closest are 1000 and 2000, which are well outside of tolerance
+ expect(handler.snap(1503, 100, 'd')).toEqual(1503);
+ });
+
+ });
+ }
+);
\ No newline at end of file
diff --git a/platform/features/timeline/test/controllers/drag/TimelineStartHandleSpec.js b/platform/features/timeline/test/controllers/drag/TimelineStartHandleSpec.js
new file mode 100644
index 0000000000..b036bf93f9
--- /dev/null
+++ b/platform/features/timeline/test/controllers/drag/TimelineStartHandleSpec.js
@@ -0,0 +1,116 @@
+/*****************************************************************************
+ * Open MCT Web, Copyright (c) 2009-2015, United States Government
+ * as represented by the Administrator of the National Aeronautics and Space
+ * Administration. All rights reserved.
+ *
+ * Open MCT Web is licensed under the Apache License, Version 2.0 (the
+ * "License"); you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ * http://www.apache.org/licenses/LICENSE-2.0.
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
+ * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
+ * License for the specific language governing permissions and limitations
+ * under the License.
+ *
+ * Open MCT Web includes source code licensed under additional open source
+ * licenses. See the Open Source Licenses file (LICENSES.md) included with
+ * this source code distribution or the Licensing information page available
+ * at runtime from the About dialog for additional information.
+ *****************************************************************************/
+/*global define,describe,it,expect,beforeEach,waitsFor,jasmine,window,afterEach*/
+
+define(
+ ['../../../src/controllers/drag/TimelineStartHandle', '../../../src/TimelineConstants'],
+ function (TimelineStartHandle, TimelineConstants) {
+ 'use strict';
+
+ describe("A Timeline start drag handle", function () {
+ var mockDragHandler,
+ mockSnapHandler,
+ mockZoomController,
+ handle;
+
+ beforeEach(function () {
+ mockDragHandler = jasmine.createSpyObj(
+ 'dragHandler',
+ [ 'start', 'persist' ]
+ );
+ mockSnapHandler = jasmine.createSpyObj(
+ 'snapHandler',
+ [ 'snap' ]
+ );
+ mockZoomController = jasmine.createSpyObj(
+ 'zoom',
+ [ 'toMillis', 'toPixels' ]
+ );
+
+ mockDragHandler.start.andReturn(12321);
+
+ // Echo back the value from snapper for most tests
+ mockSnapHandler.snap.andCallFake(function (ts) {
+ return ts;
+ });
+
+ // Double pixels to get millis, for test purposes
+ mockZoomController.toMillis.andCallFake(function (px) {
+ return px * 2;
+ });
+
+ mockZoomController.toPixels.andCallFake(function (ms) {
+ return ms / 2;
+ });
+
+ handle = new TimelineStartHandle(
+ 'test-id',
+ mockDragHandler,
+ mockSnapHandler
+ );
+ });
+
+ it("provides a style for templates", function () {
+ expect(handle.style(mockZoomController)).toEqual({
+ // Left should be adjusted by zoom controller
+ left: (12321 / 2) + 'px',
+ // Width should match the defined constant
+ width: TimelineConstants.HANDLE_WIDTH + 'px'
+ });
+ });
+
+ it("forwards drags to the drag handler", function () {
+ handle.begin();
+ handle.drag(100, mockZoomController);
+ // Should have been interpreted as a +200 ms change
+ expect(mockDragHandler.start).toHaveBeenCalledWith(
+ "test-id",
+ 12521
+ );
+ });
+
+ it("snaps drags to other end points", function () {
+ mockSnapHandler.snap.andReturn(42);
+ handle.begin();
+ handle.drag(-10, mockZoomController);
+ // Should have used snap-to timestamp
+ expect(mockDragHandler.start).toHaveBeenCalledWith(
+ "test-id",
+ 42
+ );
+ });
+
+ it("persists when a move is complete", function () {
+ // Simulate normal drag cycle
+ handle.begin();
+ handle.drag(100, mockZoomController);
+ // Should not have persisted yet
+ expect(mockDragHandler.persist).not.toHaveBeenCalled();
+ // Finish the drag
+ handle.finish();
+ // Now it should have persisted
+ expect(mockDragHandler.persist).toHaveBeenCalled();
+ });
+
+ });
+ }
+);
\ No newline at end of file
diff --git a/platform/features/timeline/test/controllers/graph/TimelineGraphPopulatorSpec.js b/platform/features/timeline/test/controllers/graph/TimelineGraphPopulatorSpec.js
new file mode 100644
index 0000000000..2fd042dc47
--- /dev/null
+++ b/platform/features/timeline/test/controllers/graph/TimelineGraphPopulatorSpec.js
@@ -0,0 +1,153 @@
+/*****************************************************************************
+ * Open MCT Web, Copyright (c) 2009-2015, United States Government
+ * as represented by the Administrator of the National Aeronautics and Space
+ * Administration. All rights reserved.
+ *
+ * Open MCT Web is licensed under the Apache License, Version 2.0 (the
+ * "License"); you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ * http://www.apache.org/licenses/LICENSE-2.0.
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
+ * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
+ * License for the specific language governing permissions and limitations
+ * under the License.
+ *
+ * Open MCT Web includes source code licensed under additional open source
+ * licenses. See the Open Source Licenses file (LICENSES.md) included with
+ * this source code distribution or the Licensing information page available
+ * at runtime from the About dialog for additional information.
+ *****************************************************************************/
+/*global define,describe,it,expect,beforeEach,waitsFor,jasmine,window,afterEach*/
+
+define(
+ ['../../../src/controllers/graph/TimelineGraphPopulator'],
+ function (TimelineGraphPopulator) {
+ 'use strict';
+
+ describe("A Timeline's resource graph populator", function () {
+ var mockSwimlanes,
+ mockQ,
+ testResources,
+ populator;
+
+ function asPromise(v) {
+ return (v || {}).then ? v : {
+ then: function (callback) {
+ return asPromise(callback(v));
+ },
+ testValue: v
+ };
+ }
+
+ function allPromises(promises) {
+ return asPromise(promises.map(function (p) {
+ return (p || {}).then ? p.testValue : p;
+ }));
+ }
+
+
+ beforeEach(function () {
+ testResources = {
+ a: [ 'xyz', 'abc' ],
+ b: [ 'xyz' ],
+ c: [ 'xyz', 'abc', 'def', 'ghi' ]
+ };
+
+ mockQ = jasmine.createSpyObj('$q', ['when', 'all']);
+
+ mockSwimlanes = ['a', 'b', 'c'].map(function (k) {
+ var mockSwimlane = jasmine.createSpyObj(
+ 'swimlane-' + k,
+ [ 'graph', 'color' ]
+ ),
+ mockGraph = jasmine.createSpyObj(
+ 'graph-' + k,
+ [ 'getPointCount', 'getDomainValue', 'getRangeValue' ]
+ );
+ mockSwimlane.graph.andReturn(true);
+ mockSwimlane.domainObject = jasmine.createSpyObj(
+ 'domainObject-' + k,
+ [ 'getCapability', 'hasCapability', 'useCapability', 'getId' ]
+ );
+ mockSwimlane.color.andReturn('#' + k);
+ // Provide just enough information about graphs to support
+ // determination of which graphs to show
+ mockSwimlane.domainObject.useCapability.andCallFake(function () {
+ var obj = {};
+ testResources[k].forEach(function (r) {
+ obj[r] = mockGraph;
+ });
+ return asPromise(obj);
+ });
+ mockSwimlane.domainObject.hasCapability
+ .andReturn(true);
+ mockSwimlane.domainObject.getId
+ .andReturn(k);
+ mockGraph.getPointCount.andReturn(0);
+
+ return mockSwimlane;
+ });
+
+ mockQ.when.andCallFake(asPromise);
+ mockQ.all.andCallFake(allPromises);
+
+ populator = new TimelineGraphPopulator(mockQ);
+ });
+
+ it("provides no graphs by default", function () {
+ expect(populator.get()).toEqual([]);
+ });
+
+ it("provides one graph per resource type", function () {
+ populator.populate(mockSwimlanes);
+ // There are 4 unique resource types shared here...
+ expect(populator.get().length).toEqual(4);
+ });
+
+ it("does not include graphs based on swimlane configuration", function () {
+ mockSwimlanes[2].graph.andReturn(false);
+ populator.populate(mockSwimlanes);
+ // Only two unique swimlanes in the other two
+ expect(populator.get().length).toEqual(2);
+ // Verify interactions as well
+ expect(mockSwimlanes[0].domainObject.useCapability)
+ .toHaveBeenCalledWith('graph');
+ expect(mockSwimlanes[1].domainObject.useCapability)
+ .toHaveBeenCalledWith('graph');
+ expect(mockSwimlanes[2].domainObject.useCapability)
+ .not.toHaveBeenCalled();
+ });
+
+ it("does not recreate graphs when swimlanes don't change", function () {
+ var initialValue;
+ // Get an initial set of graphs
+ populator.populate(mockSwimlanes);
+ initialValue = populator.get();
+ // Repopulate with same data; should get same graphs
+ populator.populate(mockSwimlanes);
+ expect(populator.get()).toBe(initialValue);
+ // Something changed...
+ mockSwimlanes.pop();
+ populator.populate(mockSwimlanes);
+ // Now we should get different graphs
+ expect(populator.get()).not.toBe(initialValue);
+ });
+
+ // Regression test for WTD-1155
+ it("does recreate graphs when inclusions change", function () {
+ var initialValue;
+ // Get an initial set of graphs
+ populator.populate(mockSwimlanes);
+ initialValue = populator.get();
+ // Change resource graph inclusion...
+ mockSwimlanes[1].graph.andReturn(false);
+ populator.populate(mockSwimlanes);
+ // Now we should get different graphs
+ expect(populator.get()).not.toBe(initialValue);
+ });
+
+ });
+ }
+);
\ No newline at end of file
diff --git a/platform/features/timeline/test/controllers/graph/TimelineGraphRendererSpec.js b/platform/features/timeline/test/controllers/graph/TimelineGraphRendererSpec.js
new file mode 100644
index 0000000000..bbaa721bb5
--- /dev/null
+++ b/platform/features/timeline/test/controllers/graph/TimelineGraphRendererSpec.js
@@ -0,0 +1,77 @@
+/*****************************************************************************
+ * Open MCT Web, Copyright (c) 2009-2015, United States Government
+ * as represented by the Administrator of the National Aeronautics and Space
+ * Administration. All rights reserved.
+ *
+ * Open MCT Web is licensed under the Apache License, Version 2.0 (the
+ * "License"); you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ * http://www.apache.org/licenses/LICENSE-2.0.
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
+ * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
+ * License for the specific language governing permissions and limitations
+ * under the License.
+ *
+ * Open MCT Web includes source code licensed under additional open source
+ * licenses. See the Open Source Licenses file (LICENSES.md) included with
+ * this source code distribution or the Licensing information page available
+ * at runtime from the About dialog for additional information.
+ *****************************************************************************/
+/*global define,describe,it,expect,beforeEach,waitsFor,jasmine,window,afterEach*/
+
+define(
+ ['../../../src/controllers/graph/TimelineGraphRenderer'],
+ function (TimelineGraphRenderer) {
+ 'use strict';
+
+ describe("A Timeline's graph renderer", function () {
+ var renderer;
+
+ beforeEach(function () {
+ renderer = new TimelineGraphRenderer();
+ });
+
+ it("converts utilizations to buffers", function () {
+ var utilization = {
+ getPointCount: function () {
+ return 10;
+ },
+ getDomainValue: function (i) {
+ return i * 10;
+ },
+ getRangeValue: function (i) {
+ return Math.sin(i);
+ }
+ },
+ buffer = renderer.render(utilization),
+ i;
+
+ // Should be flat list of alternating x/y,
+ // so 20 elements
+ expect(buffer.length).toEqual(20);
+
+ // Verify contents
+ for (i = 0; i < 10; i += 1) {
+ // Check for 6 decimal digits of precision, roughly
+ // what we expect after conversion to 32-bit float
+ expect(buffer[i * 2]).toBeCloseTo(i * 10, 6);
+ expect(buffer[i * 2 + 1]).toBeCloseTo(Math.sin(i), 6);
+ }
+ });
+
+ it("decodes color strings", function () {
+ // Note that decoded color should have alpha channel as well
+ expect(renderer.decode('#FFFFFF'))
+ .toEqual([1, 1, 1, 1]);
+ expect(renderer.decode('#000000'))
+ .toEqual([0, 0, 0, 1]);
+ expect(renderer.decode('#FF8000'))
+ .toEqual([1, 0.5019607843137255, 0, 1]);
+ });
+
+
+ });
+ }
+);
\ No newline at end of file
diff --git a/platform/features/timeline/test/controllers/graph/TimelineGraphSpec.js b/platform/features/timeline/test/controllers/graph/TimelineGraphSpec.js
new file mode 100644
index 0000000000..5240fc1152
--- /dev/null
+++ b/platform/features/timeline/test/controllers/graph/TimelineGraphSpec.js
@@ -0,0 +1,172 @@
+/*****************************************************************************
+ * Open MCT Web, Copyright (c) 2009-2015, United States Government
+ * as represented by the Administrator of the National Aeronautics and Space
+ * Administration. All rights reserved.
+ *
+ * Open MCT Web is licensed under the Apache License, Version 2.0 (the
+ * "License"); you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ * http://www.apache.org/licenses/LICENSE-2.0.
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
+ * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
+ * License for the specific language governing permissions and limitations
+ * under the License.
+ *
+ * Open MCT Web includes source code licensed under additional open source
+ * licenses. See the Open Source Licenses file (LICENSES.md) included with
+ * this source code distribution or the Licensing information page available
+ * at runtime from the About dialog for additional information.
+ *****************************************************************************/
+/*global define,describe,it,expect,beforeEach,waitsFor,jasmine,window,afterEach*/
+
+define(
+ ['../../../src/controllers/graph/TimelineGraph'],
+ function (TimelineGraph) {
+ 'use strict';
+
+ describe("A Timeline's resource graph", function () {
+ var mockDomainObjects,
+ mockRenderer,
+ testColors,
+ mockGraphs,
+ graph;
+
+ function asPromise(v) {
+ return (v || {}).then ? v : {
+ then: function (callback) {
+ return asPromise(callback(v));
+ }
+ };
+ }
+
+
+ beforeEach(function () {
+ testColors = {
+ a: [ 0, 1, 0 ],
+ b: [ 1, 0, 1 ],
+ c: [ 1, 0, 0 ]
+ };
+
+ mockGraphs = [];
+ mockDomainObjects = {};
+
+ ['a', 'b', 'c'].forEach(function (k, i) {
+ var mockGraph = jasmine.createSpyObj(
+ 'utilization-' + k,
+ [ 'getPointCount', 'getDomainValue', 'getRangeValue' ]
+ );
+ mockDomainObjects[k] = jasmine.createSpyObj(
+ 'domainObject-' + k,
+ [ 'getCapability', 'useCapability' ]
+ );
+ mockDomainObjects[k].useCapability.andReturn(asPromise({
+ testResource: mockGraph
+ }));
+ mockGraph.getPointCount.andReturn(i + 2);
+ mockGraph.testField = k;
+ mockGraph.testIndex = i;
+
+ // Store to allow changes later
+ mockGraphs.push(mockGraph);
+ });
+
+ mockRenderer = jasmine.createSpyObj(
+ 'renderer',
+ [ 'render', 'decode' ]
+ );
+
+ mockRenderer.render.andCallFake(function (utilization) {
+ var result = [];
+ while (result.length < (utilization.testIndex + 2) * 2) {
+ result.push(Math.floor(result.length / 2));
+ // Alternate +/- to give something to test to
+ result.push(
+ ((result.length % 4 > 1) ? -1 : 1) *
+ (10 * utilization.testIndex)
+ );
+ }
+ return result;
+ });
+
+ mockRenderer.decode.andCallFake(function (color) {
+ return testColors[color];
+ });
+
+ graph = new TimelineGraph(
+ 'testResource',
+ mockDomainObjects,
+ mockRenderer
+ );
+ });
+
+ it("exposes minimum/maximum", function () {
+ expect(graph.minimum()).toEqual(-20);
+ expect(graph.maximum()).toEqual(20);
+ });
+
+ it("exposes resource key", function () {
+ expect(graph.key).toEqual('testResource');
+ });
+
+ it("exposes a rendered drawing object", function () {
+ // Much of the work here is done by the renderer,
+ // so just check that it got used and assigned
+ expect(graph.drawingObject.lines).toEqual([
+ {
+ color: testColors.a,
+ buffer: [0, 0, 1, 0],
+ points: 2
+ },
+ {
+ color: testColors.b,
+ buffer: [0, 10, 1, -10, 2, 10],
+ points: 3
+ },
+ {
+ color: testColors.c,
+ buffer: [0, 20, 1, -20, 2, 20, 3, -20],
+ points: 4
+ }
+ ]);
+ });
+
+ it("allows its bounds to be specified", function () {
+ graph.setBounds(42, 12321);
+ expect(graph.drawingObject.origin[0]).toEqual(42);
+ expect(graph.drawingObject.dimensions[0]).toEqual(12321);
+ });
+
+ it("provides a minimum/maximum even with no data", function () {
+ mockGraphs.forEach(function (mockGraph) {
+ mockGraph.getPointCount.andReturn(0);
+ });
+
+ // Create a graph of these utilizations
+ graph = new TimelineGraph(
+ 'testResource',
+ mockDomainObjects,
+ mockRenderer
+ );
+
+ // Verify that we get some usable defaults
+ expect(graph.minimum()).toEqual(jasmine.any(Number));
+ expect(graph.maximum()).toEqual(jasmine.any(Number));
+ expect(graph.maximum() > graph.minimum()).toBeTruthy();
+ });
+
+ it("refreshes lines upon request", function () {
+ // Mock renderer looks at testIndex, so change it here...
+ mockGraphs[0].testIndex = 3;
+ // Should trigger a new render
+ graph.refresh();
+ // Values correspond to the new index now
+ expect(graph.drawingObject.lines[0].buffer).toEqual(
+ [0, 30, 1, -30, 2, 30, 3, -30, 4, 30]
+ );
+ });
+
+ });
+ }
+);
\ No newline at end of file
diff --git a/platform/features/timeline/test/controllers/swimlane/TimelineColorAssignerSpec.js b/platform/features/timeline/test/controllers/swimlane/TimelineColorAssignerSpec.js
new file mode 100644
index 0000000000..beb3886f2d
--- /dev/null
+++ b/platform/features/timeline/test/controllers/swimlane/TimelineColorAssignerSpec.js
@@ -0,0 +1,86 @@
+/*****************************************************************************
+ * Open MCT Web, Copyright (c) 2009-2015, United States Government
+ * as represented by the Administrator of the National Aeronautics and Space
+ * Administration. All rights reserved.
+ *
+ * Open MCT Web is licensed under the Apache License, Version 2.0 (the
+ * "License"); you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ * http://www.apache.org/licenses/LICENSE-2.0.
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
+ * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
+ * License for the specific language governing permissions and limitations
+ * under the License.
+ *
+ * Open MCT Web includes source code licensed under additional open source
+ * licenses. See the Open Source Licenses file (LICENSES.md) included with
+ * this source code distribution or the Licensing information page available
+ * at runtime from the About dialog for additional information.
+ *****************************************************************************/
+/*global define,describe,it,expect,beforeEach,waitsFor,jasmine,window,afterEach*/
+
+define(
+ ['../../../src/controllers/swimlane/TimelineColorAssigner'],
+ function (TimelineColorAssigner) {
+ 'use strict';
+
+ describe("The Timeline legend color assigner", function () {
+ var testConfiguration,
+ assigner;
+
+ beforeEach(function () {
+ testConfiguration = {};
+ assigner = new TimelineColorAssigner(testConfiguration);
+ });
+
+ it("assigns colors by identifier", function () {
+ expect(assigner.get('xyz')).toBeUndefined();
+ assigner.assign('xyz');
+ expect(assigner.get('xyz')).toEqual(jasmine.any(String));
+ });
+
+ it("releases colors", function () {
+ assigner.assign('xyz');
+ assigner.release('xyz');
+ expect(assigner.get('xyz')).toBeUndefined();
+ });
+
+ it("provides at least 30 unique colors", function () {
+ var colors = {}, i, ids = [];
+
+ // Add item to set
+ function set(c) { colors[c] = true; }
+
+ // Generate ids
+ for (i = 0; i < 30; i += 1) { ids.push("id" + i); }
+
+ // Assign colors to each id, then retrieve colors,
+ // storing into the set
+ ids.forEach(assigner.assign);
+ ids.map(assigner.get).map(set);
+
+ // Should now be 30 elements in that set
+ expect(Object.keys(colors).length).toEqual(30);
+ });
+
+ it("populates the configuration with colors", function () {
+ assigner.assign('xyz');
+ expect(testConfiguration.xyz).toBeDefined();
+ });
+
+ it("preserves other colors when releases occur", function () {
+ var c;
+ assigner.assign('xyz');
+ c = assigner.get('xyz');
+ // Assign/release a different color
+ assigner.assign('abc');
+ assigner.release('abc');
+ // Our original assignment should be preserved
+ expect(assigner.get('xyz')).toEqual(c);
+ });
+
+ });
+ }
+);
diff --git a/platform/features/timeline/test/controllers/swimlane/TimelineProxySpec.js b/platform/features/timeline/test/controllers/swimlane/TimelineProxySpec.js
new file mode 100644
index 0000000000..0ecd073219
--- /dev/null
+++ b/platform/features/timeline/test/controllers/swimlane/TimelineProxySpec.js
@@ -0,0 +1,108 @@
+/*****************************************************************************
+ * Open MCT Web, Copyright (c) 2009-2015, United States Government
+ * as represented by the Administrator of the National Aeronautics and Space
+ * Administration. All rights reserved.
+ *
+ * Open MCT Web is licensed under the Apache License, Version 2.0 (the
+ * "License"); you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ * http://www.apache.org/licenses/LICENSE-2.0.
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
+ * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
+ * License for the specific language governing permissions and limitations
+ * under the License.
+ *
+ * Open MCT Web includes source code licensed under additional open source
+ * licenses. See the Open Source Licenses file (LICENSES.md) included with
+ * this source code distribution or the Licensing information page available
+ * at runtime from the About dialog for additional information.
+ *****************************************************************************/
+/*global define,describe,it,expect,beforeEach,waitsFor,jasmine,window,afterEach*/
+
+define(
+ ['../../../src/controllers/swimlane/TimelineProxy'],
+ function (TimelineProxy) {
+ 'use strict';
+
+ describe("The Timeline's selection proxy", function () {
+ var mockDomainObject,
+ mockSelection,
+ mockActionCapability,
+ mockActions,
+ proxy;
+
+ beforeEach(function () {
+ mockDomainObject = jasmine.createSpyObj(
+ 'domainObject',
+ ['getCapability']
+ );
+ mockSelection = jasmine.createSpyObj(
+ 'selection',
+ [ 'get' ]
+ );
+ mockActionCapability = jasmine.createSpyObj(
+ 'action',
+ [ 'getActions' ]
+ );
+ mockActions = ['a', 'b', 'c'].map(function (type) {
+ var mockAction = jasmine.createSpyObj(
+ 'action-' + type,
+ [ 'perform', 'getMetadata' ]
+ );
+ mockAction.getMetadata.andReturn({ type: type });
+ return mockAction;
+ });
+
+ mockDomainObject.getCapability.andReturn(mockActionCapability);
+ mockActionCapability.getActions.andReturn(mockActions);
+
+ proxy = new TimelineProxy(mockDomainObject, mockSelection);
+ });
+
+ it("triggers a create action on add", function () {
+ // Should trigger b's create action
+ proxy.add('b');
+ expect(mockActions[1].perform).toHaveBeenCalled();
+
+ // Also check that other actions weren't invoked
+ expect(mockActions[0].perform).not.toHaveBeenCalled();
+ expect(mockActions[2].perform).not.toHaveBeenCalled();
+
+ // Verify that interactions were for correct keys
+ expect(mockDomainObject.getCapability)
+ .toHaveBeenCalledWith('action');
+ expect(mockActionCapability.getActions)
+ .toHaveBeenCalledWith('create');
+ });
+
+ it("invokes the action on the selection, if any", function () {
+ var mockOtherObject = jasmine.createSpyObj(
+ 'other',
+ ['getCapability']
+ ),
+ mockOtherAction = jasmine.createSpyObj(
+ 'actionCapability',
+ ['getActions']
+ ),
+ mockAction = jasmine.createSpyObj(
+ 'action',
+ ['perform', 'getMetadata']
+ );
+
+ // Set up mocks
+ mockSelection.get.andReturn({ domainObject: mockOtherObject });
+ mockOtherObject.getCapability.andReturn(mockOtherAction);
+ mockOtherAction.getActions.andReturn([mockAction]);
+ mockAction.getMetadata.andReturn({ type: 'z' });
+
+ // Invoke add method; should create with selected object
+ proxy.add('z');
+ expect(mockAction.perform).toHaveBeenCalled();
+ });
+
+
+ });
+ }
+);
diff --git a/platform/features/timeline/test/controllers/swimlane/TimelineSwimlaneDecoratorSpec.js b/platform/features/timeline/test/controllers/swimlane/TimelineSwimlaneDecoratorSpec.js
new file mode 100644
index 0000000000..643c32b55e
--- /dev/null
+++ b/platform/features/timeline/test/controllers/swimlane/TimelineSwimlaneDecoratorSpec.js
@@ -0,0 +1,181 @@
+/*****************************************************************************
+ * Open MCT Web, Copyright (c) 2009-2015, United States Government
+ * as represented by the Administrator of the National Aeronautics and Space
+ * Administration. All rights reserved.
+ *
+ * Open MCT Web is licensed under the Apache License, Version 2.0 (the
+ * "License"); you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ * http://www.apache.org/licenses/LICENSE-2.0.
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
+ * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
+ * License for the specific language governing permissions and limitations
+ * under the License.
+ *
+ * Open MCT Web includes source code licensed under additional open source
+ * licenses. See the Open Source Licenses file (LICENSES.md) included with
+ * this source code distribution or the Licensing information page available
+ * at runtime from the About dialog for additional information.
+ *****************************************************************************/
+/*global define,describe,it,expect,beforeEach,waitsFor,jasmine,window,afterEach*/
+
+define(
+ ['../../../src/controllers/swimlane/TimelineSwimlaneDecorator'],
+ function (TimelineSwimlaneDecorator) {
+ 'use strict';
+
+ describe("A Timeline swimlane decorator", function () {
+ var mockSwimlane,
+ mockSelection,
+ mockCapabilities,
+ testModel,
+ mockPromise,
+ decorator;
+
+ beforeEach(function () {
+ mockSwimlane = {};
+ mockCapabilities = {};
+ testModel = {};
+
+ mockSelection = jasmine.createSpyObj('selection', ['select', 'get']);
+
+ mockSwimlane.domainObject = jasmine.createSpyObj(
+ 'domainObject',
+ [ 'getCapability', 'getModel' ]
+ );
+
+ mockCapabilities.mutation = jasmine.createSpyObj(
+ 'mutation',
+ ['mutate']
+ );
+ mockCapabilities.persistence = jasmine.createSpyObj(
+ 'persistence',
+ ['persist']
+ );
+ mockCapabilities.type = jasmine.createSpyObj(
+ 'type',
+ ['instanceOf']
+ );
+ mockPromise = jasmine.createSpyObj('promise', ['then']);
+
+ mockSwimlane.domainObject.getCapability.andCallFake(function (c) {
+ return mockCapabilities[c];
+ });
+ mockSwimlane.domainObject.getModel.andReturn(testModel);
+
+ mockCapabilities.type.instanceOf.andReturn(true);
+ mockCapabilities.mutation.mutate.andReturn(mockPromise);
+
+ decorator = new TimelineSwimlaneDecorator(
+ mockSwimlane,
+ mockSelection
+ );
+ });
+
+ it("returns the same object instance", function () {
+ // Decoration should occur in-place
+ expect(decorator).toBe(mockSwimlane);
+ });
+
+ it("adds a 'modes' getter-setter to activities", function () {
+ expect(mockSwimlane.modes).toEqual(jasmine.any(Function));
+ expect(mockCapabilities.type.instanceOf)
+ .toHaveBeenCalledWith('activity');
+ });
+
+ it("adds a 'link' getter-setter to activities", function () {
+ expect(mockSwimlane.link).toEqual(jasmine.any(Function));
+ expect(mockCapabilities.type.instanceOf)
+ .toHaveBeenCalledWith('activity');
+ });
+
+ it("gets modes from the domain object model", function () {
+ testModel.relationships = { modes: ['a', 'b', 'c'] };
+ expect(decorator.modes()).toEqual(['a', 'b', 'c']);
+ testModel.relationships = { modes: ['x', 'y', 'z'] };
+ expect(decorator.modes()).toEqual(['x', 'y', 'z']);
+ // Verify that it worked as a getter
+ expect(mockCapabilities.mutation.mutate)
+ .not.toHaveBeenCalled();
+ });
+
+ it("gets links from the domain object model", function () {
+ testModel.link = "http://www.nasa.gov";
+ expect(decorator.link()).toEqual("http://www.nasa.gov");
+ // Verify that it worked as a getter
+ expect(mockCapabilities.mutation.mutate)
+ .not.toHaveBeenCalled();
+ });
+
+ it("mutates modes when used as a setter", function () {
+ decorator.modes(['abc', 'xyz']);
+ expect(mockCapabilities.mutation.mutate)
+ .toHaveBeenCalledWith(jasmine.any(Function));
+ mockCapabilities.mutation.mutate.mostRecentCall.args[0](testModel);
+ expect(testModel.relationships.modes).toEqual(['abc', 'xyz']);
+
+ // Verify that persistence is called when promise resolves
+ expect(mockCapabilities.persistence.persist).not.toHaveBeenCalled();
+ mockPromise.then.mostRecentCall.args[0]();
+ expect(mockCapabilities.persistence.persist).toHaveBeenCalled();
+ });
+
+ it("mutates modes when used as a setter", function () {
+ decorator.link("http://www.noaa.gov");
+ expect(mockCapabilities.mutation.mutate)
+ .toHaveBeenCalledWith(jasmine.any(Function));
+ mockCapabilities.mutation.mutate.mostRecentCall.args[0](testModel);
+ expect(testModel.link).toEqual("http://www.noaa.gov");
+
+ // Verify that persistence is called when promise resolves
+ expect(mockCapabilities.persistence.persist).not.toHaveBeenCalled();
+ mockPromise.then.mostRecentCall.args[0]();
+ expect(mockCapabilities.persistence.persist).toHaveBeenCalled();
+ });
+
+ it("does not provide a 'remove' method with no parent", function () {
+ expect(decorator.remove).not.toEqual(jasmine.any(Function));
+ });
+
+ it("fires the 'remove' action when remove is called", function () {
+ var mockChild = jasmine.createSpyObj(
+ 'childObject',
+ [ 'getCapability', 'getModel' ]
+ ),
+ mockAction = jasmine.createSpyObj(
+ 'action',
+ [ 'perform' ]
+ );
+
+ mockChild.getCapability.andCallFake(function (c) {
+ return c === 'action' ? mockAction : undefined;
+ });
+
+ // Create a child swimlane; it should have a remove action
+ new TimelineSwimlaneDecorator({
+ domainObject: mockChild,
+ parent: decorator,
+ depth: 1
+ }).remove();
+
+ expect(mockChild.getCapability).toHaveBeenCalledWith('action');
+ expect(mockAction.perform).toHaveBeenCalledWith('remove');
+ });
+
+ it("allows the swimlane to be selected", function () {
+ decorator.select();
+ expect(mockSelection.select).toHaveBeenCalledWith(decorator);
+ });
+
+ it("allows checking for swimlane selection state", function () {
+ expect(decorator.selected()).toBeFalsy();
+ mockSelection.get.andReturn(decorator);
+ expect(decorator.selected()).toBeTruthy();
+ });
+
+ });
+
+ }
+);
diff --git a/platform/features/timeline/test/controllers/swimlane/TimelineSwimlaneDropHandlerSpec.js b/platform/features/timeline/test/controllers/swimlane/TimelineSwimlaneDropHandlerSpec.js
new file mode 100644
index 0000000000..f9ce3d7174
--- /dev/null
+++ b/platform/features/timeline/test/controllers/swimlane/TimelineSwimlaneDropHandlerSpec.js
@@ -0,0 +1,194 @@
+/*****************************************************************************
+ * Open MCT Web, Copyright (c) 2009-2015, United States Government
+ * as represented by the Administrator of the National Aeronautics and Space
+ * Administration. All rights reserved.
+ *
+ * Open MCT Web is licensed under the Apache License, Version 2.0 (the
+ * "License"); you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ * http://www.apache.org/licenses/LICENSE-2.0.
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
+ * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
+ * License for the specific language governing permissions and limitations
+ * under the License.
+ *
+ * Open MCT Web includes source code licensed under additional open source
+ * licenses. See the Open Source Licenses file (LICENSES.md) included with
+ * this source code distribution or the Licensing information page available
+ * at runtime from the About dialog for additional information.
+ *****************************************************************************/
+/*global define,describe,it,expect,beforeEach,waitsFor,jasmine,window,afterEach*/
+
+define(
+ ['../../../src/controllers/swimlane/TimelineSwimlaneDropHandler'],
+ function (TimelineSwimlaneDropHandler) {
+ "use strict";
+
+ describe("A timeline's swimlane drop handler", function () {
+ var mockSwimlane,
+ mockOtherObject,
+ mockActionCapability,
+ mockPersistence,
+ handler;
+
+ beforeEach(function () {
+ mockSwimlane = jasmine.createSpyObj(
+ "swimlane",
+ [ "highlight", "highlightBottom" ]
+ );
+ // domainObject, idPath, children, expanded
+ mockSwimlane.domainObject = jasmine.createSpyObj(
+ "domainObject",
+ [ "getId", "getCapability", "useCapability", "hasCapability" ]
+ );
+ mockSwimlane.idPath = [ 'a', 'b' ];
+ mockSwimlane.children = [ {} ];
+ mockSwimlane.expanded = true;
+
+ mockSwimlane.parent = {};
+ mockSwimlane.parent.idPath = ['a'];
+ mockSwimlane.parent.domainObject = jasmine.createSpyObj(
+ "domainObject",
+ [ "getId", "getCapability", "useCapability", "hasCapability" ]
+ );
+ mockSwimlane.parent.children = [ mockSwimlane ];
+
+ mockSwimlane.children[0].domainObject = jasmine.createSpyObj(
+ "domainObject",
+ [ "getId", "getCapability", "useCapability", "hasCapability" ]
+ );
+
+
+ mockOtherObject = jasmine.createSpyObj(
+ "domainObject",
+ [ "getId", "getCapability", "useCapability", "hasCapability" ]
+ );
+ mockActionCapability = jasmine.createSpyObj("action", ["perform", "getActions"]);
+ mockPersistence = jasmine.createSpyObj("persistence", ["persist"]);
+
+ mockActionCapability.getActions.andReturn([{}]);
+ mockSwimlane.parent.domainObject.getId.andReturn('a');
+ mockSwimlane.domainObject.getId.andReturn('b');
+ mockSwimlane.children[0].domainObject.getId.andReturn('c');
+ mockOtherObject.getId.andReturn('d');
+
+ mockSwimlane.domainObject.getCapability.andCallFake(function (c) {
+ return {
+ action: mockActionCapability,
+ persistence: mockPersistence
+ }[c];
+ });
+ mockOtherObject.getCapability.andReturn(mockActionCapability);
+
+ mockSwimlane.domainObject.hasCapability.andReturn(true);
+
+ handler = new TimelineSwimlaneDropHandler(mockSwimlane);
+ });
+
+ it("disallows drop outside of edit mode", function () {
+ // Verify precondition
+ expect(handler.allowDropIn('d')).toBeTruthy();
+ expect(handler.allowDropAfter('d')).toBeTruthy();
+ // Act as if we're not in edit mode
+ mockSwimlane.domainObject.hasCapability.andReturn(false);
+ // Now, they should be disallowed
+ expect(handler.allowDropIn('d')).toBeFalsy();
+ expect(handler.allowDropAfter('d')).toBeFalsy();
+
+ // Verify that editor capability was really checked for
+ expect(mockSwimlane.domainObject.hasCapability)
+ .toHaveBeenCalledWith('editor');
+ });
+
+ it("disallows dropping of parents", function () {
+ expect(handler.allowDropIn('a')).toBeFalsy();
+ expect(handler.allowDropAfter('a')).toBeFalsy();
+ });
+
+ it("does not drop when no highlight state is present", function () {
+ // If there's no hover highlight, there's no drop allowed
+ handler.drop('d', mockOtherObject);
+ expect(mockOtherObject.getCapability)
+ .not.toHaveBeenCalled();
+ expect(mockSwimlane.domainObject.useCapability)
+ .not.toHaveBeenCalled();
+ expect(mockSwimlane.parent.domainObject.useCapability)
+ .not.toHaveBeenCalled();
+ });
+
+ it("inserts into when highlighted", function () {
+ var testModel = { composition: [ 'c' ] };
+ mockSwimlane.highlight.andReturn(true);
+ handler.drop('d');
+ // Should have mutated
+ expect(mockSwimlane.domainObject.useCapability)
+ .toHaveBeenCalledWith("mutation", jasmine.any(Function));
+ // Run the mutator
+ mockSwimlane.domainObject.useCapability.mostRecentCall
+ .args[1](testModel);
+ expect(testModel.composition).toEqual(['c', 'd']);
+ // Finally, should also have persisted
+ expect(mockPersistence.persist).toHaveBeenCalled();
+ });
+
+ it("removes objects before insertion, if provided", function () {
+ var testModel = { composition: [ 'c' ] };
+ mockSwimlane.highlight.andReturn(true);
+ handler.drop('d', mockOtherObject);
+ // Should have invoked a remove action
+ expect(mockActionCapability.perform)
+ .toHaveBeenCalledWith('remove');
+ // Verify that mutator still ran as expected
+ mockSwimlane.domainObject.useCapability.mostRecentCall
+ .args[1](testModel);
+ expect(testModel.composition).toEqual(['c', 'd']);
+ });
+
+ it("inserts after as a peer when highlighted at the bottom", function () {
+ var testModel = { composition: [ 'x', 'b', 'y' ] };
+ mockSwimlane.highlightBottom.andReturn(true);
+ mockSwimlane.expanded = false;
+ handler.drop('d');
+ // Should have mutated
+ expect(mockSwimlane.parent.domainObject.useCapability)
+ .toHaveBeenCalledWith("mutation", jasmine.any(Function));
+ // Run the mutator
+ mockSwimlane.parent.domainObject.useCapability.mostRecentCall
+ .args[1](testModel);
+ expect(testModel.composition).toEqual([ 'x', 'b', 'd', 'y']);
+ });
+
+ it("inserts into when highlighted at the bottom and expanded", function () {
+ var testModel = { composition: [ 'c' ] };
+ mockSwimlane.highlightBottom.andReturn(true);
+ mockSwimlane.expanded = true;
+ handler.drop('d');
+ // Should have mutated
+ expect(mockSwimlane.domainObject.useCapability)
+ .toHaveBeenCalledWith("mutation", jasmine.any(Function));
+ // Run the mutator
+ mockSwimlane.domainObject.useCapability.mostRecentCall
+ .args[1](testModel);
+ expect(testModel.composition).toEqual([ 'd', 'c' ]);
+ });
+
+ it("inserts after as a peer when highlighted at the bottom and childless", function () {
+ var testModel = { composition: [ 'x', 'b', 'y' ] };
+ mockSwimlane.highlightBottom.andReturn(true);
+ mockSwimlane.expanded = true;
+ mockSwimlane.children = [];
+ handler.drop('d');
+ // Should have mutated
+ expect(mockSwimlane.parent.domainObject.useCapability)
+ .toHaveBeenCalledWith("mutation", jasmine.any(Function));
+ // Run the mutator
+ mockSwimlane.parent.domainObject.useCapability.mostRecentCall
+ .args[1](testModel);
+ expect(testModel.composition).toEqual([ 'x', 'b', 'd', 'y']);
+ });
+
+ });
+ }
+);
\ No newline at end of file
diff --git a/platform/features/timeline/test/controllers/swimlane/TimelineSwimlanePopulatorSpec.js b/platform/features/timeline/test/controllers/swimlane/TimelineSwimlanePopulatorSpec.js
new file mode 100644
index 0000000000..02b2010def
--- /dev/null
+++ b/platform/features/timeline/test/controllers/swimlane/TimelineSwimlanePopulatorSpec.js
@@ -0,0 +1,156 @@
+/*****************************************************************************
+ * Open MCT Web, Copyright (c) 2009-2015, United States Government
+ * as represented by the Administrator of the National Aeronautics and Space
+ * Administration. All rights reserved.
+ *
+ * Open MCT Web is licensed under the Apache License, Version 2.0 (the
+ * "License"); you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ * http://www.apache.org/licenses/LICENSE-2.0.
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
+ * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
+ * License for the specific language governing permissions and limitations
+ * under the License.
+ *
+ * Open MCT Web includes source code licensed under additional open source
+ * licenses. See the Open Source Licenses file (LICENSES.md) included with
+ * this source code distribution or the Licensing information page available
+ * at runtime from the About dialog for additional information.
+ *****************************************************************************/
+/*global define,describe,it,expect,beforeEach,waitsFor,jasmine,window,afterEach*/
+
+define(
+ ['../../../src/controllers/swimlane/TimelineSwimlanePopulator'],
+ function (TimelineSwimlanePopulator) {
+ 'use strict';
+
+ describe("A Timeline swimlane populator", function () {
+ var mockLoader,
+ mockSelection,
+ testConfiguration,
+ mockDomainObject,
+ mockDomainObjects,
+ mockCallback,
+ populator;
+
+ function asPromise(value) {
+ return (value || {}).then ? value : {
+ then: function (callback) {
+ return asPromise(callback(value));
+ }
+ };
+ }
+
+ function makeMockDomainObject(id, composition) {
+ var mockDomainObject = jasmine.createSpyObj(
+ 'domainObject-' + id,
+ ['getId', 'getModel', 'getCapability', 'useCapability']
+ );
+
+ mockDomainObject.getId.andReturn(id);
+ mockDomainObject.getModel.andReturn({ composition: composition });
+ mockDomainObject.useCapability.andReturn(asPromise(false));
+
+ return mockDomainObject;
+ }
+
+ function subgraph(domainObject, objects) {
+ function lookupSubgraph(id) {
+ return subgraph(objects[id], objects);
+ }
+ return {
+ domainObject: domainObject,
+ composition: domainObject.getModel().composition
+ .map(lookupSubgraph)
+ };
+ }
+
+ beforeEach(function () {
+ mockLoader = jasmine.createSpyObj('objectLoader', ['load']);
+ mockDomainObject = makeMockDomainObject('a', ['b', 'c']);
+ mockDomainObjects = {
+ a: mockDomainObject,
+ b: makeMockDomainObject('b', ['d']),
+ c: makeMockDomainObject('c', ['e', 'f']),
+ d: makeMockDomainObject('d', []),
+ e: makeMockDomainObject('e', []),
+ f: makeMockDomainObject('f', [])
+ };
+ mockSelection = jasmine.createSpyObj(
+ 'selection',
+ ['get', 'select', 'proxy']
+ );
+ mockCallback = jasmine.createSpy('callback');
+
+ testConfiguration = {};
+
+ mockLoader.load.andReturn(asPromise(subgraph(
+ mockDomainObject,
+ mockDomainObjects
+ )));
+
+ populator = new TimelineSwimlanePopulator(
+ mockLoader,
+ testConfiguration,
+ mockSelection
+ );
+ });
+
+ it("uses the loader to find subgraph", function () {
+ populator.populate(mockDomainObject);
+ expect(mockLoader.load).toHaveBeenCalledWith(
+ mockDomainObject,
+ 'timespan'
+ );
+ });
+
+ it("provides a list of swimlanes", function () {
+ populator.populate(mockDomainObject);
+ // Ensure swimlane order matches depth-first search
+ expect(populator.get().map(function (swimlane) {
+ return swimlane.domainObject;
+ })).toEqual([
+ mockDomainObjects.a,
+ mockDomainObjects.b,
+ mockDomainObjects.d,
+ mockDomainObjects.c,
+ mockDomainObjects.e,
+ mockDomainObjects.f
+ ]);
+ });
+
+ it("clears swimlanes if no object is provided", function () {
+ populator.populate();
+ expect(populator.get()).toEqual([]);
+ });
+
+ it("preserves selection state when possible", function () {
+ // Repopulate swimlanes
+ populator.populate(mockDomainObject);
+
+ // Act as if something is already selected
+ mockSelection.get.andReturn(populator.get()[1]);
+
+ // Verify precondition
+ expect(mockSelection.select).not.toHaveBeenCalled();
+
+ // Repopulate swimlanes
+ populator.populate(mockDomainObject);
+
+ // Selection should have been preserved
+ expect(mockSelection.select).toHaveBeenCalled();
+ expect(mockSelection.select.mostRecentCall.args[0].domainObject)
+ .toEqual(mockDomainObjects.b);
+ });
+
+ it("exposes a selection proxy for the timeline", function () {
+ populator.populate(mockDomainObject);
+ expect(mockSelection.proxy).toHaveBeenCalled();
+ });
+
+
+ });
+ }
+);
\ No newline at end of file
diff --git a/platform/features/timeline/test/controllers/swimlane/TimelineSwimlaneSpec.js b/platform/features/timeline/test/controllers/swimlane/TimelineSwimlaneSpec.js
new file mode 100644
index 0000000000..7100e82c35
--- /dev/null
+++ b/platform/features/timeline/test/controllers/swimlane/TimelineSwimlaneSpec.js
@@ -0,0 +1,223 @@
+/*****************************************************************************
+ * Open MCT Web, Copyright (c) 2009-2015, United States Government
+ * as represented by the Administrator of the National Aeronautics and Space
+ * Administration. All rights reserved.
+ *
+ * Open MCT Web is licensed under the Apache License, Version 2.0 (the
+ * "License"); you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ * http://www.apache.org/licenses/LICENSE-2.0.
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
+ * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
+ * License for the specific language governing permissions and limitations
+ * under the License.
+ *
+ * Open MCT Web includes source code licensed under additional open source
+ * licenses. See the Open Source Licenses file (LICENSES.md) included with
+ * this source code distribution or the Licensing information page available
+ * at runtime from the About dialog for additional information.
+ *****************************************************************************/
+/*global define,describe,it,expect,beforeEach,waitsFor,jasmine,window,afterEach*/
+
+define(
+ ['../../../src/controllers/swimlane/TimelineSwimlane'],
+ function (TimelineSwimlane) {
+ 'use strict';
+
+ describe("A Timeline swimlane", function () {
+ var parent,
+ child,
+ mockParentObject,
+ mockChildObject,
+ mockAssigner,
+ mockActionCapability,
+ mockParentTimespan,
+ mockChildTimespan,
+ testConfiguration;
+
+ function asPromise(v) {
+ return { then: function (cb) { cb(v); } };
+ }
+
+ beforeEach(function () {
+ mockParentObject = jasmine.createSpyObj(
+ 'parent',
+ ['getId', 'getCapability', 'useCapability', 'getModel']
+ );
+ mockChildObject = jasmine.createSpyObj(
+ 'child',
+ ['getId', 'getCapability', 'useCapability', 'getModel']
+ );
+ mockAssigner = jasmine.createSpyObj(
+ 'assigner',
+ ['get', 'assign', 'release']
+ );
+ mockParentTimespan = jasmine.createSpyObj(
+ 'parentTimespan',
+ ['getStart', 'getEnd']
+ );
+ mockChildTimespan = jasmine.createSpyObj(
+ 'childTimespan',
+ ['getStart', 'getEnd']
+ );
+ mockActionCapability = jasmine.createSpyObj('action', ['perform']);
+
+ mockParentObject.getId.andReturn('test-parent');
+ mockParentObject.getCapability.andReturn(mockActionCapability);
+ mockParentObject.useCapability.andReturn(asPromise(mockParentTimespan));
+ mockParentObject.getModel.andReturn({ name: "Test Parent" });
+ mockChildObject.getModel.andReturn({ name: "Test Child" });
+ mockChildObject.useCapability.andReturn(asPromise(mockChildTimespan));
+
+ testConfiguration = { graph: {} };
+
+ parent = new TimelineSwimlane(
+ mockParentObject,
+ mockAssigner,
+ testConfiguration
+ );
+ child = new TimelineSwimlane(
+ mockChildObject,
+ mockAssigner,
+ testConfiguration,
+ parent
+ );
+ });
+
+ it("exposes its domain object", function () {
+ expect(parent.domainObject).toEqual(mockParentObject);
+ expect(child.domainObject).toEqual(mockChildObject);
+ });
+
+ it("exposes its depth", function () {
+ expect(parent.depth).toEqual(0);
+ expect(child.depth).toEqual(1);
+ expect(new TimelineSwimlane(mockParentObject, {}, {}, child).depth)
+ .toEqual(2);
+ });
+
+ it("exposes its path as readable text", function () {
+ var grandchild = new TimelineSwimlane(mockParentObject, {}, {}, child),
+ ggc = new TimelineSwimlane(mockParentObject, {}, {}, grandchild);
+
+ expect(parent.path).toEqual("");
+ expect(child.path).toEqual("");
+ expect(grandchild.path).toEqual("Test Child > ");
+ expect(ggc.path).toEqual("Test Child > Test Parent > ");
+ });
+
+ it("starts off expanded", function () {
+ expect(parent.expanded).toBeTruthy();
+ expect(child.expanded).toBeTruthy();
+ });
+
+ it("determines visibility based on parent expansion", function () {
+ parent.expanded = false;
+ expect(child.visible()).toBeFalsy();
+ parent.expanded = true;
+ expect(child.visible()).toBeTruthy();
+ });
+
+ it("is visible when it is the root of the timeline subgraph", function () {
+ expect(parent.visible()).toBeTruthy();
+ });
+
+ it("fires the Edit Properties action on request", function () {
+ parent.properties();
+ expect(mockParentObject.getCapability).toHaveBeenCalledWith('action');
+ expect(mockActionCapability.perform).toHaveBeenCalledWith('properties');
+ });
+
+ it("allows resource graph inclusion to be toggled", function () {
+ expect(testConfiguration.graph['test-parent']).toBeFalsy();
+ parent.toggleGraph();
+ expect(testConfiguration.graph['test-parent']).toBeTruthy();
+ parent.toggleGraph();
+ expect(testConfiguration.graph['test-parent']).toBeFalsy();
+ });
+
+ it("provides a getter-setter for graph inclusion", function () {
+ expect(testConfiguration.graph['test-parent']).toBeFalsy();
+ expect(parent.graph(true)).toBeTruthy();
+ expect(parent.graph()).toBeTruthy();
+ expect(testConfiguration.graph['test-parent']).toBeTruthy();
+ expect(parent.graph(false)).toBeFalsy();
+ expect(parent.graph()).toBeFalsy();
+ expect(testConfiguration.graph['test-parent']).toBeFalsy();
+ });
+
+ it("gets colors from the provided assigner", function () {
+ mockAssigner.get.andReturn("#ABCABC");
+ expect(parent.color()).toEqual("#ABCABC");
+ // Verify that id was passed, and no other interactions
+ expect(mockAssigner.get).toHaveBeenCalledWith('test-parent');
+ expect(mockAssigner.assign).not.toHaveBeenCalled();
+ expect(mockAssigner.release).not.toHaveBeenCalled();
+ });
+
+ it("allows colors to be set", function () {
+ parent.color("#F0000D");
+ expect(mockAssigner.assign).toHaveBeenCalledWith(
+ 'test-parent',
+ "#F0000D"
+ );
+ });
+
+ it("assigns colors when resource graph state is toggled", function () {
+ expect(mockAssigner.assign).not.toHaveBeenCalled();
+ parent.toggleGraph();
+ expect(mockAssigner.assign).toHaveBeenCalledWith('test-parent');
+ expect(mockAssigner.release).not.toHaveBeenCalled();
+ parent.toggleGraph();
+ expect(mockAssigner.release).toHaveBeenCalledWith('test-parent');
+ });
+
+ it("assigns colors when resource graph state is set", function () {
+ expect(mockAssigner.assign).not.toHaveBeenCalled();
+ parent.graph(true);
+ expect(mockAssigner.assign).toHaveBeenCalledWith('test-parent');
+ expect(mockAssigner.release).not.toHaveBeenCalled();
+ parent.graph(false);
+ expect(mockAssigner.release).toHaveBeenCalledWith('test-parent');
+ });
+
+ it("provides getter-setters for drag-drop highlights", function () {
+ expect(parent.highlight()).toBeFalsy();
+ parent.highlight(true);
+ expect(parent.highlight()).toBeTruthy();
+
+ expect(parent.highlightBottom()).toBeFalsy();
+ parent.highlightBottom(true);
+ expect(parent.highlightBottom()).toBeTruthy();
+ });
+
+ it("detects start/end violations", function () {
+ mockParentTimespan.getStart.andReturn(42);
+ mockParentTimespan.getEnd.andReturn(12321);
+
+ // First, start with a valid timespan
+ mockChildTimespan.getStart.andReturn(84);
+ mockChildTimespan.getEnd.andReturn(100);
+ expect(child.exceeded()).toBeFalsy();
+
+ // Start time violation
+ mockChildTimespan.getStart.andReturn(21);
+ expect(child.exceeded()).toBeTruthy();
+
+ // Now both in violation
+ mockChildTimespan.getEnd.andReturn(20000);
+ expect(child.exceeded()).toBeTruthy();
+
+ // And just the end
+ mockChildTimespan.getStart.andReturn(100);
+ expect(child.exceeded()).toBeTruthy();
+
+ // Now back to everything's-just-fine
+ mockChildTimespan.getEnd.andReturn(10000);
+ expect(child.exceeded()).toBeFalsy();
+ });
+ });
+ }
+);
\ No newline at end of file
diff --git a/platform/features/timeline/test/directives/MCTSwimlaneDragSpec.js b/platform/features/timeline/test/directives/MCTSwimlaneDragSpec.js
new file mode 100644
index 0000000000..48991b83ae
--- /dev/null
+++ b/platform/features/timeline/test/directives/MCTSwimlaneDragSpec.js
@@ -0,0 +1,97 @@
+/*****************************************************************************
+ * Open MCT Web, Copyright (c) 2009-2015, United States Government
+ * as represented by the Administrator of the National Aeronautics and Space
+ * Administration. All rights reserved.
+ *
+ * Open MCT Web is licensed under the Apache License, Version 2.0 (the
+ * "License"); you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ * http://www.apache.org/licenses/LICENSE-2.0.
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
+ * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
+ * License for the specific language governing permissions and limitations
+ * under the License.
+ *
+ * Open MCT Web includes source code licensed under additional open source
+ * licenses. See the Open Source Licenses file (LICENSES.md) included with
+ * this source code distribution or the Licensing information page available
+ * at runtime from the About dialog for additional information.
+ *****************************************************************************/
+/*global define,describe,it,expect,beforeEach,waitsFor,jasmine,window,afterEach*/
+
+define(
+ ['../../src/directives/MCTSwimlaneDrag', '../../src/directives/SwimlaneDragConstants'],
+ function (MCTSwimlaneDrag, SwimlaneDragConstants) {
+ "use strict";
+
+ describe("The mct-swimlane-drag directive", function () {
+ var mockDndService,
+ mockScope,
+ mockElement,
+ testAttrs,
+ handlers,
+ directive;
+
+ beforeEach(function () {
+ var scopeExprs = {
+ someTestExpr: "some swimlane"
+ };
+
+ handlers = {};
+
+ mockDndService = jasmine.createSpyObj(
+ 'dndService',
+ ['setData', 'getData', 'removeData']
+ );
+ mockScope = jasmine.createSpyObj('$scope', ['$eval']);
+ mockElement = jasmine.createSpyObj('element', ['on']);
+ testAttrs = { mctSwimlaneDrag: "someTestExpr" };
+
+ // Simulate evaluation of expressions in scope
+ mockScope.$eval.andCallFake(function (expr) {
+ return scopeExprs[expr];
+ });
+
+ directive = new MCTSwimlaneDrag(mockDndService);
+
+ // Run the link function, then capture the event handlers
+ // for testing.
+ directive.link(mockScope, mockElement, testAttrs);
+
+ mockElement.on.calls.forEach(function (call) {
+ handlers[call.args[0]] = call.args[1];
+ });
+
+ });
+
+ it("is available as an attribute", function () {
+ expect(directive.restrict).toEqual("A");
+ });
+
+ it("exposes the swimlane when dragging starts", function () {
+ // Verify precondition
+ expect(mockDndService.setData).not.toHaveBeenCalled();
+ // Start a drag
+ handlers.dragstart();
+ // Should have exposed the swimlane
+ expect(mockDndService.setData).toHaveBeenCalledWith(
+ SwimlaneDragConstants.TIMELINE_SWIMLANE_DRAG_TYPE,
+ "some swimlane"
+ );
+ });
+
+ it("clears the swimlane when dragging ends", function () {
+ // Verify precondition
+ expect(mockDndService.removeData).not.toHaveBeenCalled();
+ // Start a drag
+ handlers.dragend();
+ // Should have exposed the swimlane
+ expect(mockDndService.removeData).toHaveBeenCalledWith(
+ SwimlaneDragConstants.TIMELINE_SWIMLANE_DRAG_TYPE
+ );
+ });
+ });
+ }
+);
diff --git a/platform/features/timeline/test/directives/MCTSwimlaneDropSpec.js b/platform/features/timeline/test/directives/MCTSwimlaneDropSpec.js
new file mode 100644
index 0000000000..80eeb43a74
--- /dev/null
+++ b/platform/features/timeline/test/directives/MCTSwimlaneDropSpec.js
@@ -0,0 +1,168 @@
+/*****************************************************************************
+ * Open MCT Web, Copyright (c) 2009-2015, United States Government
+ * as represented by the Administrator of the National Aeronautics and Space
+ * Administration. All rights reserved.
+ *
+ * Open MCT Web is licensed under the Apache License, Version 2.0 (the
+ * "License"); you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ * http://www.apache.org/licenses/LICENSE-2.0.
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
+ * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
+ * License for the specific language governing permissions and limitations
+ * under the License.
+ *
+ * Open MCT Web includes source code licensed under additional open source
+ * licenses. See the Open Source Licenses file (LICENSES.md) included with
+ * this source code distribution or the Licensing information page available
+ * at runtime from the About dialog for additional information.
+ *****************************************************************************/
+/*global define,describe,it,expect,beforeEach,waitsFor,jasmine,window,afterEach*/
+
+define(
+ ['../../src/directives/MCTSwimlaneDrop'],
+ function (MCTSwimlaneDrop) {
+ "use strict";
+
+ var TEST_HEIGHT = 100,
+ TEST_TOP = 600;
+
+ describe("The mct-swimlane-drop directive", function () {
+ var mockDndService,
+ mockScope,
+ mockElement,
+ testAttrs,
+ mockSwimlane,
+ mockRealElement,
+ testEvent,
+ handlers,
+ directive;
+
+ function getterSetter(value) {
+ return function (newValue) {
+ return (value = (arguments.length > 0) ? newValue : value);
+ };
+ }
+
+ beforeEach(function () {
+ var scopeExprs = {};
+
+ handlers = {};
+
+ mockDndService = jasmine.createSpyObj(
+ 'dndService',
+ ['setData', 'getData', 'removeData']
+ );
+ mockScope = jasmine.createSpyObj('$scope', ['$eval']);
+ mockElement = jasmine.createSpyObj('element', ['on']);
+ testAttrs = { mctSwimlaneDrop: "mockSwimlane" };
+ mockSwimlane = jasmine.createSpyObj(
+ "swimlane",
+ [ "allowDropIn", "allowDropAfter", "drop", "highlight", "highlightBottom" ]
+ );
+ mockElement[0] = jasmine.createSpyObj(
+ "realElement",
+ [ "getBoundingClientRect" ]
+ );
+ mockElement[0].offsetHeight = TEST_HEIGHT;
+ mockElement[0].getBoundingClientRect.andReturn({ top: TEST_TOP });
+
+ // Simulate evaluation of expressions in scope
+ scopeExprs.mockSwimlane = mockSwimlane;
+ mockScope.$eval.andCallFake(function (expr) {
+ return scopeExprs[expr];
+ });
+
+
+ mockSwimlane.allowDropIn.andReturn(true);
+ mockSwimlane.allowDropAfter.andReturn(true);
+ // Simulate getter-setter behavior
+ mockSwimlane.highlight.andCallFake(getterSetter(false));
+ mockSwimlane.highlightBottom.andCallFake(getterSetter(false));
+
+
+
+ testEvent = {
+ pageY: TEST_TOP + TEST_HEIGHT / 10,
+ dataTransfer: { getData: jasmine.createSpy() },
+ preventDefault: jasmine.createSpy()
+ };
+
+ testEvent.dataTransfer.getData.andReturn('abc');
+ mockDndService.getData.andReturn({ domainObject: 'someDomainObject' });
+
+ directive = new MCTSwimlaneDrop(mockDndService);
+
+ // Run the link function, then capture the event handlers
+ // for testing.
+ directive.link(mockScope, mockElement, testAttrs);
+
+ mockElement.on.calls.forEach(function (call) {
+ handlers[call.args[0]] = call.args[1];
+ });
+
+ });
+
+ it("is available as an attribute", function () {
+ expect(directive.restrict).toEqual("A");
+ });
+
+ it("updates highlights on drag over", function () {
+ // Near the top
+ testEvent.pageY = TEST_TOP + TEST_HEIGHT / 10;
+
+ handlers.dragover(testEvent);
+
+ expect(mockSwimlane.highlight).toHaveBeenCalledWith(true);
+ expect(mockSwimlane.highlightBottom).toHaveBeenCalledWith(false);
+ });
+
+ it("updates bottom highlights on drag over", function () {
+ // Near the bottom
+ testEvent.pageY = TEST_TOP + TEST_HEIGHT - TEST_HEIGHT / 10;
+
+ handlers.dragover(testEvent);
+
+ expect(mockSwimlane.highlight).toHaveBeenCalledWith(false);
+ expect(mockSwimlane.highlightBottom).toHaveBeenCalledWith(true);
+ });
+
+ it("respects swimlane's allowDropIn response", function () {
+ // Near the top
+ testEvent.pageY = TEST_TOP + TEST_HEIGHT / 10;
+
+ mockSwimlane.allowDropIn.andReturn(false);
+
+ handlers.dragover(testEvent);
+
+ expect(mockSwimlane.highlight).toHaveBeenCalledWith(false);
+ expect(mockSwimlane.highlightBottom).toHaveBeenCalledWith(false);
+ });
+
+ it("respects swimlane's allowDropAfter response", function () {
+ // Near the top
+ testEvent.pageY = TEST_TOP + TEST_HEIGHT - TEST_HEIGHT / 10;
+
+ mockSwimlane.allowDropAfter.andReturn(false);
+
+ handlers.dragover(testEvent);
+
+ expect(mockSwimlane.highlight).toHaveBeenCalledWith(false);
+ expect(mockSwimlane.highlightBottom).toHaveBeenCalledWith(false);
+ });
+
+ it("notifies swimlane on drop", function () {
+ handlers.drop(testEvent);
+ expect(mockSwimlane.drop).toHaveBeenCalledWith('abc', 'someDomainObject');
+ });
+
+ it("clears highlights when drag leaves", function () {
+ handlers.dragleave();
+ expect(mockSwimlane.highlight).toHaveBeenCalledWith(false);
+ expect(mockSwimlane.highlightBottom).toHaveBeenCalledWith(false);
+ });
+ });
+ }
+);
diff --git a/platform/features/timeline/test/directives/SwimlaneDragConstantsSpec.js b/platform/features/timeline/test/directives/SwimlaneDragConstantsSpec.js
new file mode 100644
index 0000000000..42122647dc
--- /dev/null
+++ b/platform/features/timeline/test/directives/SwimlaneDragConstantsSpec.js
@@ -0,0 +1,36 @@
+/*****************************************************************************
+ * Open MCT Web, Copyright (c) 2009-2015, United States Government
+ * as represented by the Administrator of the National Aeronautics and Space
+ * Administration. All rights reserved.
+ *
+ * Open MCT Web is licensed under the Apache License, Version 2.0 (the
+ * "License"); you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ * http://www.apache.org/licenses/LICENSE-2.0.
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
+ * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
+ * License for the specific language governing permissions and limitations
+ * under the License.
+ *
+ * Open MCT Web includes source code licensed under additional open source
+ * licenses. See the Open Source Licenses file (LICENSES.md) included with
+ * this source code distribution or the Licensing information page available
+ * at runtime from the About dialog for additional information.
+ *****************************************************************************/
+/*global define,describe,it,expect,beforeEach,waitsFor,jasmine,window,afterEach*/
+
+define(
+ ['../../src/directives/SwimlaneDragConstants'],
+ function (SwimlaneDragConstants) {
+ "use strict";
+
+ describe("Timeline swimlane drag constants", function () {
+ it("define a custom type for swimlane drag-drop", function () {
+ expect(SwimlaneDragConstants.TIMELINE_SWIMLANE_DRAG_TYPE)
+ .toEqual(jasmine.any(String));
+ });
+ });
+ }
+);
diff --git a/platform/features/timeline/test/services/ObjectLoaderSpec.js b/platform/features/timeline/test/services/ObjectLoaderSpec.js
new file mode 100644
index 0000000000..57f646e08f
--- /dev/null
+++ b/platform/features/timeline/test/services/ObjectLoaderSpec.js
@@ -0,0 +1,157 @@
+/*****************************************************************************
+ * Open MCT Web, Copyright (c) 2009-2015, United States Government
+ * as represented by the Administrator of the National Aeronautics and Space
+ * Administration. All rights reserved.
+ *
+ * Open MCT Web is licensed under the Apache License, Version 2.0 (the
+ * "License"); you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ * http://www.apache.org/licenses/LICENSE-2.0.
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
+ * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
+ * License for the specific language governing permissions and limitations
+ * under the License.
+ *
+ * Open MCT Web includes source code licensed under additional open source
+ * licenses. See the Open Source Licenses file (LICENSES.md) included with
+ * this source code distribution or the Licensing information page available
+ * at runtime from the About dialog for additional information.
+ *****************************************************************************/
+/*global define,describe,it,expect,beforeEach,waitsFor,jasmine,window,afterEach*/
+
+define(
+ ['../../src/services/ObjectLoader'],
+ function (ObjectLoader) {
+ "use strict";
+
+ describe("The domain object loader", function () {
+ var mockQ,
+ mockCallback,
+ mockDomainObjects,
+ testCompositions,
+ objectLoader;
+
+ function asPromise(value) {
+ return (value || {}).then ? value : {
+ then: function (callback) {
+ return asPromise(callback(value));
+ }
+ };
+ }
+
+ function lookupObject(id) {
+ return mockDomainObjects[id];
+ }
+
+ function fullSubgraph(id) {
+ return {
+ domainObject: mockDomainObjects[id],
+ composition: (testCompositions[id] || [])
+ .map(fullSubgraph)
+ };
+ }
+
+ function addDomainObject(id, children, capabilities) {
+ var mockDomainObject = jasmine.createSpyObj(
+ 'object-' + id,
+ [ 'useCapability', 'hasCapability', 'getId' ]
+ );
+
+ mockDomainObject.getId.andReturn(id);
+ mockDomainObject.useCapability.andCallFake(function (c) {
+ return c === 'composition' ?
+ asPromise(children.map(lookupObject)) :
+ undefined;
+ });
+ mockDomainObject.hasCapability.andCallFake(function (c) {
+ return (capabilities.indexOf(c) !== -1) || (c === 'composition');
+ });
+ mockDomainObjects[id] = mockDomainObject;
+
+ testCompositions[id] = children;
+ }
+
+ beforeEach(function () {
+ mockQ = jasmine.createSpyObj('$q', [ 'when', 'all' ]);
+ mockCallback = jasmine.createSpy('callback');
+ mockDomainObjects = {};
+ testCompositions = {};
+
+ // Provide subset of q's actual behavior which we
+ // expect object loader to really need
+ mockQ.when.andCallFake(asPromise);
+ mockQ.all.andCallFake(function (values) {
+ var result = [];
+ function addResult(v) { result.push(v); }
+ function promiseResult(v) { asPromise(v).then(addResult); }
+ values.forEach(promiseResult);
+ return asPromise(result);
+ });
+
+ // Populate some mock domain objects
+ addDomainObject('a', ['b', 'c', 'd'], ['test']);
+ addDomainObject('b', ['c', 'd', 'ba'], []);
+ addDomainObject('c', ['ca'], ['test']);
+ addDomainObject('d', [], ['test']);
+ addDomainObject('ba', [], ['test']);
+ addDomainObject('ca', [], ['test']);
+
+ objectLoader = new ObjectLoader(mockQ);
+ });
+
+
+
+ it("loads sub-graphs of composition hierarchy", function () {
+ objectLoader.load(mockDomainObjects.a).then(mockCallback);
+ // Should have loaded full graph
+ expect(mockCallback).toHaveBeenCalledWith(fullSubgraph('a'));
+ });
+
+ it("filters based on capabilities, if requested", function () {
+ objectLoader.load(mockDomainObjects.a, 'test')
+ .then(mockCallback);
+ // Should have pruned 'b'
+ expect(mockCallback).toHaveBeenCalledWith({
+ domainObject: mockDomainObjects.a,
+ composition: [
+ fullSubgraph('c'),
+ fullSubgraph('d')
+ ]
+ });
+ });
+
+ it("filters with a function, if requested", function () {
+ function shortName(domainObject) {
+ return domainObject.getId().length === 1;
+ }
+ objectLoader.load(mockDomainObjects.a, shortName)
+ .then(mockCallback);
+ // Should have pruned 'ba' and 'ca'
+ expect(mockCallback).toHaveBeenCalledWith({
+ domainObject: mockDomainObjects.a,
+ composition: [
+ {
+ domainObject: mockDomainObjects.b,
+ composition: [
+ {
+ domainObject: mockDomainObjects.c,
+ composition: []
+ },
+ fullSubgraph('d')
+ ]
+ },
+ {
+ domainObject: mockDomainObjects.c,
+ composition: []
+ },
+ fullSubgraph('d')
+ ]
+ });
+ });
+
+ });
+
+ }
+);
\ No newline at end of file
diff --git a/platform/features/timeline/test/suite.json b/platform/features/timeline/test/suite.json
new file mode 100644
index 0000000000..e5028ef25d
--- /dev/null
+++ b/platform/features/timeline/test/suite.json
@@ -0,0 +1,50 @@
+[
+ "TimelineConstants",
+ "TimelineFormatter",
+
+ "capabilities/ActivityTimespan",
+ "capabilities/ActivityTimespanCapability",
+ "capabilities/ActivityUtilization",
+ "capabilities/CostCapability",
+ "capabilities/GraphCapability",
+ "capabilities/CumulativeGraph",
+ "capabilities/ResourceGraph",
+ "capabilities/TimelineTimespan",
+ "capabilities/TimelineTimespanCapability",
+ "capabilities/TimelineUtilization",
+ "capabilities/UtilizationCapability",
+
+ "controllers/ActivityModeValuesController",
+ "controllers/TimelineController",
+ "controllers/TimelineGanttController",
+ "controllers/TimelineGraphController",
+ "controllers/TimelineTableController",
+ "controllers/TimelineTickController",
+ "controllers/TimelineZoomController",
+ "controllers/TimelineDateTimeController",
+
+ "controllers/drag/TimelineDragHandler",
+ "controllers/drag/TimelineDragHandleFactory",
+ "controllers/drag/TimelineDragPopulator",
+ "controllers/drag/TimelineSnapHandler",
+ "controllers/drag/TimelineStartHandle",
+ "controllers/drag/TimelineMoveHandle",
+ "controllers/drag/TimelineEndHandle",
+
+ "controllers/graph/TimelineGraph",
+ "controllers/graph/TimelineGraphPopulator",
+ "controllers/graph/TimelineGraphRenderer",
+
+ "controllers/swimlane/TimelineColorAssigner",
+ "controllers/swimlane/TimelineProxy",
+ "controllers/swimlane/TimelineSwimlane",
+ "controllers/swimlane/TimelineSwimlaneDecorator",
+ "controllers/swimlane/TimelineSwimlaneDropHandler",
+ "controllers/swimlane/TimelineSwimlanePopulator",
+
+ "directives/SwimlaneDragConstants",
+ "directives/MCTSwimlaneDrag",
+ "directives/MCTSwimlaneDrop",
+
+ "services/ObjectLoader"
+]
diff --git a/test-main.js b/test-main.js
index 18b5f2a0d4..77f6bb4d86 100644
--- a/test-main.js
+++ b/test-main.js
@@ -44,10 +44,17 @@ require.config({
paths: {
'es6-promise': 'platform/framework/lib/es6-promise-2.0.0.min',
- 'moment-duration-format': 'warp/clock/lib/moment-duration-format',
+ 'moment': 'platform/telemetry/lib/moment.min',
+ 'moment-duration-format': 'platform/features/clock/lib/moment-duration-format',
'uuid': 'platform/commonUI/browse/lib/uuid'
},
+ shim: {
+ 'moment-duration-format': {
+ deps: [ 'moment' ]
+ }
+ },
+
// dynamically load all test files
deps: allTestFiles,