From 49560698f67a53d117d4fcd51050f3681e0fba32 Mon Sep 17 00:00:00 2001 From: Pete Richards Date: Tue, 17 Jan 2017 09:59:00 -0800 Subject: [PATCH 01/90] [Reorg] Make timeline-specific chart directive Make a separate chart directive for drawing resource graphs in timelines. This is in preparation for a new plot bundle which will make a large number of changes to the drawing API to support newly requested features. By separating code, there will be no impact to the timeline when the new plot features are added. --- docs/src/guide/index.md | 35 --- platform/features/plot/README.md | 37 +++ platform/features/timeline/bundle.js | 10 + .../res/templates/resource-graphs.html | 4 +- .../timeline/src/chart/Canvas2DChart.js | 117 ++++++++ .../features/timeline/src/chart/GLChart.js | 160 +++++++++++ .../timeline/src/chart/MCTTimelineChart.js | 250 ++++++++++++++++++ .../src/controllers/graph/TimelineGraph.js | 2 +- .../graph/TimelineGraphRenderer.js | 4 +- .../timeline/test/chart/Canvas2DChartSpec.js | 95 +++++++ .../timeline/test/chart/GLChartSpec.js | 143 ++++++++++ .../test/chart/MCTTimelineChartSpec.js | 216 +++++++++++++++ 12 files changed, 1033 insertions(+), 40 deletions(-) create mode 100644 platform/features/plot/README.md create mode 100644 platform/features/timeline/src/chart/Canvas2DChart.js create mode 100644 platform/features/timeline/src/chart/GLChart.js create mode 100644 platform/features/timeline/src/chart/MCTTimelineChart.js create mode 100644 platform/features/timeline/test/chart/Canvas2DChartSpec.js create mode 100644 platform/features/timeline/test/chart/GLChartSpec.js create mode 100644 platform/features/timeline/test/chart/MCTTimelineChartSpec.js diff --git a/docs/src/guide/index.md b/docs/src/guide/index.md index c4d52c9501..4e8684fc32 100644 --- a/docs/src/guide/index.md +++ b/docs/src/guide/index.md @@ -1339,41 +1339,6 @@ are supported: Open MCT defines several Angular directives that are intended for use both internally within the platform, and by plugins. -## Chart - -The `mct-chart` directive is used to support drawing of simple charts. It is -present to support the Plot view, and its functionality is limited to the -functionality that is relevant for that view. - -This directive is used at the element level and takes one attribute, `draw` -which is an Angular expression which will should evaluate to a drawing object. -This drawing object should contain the following properties: - -* `dimensions`: The size, in logical coordinates, of the chart area. A -two-element array or numbers. -* `origin`: The position, in logical coordinates, of the lower-left corner of -the chart area. A two-element array or numbers. -* `lines`: An array of lines (e.g. as a plot line) to draw, where each line is -expressed as an object containing: - * `buffer`: A Float32Array containing points in the line, in logical - coordinates, in sequential x,y pairs. - * `color`: The color of the line, as a four-element RGBA array, where - each element is a number in the range of 0.0-1.0. - * `points`: The number of points in the line. -* `boxes`: An array of rectangles to draw in the chart area. Each is an object -containing: - * `start`: The first corner of the rectangle, as a two-element array of - numbers, in logical coordinates. - * `end`: The opposite corner of the rectangle, as a two-element array of - numbers, in logical coordinates. color : The color of the line, as a - four-element RGBA array, where each element is a number in the range of - 0.0-1.0. - -While `mct-chart` is intended to support plots specifically, it does perform -some useful management of canvas objects (e.g. choosing between WebGL and Canvas -2D APIs for drawing based on browser support) so its usage is recommended when -its supported drawing primitives are sufficient for other charting tasks. - ## Container The `mct-container` is similar to the `mct-include` directive insofar as it allows diff --git a/platform/features/plot/README.md b/platform/features/plot/README.md new file mode 100644 index 0000000000..a4a6537fe1 --- /dev/null +++ b/platform/features/plot/README.md @@ -0,0 +1,37 @@ +# Plot README + +## Chart + +The `mct-chart` directive is used to support drawing of simple charts. It is +present to support the Plot view, and its functionality is limited to the +functionality that is relevant for that view. + +This directive is used at the element level and takes one attribute, `draw` +which is an Angular expression which will should evaluate to a drawing object. +This drawing object should contain the following properties: + +* `dimensions`: The size, in logical coordinates, of the chart area. A +two-element array or numbers. +* `origin`: The position, in logical coordinates, of the lower-left corner of +the chart area. A two-element array or numbers. +* `lines`: An array of lines (e.g. as a plot line) to draw, where each line is +expressed as an object containing: + * `buffer`: A Float32Array containing points in the line, in logical + coordinates, in sequential x,y pairs. + * `color`: The color of the line, as a four-element RGBA array, where + each element is a number in the range of 0.0-1.0. + * `points`: The number of points in the line. +* `boxes`: An array of rectangles to draw in the chart area. Each is an object +containing: + * `start`: The first corner of the rectangle, as a two-element array of + numbers, in logical coordinates. + * `end`: The opposite corner of the rectangle, as a two-element array of + numbers, in logical coordinates. color : The color of the line, as a + four-element RGBA array, where each element is a number in the range of + 0.0-1.0. + +While `mct-chart` is intended to support plots specifically, it does perform +some useful management of canvas objects (e.g. choosing between WebGL and Canvas +2D APIs for drawing based on browser support) so its usage is recommended when +its supported drawing primitives are sufficient for other charting tasks. + diff --git a/platform/features/timeline/bundle.js b/platform/features/timeline/bundle.js index 42b3c948f9..6ec0e2006b 100644 --- a/platform/features/timeline/bundle.js +++ b/platform/features/timeline/bundle.js @@ -38,6 +38,7 @@ define([ "./src/directives/MCTSwimlaneDrop", "./src/directives/MCTSwimlaneDrag", "./src/services/ObjectLoader", + "./src/chart/MCTTimelineChart", "text!./res/templates/values.html", "text!./res/templates/timeline.html", "text!./res/templates/activity-gantt.html", @@ -67,6 +68,7 @@ define([ MCTSwimlaneDrop, MCTSwimlaneDrag, ObjectLoader, + MCTTimelineChart, valuesTemplate, timelineTemplate, activityGanttTemplate, @@ -556,6 +558,14 @@ define([ "depends": [ "dndService" ] + }, + { + "key": "mctTimelineChart", + "implementation": MCTTimelineChart, + "depends": [ + "$interval", + "$log" + ] } ], "services": [ diff --git a/platform/features/timeline/res/templates/resource-graphs.html b/platform/features/timeline/res/templates/resource-graphs.html index 31a139ea16..51d2f6a053 100644 --- a/platform/features/timeline/res/templates/resource-graphs.html +++ b/platform/features/timeline/res/templates/resource-graphs.html @@ -22,7 +22,7 @@
- +
-
\ No newline at end of file + diff --git a/platform/features/timeline/src/chart/Canvas2DChart.js b/platform/features/timeline/src/chart/Canvas2DChart.js new file mode 100644 index 0000000000..e4a8c7fe43 --- /dev/null +++ b/platform/features/timeline/src/chart/Canvas2DChart.js @@ -0,0 +1,117 @@ +/***************************************************************************** + * Open MCT, Copyright (c) 2014-2016, United States Government + * as represented by the Administrator of the National Aeronautics and Space + * Administration. All rights reserved. + * + * Open MCT is licensed under the Apache License, Version 2.0 (the + * "License"); you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * http://www.apache.org/licenses/LICENSE-2.0. + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT + * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the + * License for the specific language governing permissions and limitations + * under the License. + * + * Open MCT includes source code licensed under additional open source + * licenses. See the Open Source Licenses file (LICENSES.md) included with + * this source code distribution or the Licensing information page available + * at runtime from the About dialog for additional information. + *****************************************************************************/ + +define( + [], + function () { + + /** + * Create a new chart which uses Canvas's 2D API for rendering. + * + * @memberof platform/features/plot + * @constructor + * @implements {platform/features/plot.Chart} + * @param {CanvasElement} canvas the canvas object to render upon + * @throws {Error} an error is thrown if Canvas's 2D API is unavailable. + */ + function Canvas2DChart(canvas) { + this.canvas = canvas; + this.c2d = canvas.getContext('2d'); + this.width = canvas.width; + this.height = canvas.height; + this.dimensions = [this.width, this.height]; + this.origin = [0, 0]; + + if (!this.c2d) { + throw new Error("Canvas 2d API unavailable."); + } + } + + // Convert from logical to physical x coordinates + Canvas2DChart.prototype.x = function (v) { + return ((v - this.origin[0]) / this.dimensions[0]) * this.width; + }; + + // Convert from logical to physical y coordinates + Canvas2DChart.prototype.y = function (v) { + return this.height - + ((v - this.origin[1]) / this.dimensions[1]) * this.height; + }; + + // Set the color to be used for drawing operations + Canvas2DChart.prototype.setColor = function (color) { + var mappedColor = color.map(function (c, i) { + return i < 3 ? Math.floor(c * 255) : (c); + }).join(','); + this.c2d.strokeStyle = "rgba(" + mappedColor + ")"; + this.c2d.fillStyle = "rgba(" + mappedColor + ")"; + }; + + + Canvas2DChart.prototype.clear = function () { + var canvas = this.canvas; + this.width = canvas.width; + this.height = canvas.height; + this.c2d.clearRect(0, 0, this.width, this.height); + }; + + Canvas2DChart.prototype.setDimensions = function (newDimensions, newOrigin) { + this.dimensions = newDimensions; + this.origin = newOrigin; + }; + + Canvas2DChart.prototype.drawLine = function (buf, color, points) { + var i; + + this.setColor(color); + + // Configure context to draw two-pixel-thick lines + this.c2d.lineWidth = 2; + + // Start a new path... + if (buf.length > 1) { + this.c2d.beginPath(); + this.c2d.moveTo(this.x(buf[0]), this.y(buf[1])); + } + + // ...and add points to it... + for (i = 2; i < points * 2; i = i + 2) { + this.c2d.lineTo(this.x(buf[i]), this.y(buf[i + 1])); + } + + // ...before finally drawing it. + this.c2d.stroke(); + }; + + Canvas2DChart.prototype.drawSquare = function (min, max, color) { + var x1 = this.x(min[0]), + y1 = this.y(min[1]), + w = this.x(max[0]) - x1, + h = this.y(max[1]) - y1; + + this.setColor(color); + this.c2d.fillRect(x1, y1, w, h); + }; + + return Canvas2DChart; + } +); diff --git a/platform/features/timeline/src/chart/GLChart.js b/platform/features/timeline/src/chart/GLChart.js new file mode 100644 index 0000000000..0ca7776171 --- /dev/null +++ b/platform/features/timeline/src/chart/GLChart.js @@ -0,0 +1,160 @@ +/***************************************************************************** + * Open MCT, Copyright (c) 2014-2016, United States Government + * as represented by the Administrator of the National Aeronautics and Space + * Administration. All rights reserved. + * + * Open MCT is licensed under the Apache License, Version 2.0 (the + * "License"); you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * http://www.apache.org/licenses/LICENSE-2.0. + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT + * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the + * License for the specific language governing permissions and limitations + * under the License. + * + * Open MCT includes source code licensed under additional open source + * licenses. See the Open Source Licenses file (LICENSES.md) included with + * this source code distribution or the Licensing information page available + * at runtime from the About dialog for additional information. + *****************************************************************************/ + +/** + * Module defining GLPlot. Created by vwoeltje on 11/12/14. + */ +define( + [], + function () { + + // WebGL shader sources (for drawing plain colors) + var FRAGMENT_SHADER = [ + "precision mediump float;", + "uniform vec4 uColor;", + "void main(void) {", + "gl_FragColor = uColor;", + "}" + ].join('\n'), + VERTEX_SHADER = [ + "attribute vec2 aVertexPosition;", + "uniform vec2 uDimensions;", + "uniform vec2 uOrigin;", + "void main(void) {", + "gl_Position = vec4(2.0 * ((aVertexPosition - uOrigin) / uDimensions) - vec2(1,1), 0, 1);", + "}" + ].join('\n'); + + /** + * Create a new chart which uses WebGL for rendering. + * + * @memberof platform/features/plot + * @constructor + * @implements {platform/features/plot.Chart} + * @param {CanvasElement} canvas the canvas object to render upon + * @throws {Error} an error is thrown if WebGL is unavailable. + */ + function GLChart(canvas) { + var gl = canvas.getContext("webgl", { preserveDrawingBuffer: true }) || + canvas.getContext("experimental-webgl", { preserveDrawingBuffer: true }), + vertexShader, + fragmentShader, + program, + aVertexPosition, + uColor, + uDimensions, + uOrigin; + + // Ensure a context was actually available before proceeding + if (!gl) { + throw new Error("WebGL unavailable."); + } + + // Initialize shaders + vertexShader = gl.createShader(gl.VERTEX_SHADER); + gl.shaderSource(vertexShader, VERTEX_SHADER); + gl.compileShader(vertexShader); + fragmentShader = gl.createShader(gl.FRAGMENT_SHADER); + gl.shaderSource(fragmentShader, FRAGMENT_SHADER); + gl.compileShader(fragmentShader); + + // Assemble vertex/fragment shaders into programs + program = gl.createProgram(); + gl.attachShader(program, vertexShader); + gl.attachShader(program, fragmentShader); + gl.linkProgram(program); + gl.useProgram(program); + + // Get locations for attribs/uniforms from the + // shader programs (to pass values into shaders at draw-time) + aVertexPosition = gl.getAttribLocation(program, "aVertexPosition"); + uColor = gl.getUniformLocation(program, "uColor"); + uDimensions = gl.getUniformLocation(program, "uDimensions"); + uOrigin = gl.getUniformLocation(program, "uOrigin"); + gl.enableVertexAttribArray(aVertexPosition); + + // Create a buffer to holds points which will be drawn + this.buffer = gl.createBuffer(); + + // Use a line width of 2.0 for legibility + gl.lineWidth(2.0); + + // Enable blending, for smoothness + gl.enable(gl.BLEND); + gl.blendFunc(gl.SRC_ALPHA, gl.ONE_MINUS_SRC_ALPHA); + + this.gl = gl; + this.aVertexPosition = aVertexPosition; + this.uColor = uColor; + this.uDimensions = uDimensions; + this.uOrigin = uOrigin; + } + + // Utility function to handle drawing of a buffer; + // drawType will determine whether this is a box, line, etc. + GLChart.prototype.doDraw = function (drawType, buf, color, points) { + var gl = this.gl; + gl.bindBuffer(gl.ARRAY_BUFFER, this.buffer); + gl.bufferData(gl.ARRAY_BUFFER, buf, gl.DYNAMIC_DRAW); + gl.vertexAttribPointer(this.aVertexPosition, 2, gl.FLOAT, false, 0, 0); + gl.uniform4fv(this.uColor, color); + gl.drawArrays(drawType, 0, points); + }; + + GLChart.prototype.clear = function () { + var gl = this.gl; + + // Set the viewport size; note that we use the width/height + // that our WebGL context reports, which may be lower + // resolution than the canvas we requested. + gl.viewport( + 0, + 0, + gl.drawingBufferWidth, + gl.drawingBufferHeight + ); + gl.clear(gl.COLOR_BUFFER_BIT + gl.DEPTH_BUFFER_BIT); + }; + + + GLChart.prototype.setDimensions = function (dimensions, origin) { + var gl = this.gl; + if (dimensions && dimensions.length > 0 && + origin && origin.length > 0) { + gl.uniform2fv(this.uDimensions, dimensions); + gl.uniform2fv(this.uOrigin, origin); + } + }; + + GLChart.prototype.drawLine = function (buf, color, points) { + this.doDraw(this.gl.LINE_STRIP, buf, color, points); + }; + + GLChart.prototype.drawSquare = function (min, max, color) { + this.doDraw(this.gl.TRIANGLE_FAN, new Float32Array( + min.concat([min[0], max[1]]).concat(max).concat([max[0], min[1]]) + ), color, 4); + }; + + return GLChart; + } +); diff --git a/platform/features/timeline/src/chart/MCTTimelineChart.js b/platform/features/timeline/src/chart/MCTTimelineChart.js new file mode 100644 index 0000000000..67bd0b4a6d --- /dev/null +++ b/platform/features/timeline/src/chart/MCTTimelineChart.js @@ -0,0 +1,250 @@ +/***************************************************************************** + * Open MCT, Copyright (c) 2014-2016, United States Government + * as represented by the Administrator of the National Aeronautics and Space + * Administration. All rights reserved. + * + * Open MCT is licensed under the Apache License, Version 2.0 (the + * "License"); you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * http://www.apache.org/licenses/LICENSE-2.0. + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT + * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the + * License for the specific language governing permissions and limitations + * under the License. + * + * Open MCT includes source code licensed under additional open source + * licenses. See the Open Source Licenses file (LICENSES.md) included with + * this source code distribution or the Licensing information page available + * at runtime from the About dialog for additional information. + *****************************************************************************/ + +/** + * Module defining MCTTimelineChart. Created by vwoeltje on 11/12/14. + */ +define( + ["./GLChart", "./Canvas2DChart"], + function (GLChart, Canvas2DChart) { + + var TEMPLATE = ""; + + /** + * The mct-timeline-chart directive provides a canvas element which can be + * drawn upon, to support Plot view and similar visualizations. + * + * This directive takes one attribute, "draw", which is an Angular + * expression which will be two-way bound to a drawing object. This + * drawing object should contain: + * + * * `dimensions`: An object describing the logical bounds of the + * drawable area, containing two fields: + * * `origin`: The position, in logical coordinates, of the + * lower-left corner of the chart area. A two-element array. + * * `dimensions`: A two-element array containing the width + * and height of the chart area, in logical coordinates. + * * `lines`: An array of lines to be drawn, where each line is + * expressed as an object containing: + * * `buffer`: A Float32Array containing points in the line, + * in logical coordinate, in sequential x/y pairs. + * * `color`: The color of the line, as a four-element RGBA + * array, where each element is in the range of 0.0-1.0 + * * `points`: The number of points in the line. + * * `boxes`: An array of rectangles to draw in the chart area + * (used for marquee zoom). Each is an object containing: + * * `start`: The first corner of the rectangle (as a two-element + * array, logical coordinates) + * * `end`: The opposite corner of the rectangle (again, as a + * two-element array) + * * `color`: The color of the box, as a four-element RGBA + * array, where each element is in the range of 0.0-1.0 + * + * @memberof platform/features/plot + * @constructor + */ + function MCTTimelineChart($interval, $log) { + // Get an underlying chart implementation + function getChart(Charts, canvas) { + // Try the first available option... + var Chart = Charts[0]; + + // This function recursively try-catches all options; + // if these all fail, issue a warning. + if (!Chart) { + $log.warn("Cannot initialize mct-timeline-chart."); + return undefined; + } + + // Try first option; if it fails, try remaining options + try { + return new Chart(canvas); + } catch (e) { + $log.warn([ + "Could not instantiate chart", + Chart.name, + ";", + e.message + ].join(" ")); + + return getChart(Charts.slice(1), canvas); + } + } + + function linkChart(scope, element) { + var canvas = element.find("canvas")[0], + activeInterval, + chart; + + // Handle drawing, based on contents of the "draw" object + // in scope + function doDraw(draw) { + // Ensure canvas context has same resolution + // as canvas element + canvas.width = canvas.offsetWidth; + canvas.height = canvas.offsetHeight; + + // Clear previous contents + chart.clear(); + + // Nothing to draw if no draw object defined + if (!draw) { + return; + } + + // Set logical boundaries for the chart + chart.setDimensions( + draw.dimensions || [1, 1], + draw.origin || [0, 0] + ); + + // Draw line segments + (draw.lines || []).forEach(function (line) { + chart.drawLine( + line.buffer, + line.color, + line.points + ); + }); + + // Draw boxes (e.g. marquee zoom rect) + (draw.boxes || []).forEach(function (box) { + chart.drawSquare( + box.start, + box.end, + box.color + ); + }); + + } + + // Issue a drawing call, if-and-only-if canvas size + // has changed. This will be called on a timer, since + // there is no event to depend on. + function drawIfResized() { + if (canvas.width !== canvas.offsetWidth || + canvas.height !== canvas.offsetHeight) { + doDraw(scope.draw); + scope.$apply(); + } + } + + // Stop watching for changes to size (scope destroyed) + function releaseInterval() { + if (activeInterval) { + $interval.cancel(activeInterval); + } + } + + // Switch from WebGL to plain 2D if context is lost + function fallbackFromWebGL() { + element.html(TEMPLATE); + canvas = element.find("canvas")[0]; + chart = getChart([Canvas2DChart], canvas); + if (chart) { + doDraw(scope.draw); + } + } + + // Try to initialize a chart. + chart = getChart([GLChart, Canvas2DChart], canvas); + + // If that failed, there's nothing more we can do here. + // (A warning will already have been issued) + if (!chart) { + return; + } + + // WebGL is a bit of a special case; it may work, then fail + // later for various reasons, so we need to listen for this + // and fall back to plain canvas drawing when it occurs. + canvas.addEventListener("webglcontextlost", fallbackFromWebGL); + + // Check for resize, on a timer + activeInterval = $interval(drawIfResized, 1000, 0, false); + + // Watch "draw" for external changes to the set of + // things to be drawn. + scope.$watchCollection("draw", doDraw); + + // Stop checking for resize when scope is destroyed + scope.$on("$destroy", releaseInterval); + } + + return { + // Apply directive only to elements + restrict: "E", + + // Template to use (a canvas element) + template: TEMPLATE, + + // Link function; set up scope + link: linkChart, + + // Initial, isolate scope for the directive + scope: { draw: "=" } + }; + } + + /** + * @interface platform/features/plot.Chart + * @private + */ + + /** + * Clear the chart. + * @method platform/features/plot.Chart#clear + */ + /** + * Set the logical boundaries of the chart. + * @param {number[]} dimensions the horizontal and + * vertical dimensions of the chart + * @param {number[]} origin the horizontal/vertical + * origin of the chart + * @memberof platform/features/plot.Chart#setDimensions + */ + /** + * Draw the supplied buffer as a line strip (a sequence + * of line segments), in the chosen color. + * @param {Float32Array} buf the line strip to draw, + * in alternating x/y positions + * @param {number[]} color the color to use when drawing + * the line, as an RGBA color where each element + * is in the range of 0.0-1.0 + * @param {number} points the number of points to draw + * @memberof platform/features/plot.Chart#drawLine + */ + /** + * Draw a rectangle extending from one corner to another, + * in the chosen color. + * @param {number[]} min the first corner of the rectangle + * @param {number[]} max the opposite corner + * @param {number[]} color the color to use when drawing + * the rectangle, as an RGBA color where each element + * is in the range of 0.0-1.0 + * @memberof platform/features/plot.Chart#drawSquare + */ + + return MCTTimelineChart; + } +); + diff --git a/platform/features/timeline/src/controllers/graph/TimelineGraph.js b/platform/features/timeline/src/controllers/graph/TimelineGraph.js index 17c36d55ac..62ffa68986 100644 --- a/platform/features/timeline/src/controllers/graph/TimelineGraph.js +++ b/platform/features/timeline/src/controllers/graph/TimelineGraph.js @@ -167,7 +167,7 @@ define( */ setBounds: function (offset, duration) { // We don't update in-place, because we need the change - // to trigger a watch in mct-chart. + // to trigger a watch in mct-timeline-chart. drawingObject.origin = [offset, drawingObject.origin[1]]; drawingObject.dimensions = [duration, drawingObject.dimensions[1]]; }, diff --git a/platform/features/timeline/src/controllers/graph/TimelineGraphRenderer.js b/platform/features/timeline/src/controllers/graph/TimelineGraphRenderer.js index fc4d60c34c..f826352c6f 100644 --- a/platform/features/timeline/src/controllers/graph/TimelineGraphRenderer.js +++ b/platform/features/timeline/src/controllers/graph/TimelineGraphRenderer.js @@ -26,7 +26,7 @@ define( /** * Responsible for preparing data for display by - * `mct-chart` in a timeline's resource graph. + * `mct-timeline-chart` in a timeline's resource graph. * @constructor */ function TimelineGraphRenderer() { @@ -54,7 +54,7 @@ define( * 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.) + * `mct-timeline-chart` (which uses WebGL.) * @param {string} the color * @returns {number[]} the same color, in floating-point format */ diff --git a/platform/features/timeline/test/chart/Canvas2DChartSpec.js b/platform/features/timeline/test/chart/Canvas2DChartSpec.js new file mode 100644 index 0000000000..aef0e07131 --- /dev/null +++ b/platform/features/timeline/test/chart/Canvas2DChartSpec.js @@ -0,0 +1,95 @@ +/***************************************************************************** + * Open MCT, Copyright (c) 2014-2016, United States Government + * as represented by the Administrator of the National Aeronautics and Space + * Administration. All rights reserved. + * + * Open MCT is licensed under the Apache License, Version 2.0 (the + * "License"); you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * http://www.apache.org/licenses/LICENSE-2.0. + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT + * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the + * License for the specific language governing permissions and limitations + * under the License. + * + * Open MCT includes source code licensed under additional open source + * licenses. See the Open Source Licenses file (LICENSES.md) included with + * this source code distribution or the Licensing information page available + * at runtime from the About dialog for additional information. + *****************************************************************************/ + +/** + * MergeModelsSpec. Created by vwoeltje on 11/6/14. + */ +define( + ["../../src/chart/Canvas2DChart"], + function (Canvas2DChart) { + + describe("A canvas 2d chart", function () { + var mockCanvas, + mock2d, + chart; + + beforeEach(function () { + mockCanvas = jasmine.createSpyObj("canvas", ["getContext"]); + mock2d = jasmine.createSpyObj( + "2d", + [ + "clearRect", + "beginPath", + "moveTo", + "lineTo", + "stroke", + "fillRect" + ] + ); + mockCanvas.getContext.andReturn(mock2d); + + chart = new Canvas2DChart(mockCanvas); + }); + + // Note that tests below are less specific than they + // could be, esp. w.r.t. arguments to drawing calls; + // this is a fallback option so is a lower test priority. + + it("allows the canvas to be cleared", function () { + chart.clear(); + expect(mock2d.clearRect).toHaveBeenCalled(); + }); + + it("does not construct if 2D is unavailable", function () { + mockCanvas.getContext.andReturn(undefined); + expect(function () { + return new Canvas2DChart(mockCanvas); + }).toThrow(); + }); + + it("allows dimensions to be set", function () { + // No return value, just verify API is present + chart.setDimensions([120, 120], [0, 10]); + }); + + it("allows lines to be drawn", function () { + var testBuffer = [0, 1, 3, 8], + testColor = [0.25, 0.33, 0.66, 1.0], + testPoints = 2; + chart.drawLine(testBuffer, testColor, testPoints); + expect(mock2d.beginPath).toHaveBeenCalled(); + expect(mock2d.lineTo.calls.length).toEqual(1); + expect(mock2d.stroke).toHaveBeenCalled(); + }); + + it("allows squares to be drawn", function () { + var testMin = [0, 1], + testMax = [10, 10], + testColor = [0.25, 0.33, 0.66, 1.0]; + + chart.drawSquare(testMin, testMax, testColor); + expect(mock2d.fillRect).toHaveBeenCalled(); + }); + + }); + } +); diff --git a/platform/features/timeline/test/chart/GLChartSpec.js b/platform/features/timeline/test/chart/GLChartSpec.js new file mode 100644 index 0000000000..f3cbd5b763 --- /dev/null +++ b/platform/features/timeline/test/chart/GLChartSpec.js @@ -0,0 +1,143 @@ +/***************************************************************************** + * Open MCT, Copyright (c) 2014-2016, United States Government + * as represented by the Administrator of the National Aeronautics and Space + * Administration. All rights reserved. + * + * Open MCT is licensed under the Apache License, Version 2.0 (the + * "License"); you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * http://www.apache.org/licenses/LICENSE-2.0. + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT + * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the + * License for the specific language governing permissions and limitations + * under the License. + * + * Open MCT includes source code licensed under additional open source + * licenses. See the Open Source Licenses file (LICENSES.md) included with + * this source code distribution or the Licensing information page available + * at runtime from the About dialog for additional information. + *****************************************************************************/ + +/** + * MergeModelsSpec. Created by vwoeltje on 11/6/14. + */ +define( + ["../../src/chart/GLChart"], + function (GLChart) { + + describe("A WebGL chart", function () { + var mockCanvas, + mockGL, + glChart; + + beforeEach(function () { + mockCanvas = jasmine.createSpyObj("canvas", ["getContext"]); + mockGL = jasmine.createSpyObj( + "gl", + [ + "createShader", + "compileShader", + "shaderSource", + "attachShader", + "createProgram", + "linkProgram", + "useProgram", + "enableVertexAttribArray", + "getAttribLocation", + "getUniformLocation", + "createBuffer", + "lineWidth", + "enable", + "blendFunc", + "viewport", + "clear", + "uniform2fv", + "uniform4fv", + "bufferData", + "bindBuffer", + "vertexAttribPointer", + "drawArrays" + ] + ); + mockGL.ARRAY_BUFFER = "ARRAY_BUFFER"; + mockGL.DYNAMIC_DRAW = "DYNAMIC_DRAW"; + mockGL.TRIANGLE_FAN = "TRIANGLE_FAN"; + mockGL.LINE_STRIP = "LINE_STRIP"; + + // Echo back names for uniform locations, so we can + // test which of these are set for certain operations. + mockGL.getUniformLocation.andCallFake(function (a, name) { + return name; + }); + + mockCanvas.getContext.andReturn(mockGL); + + glChart = new GLChart(mockCanvas); + }); + + it("allows the canvas to be cleared", function () { + glChart.clear(); + expect(mockGL.clear).toHaveBeenCalled(); + }); + + it("does not construct if WebGL is unavailable", function () { + mockCanvas.getContext.andReturn(undefined); + expect(function () { + return new GLChart(mockCanvas); + }).toThrow(); + }); + + it("allows dimensions to be set", function () { + glChart.setDimensions([120, 120], [0, 10]); + expect(mockGL.uniform2fv) + .toHaveBeenCalledWith("uDimensions", [120, 120]); + expect(mockGL.uniform2fv) + .toHaveBeenCalledWith("uOrigin", [0, 10]); + }); + + it("allows lines to be drawn", function () { + var testBuffer = [0, 1, 3, 8], + testColor = [0.25, 0.33, 0.66, 1.0], + testPoints = 2; + glChart.drawLine(testBuffer, testColor, testPoints); + expect(mockGL.bufferData).toHaveBeenCalledWith( + mockGL.ARRAY_BUFFER, + testBuffer, + mockGL.DYNAMIC_DRAW + ); + expect(mockGL.uniform4fv) + .toHaveBeenCalledWith("uColor", testColor); + expect(mockGL.drawArrays) + .toHaveBeenCalledWith("LINE_STRIP", 0, testPoints); + }); + + it("allows squares to be drawn", function () { + var testMin = [0, 1], + testMax = [10, 10], + testColor = [0.25, 0.33, 0.66, 1.0]; + + glChart.drawSquare(testMin, testMax, testColor); + + expect(mockGL.uniform4fv) + .toHaveBeenCalledWith("uColor", testColor); + expect(mockGL.drawArrays) + .toHaveBeenCalledWith("TRIANGLE_FAN", 0, 4); + }); + + it("uses buffer sizes reported by WebGL", function () { + // Make sure that GLChart uses the GL buffer size, which may + // differ from what canvas requested. WTD-852 + mockCanvas.width = 300; + mockCanvas.height = 150; + mockGL.drawingBufferWidth = 200; + mockGL.drawingBufferHeight = 175; + + glChart.clear(); + + expect(mockGL.viewport).toHaveBeenCalledWith(0, 0, 200, 175); + }); + }); + } +); diff --git a/platform/features/timeline/test/chart/MCTTimelineChartSpec.js b/platform/features/timeline/test/chart/MCTTimelineChartSpec.js new file mode 100644 index 0000000000..f3e950d1f5 --- /dev/null +++ b/platform/features/timeline/test/chart/MCTTimelineChartSpec.js @@ -0,0 +1,216 @@ +/***************************************************************************** + * Open MCT, Copyright (c) 2014-2016, United States Government + * as represented by the Administrator of the National Aeronautics and Space + * Administration. All rights reserved. + * + * Open MCT is licensed under the Apache License, Version 2.0 (the + * "License"); you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * http://www.apache.org/licenses/LICENSE-2.0. + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT + * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the + * License for the specific language governing permissions and limitations + * under the License. + * + * Open MCT includes source code licensed under additional open source + * licenses. See the Open Source Licenses file (LICENSES.md) included with + * this source code distribution or the Licensing information page available + * at runtime from the About dialog for additional information. + *****************************************************************************/ + +/** + * MCTTimelineChart. Created by vwoeltje on 11/6/14. + */ +define( + ["../../src/chart/MCTTimelineChart"], + function (MCTTimelineChart) { + + describe("The mct-timeline-chart directive", function () { + var mockInterval, + mockLog, + mockScope, + mockElement, + mockCanvas, + mockGL, + mockC2d, + mockPromise, + mctChart; + + beforeEach(function () { + mockInterval = + jasmine.createSpy("$interval"); + mockLog = + jasmine.createSpyObj("$log", ["warn", "info", "debug"]); + mockScope = jasmine.createSpyObj( + "$scope", + ["$watchCollection", "$on", "$apply"] + ); + mockElement = + jasmine.createSpyObj("element", ["find", "html"]); + mockInterval.cancel = jasmine.createSpy("cancelInterval"); + mockPromise = jasmine.createSpyObj("promise", ["then"]); + + + // mct-timeline-chart uses GLChart, so it needs WebGL API + mockCanvas = + jasmine.createSpyObj("canvas", ["getContext", "addEventListener"]); + mockGL = jasmine.createSpyObj( + "gl", + [ + "createShader", + "compileShader", + "shaderSource", + "attachShader", + "createProgram", + "linkProgram", + "useProgram", + "enableVertexAttribArray", + "getAttribLocation", + "getUniformLocation", + "createBuffer", + "lineWidth", + "enable", + "blendFunc", + "viewport", + "clear", + "uniform2fv", + "uniform4fv", + "bufferData", + "bindBuffer", + "vertexAttribPointer", + "drawArrays" + ] + ); + mockC2d = jasmine.createSpyObj('c2d', ['clearRect']); + mockGL.ARRAY_BUFFER = "ARRAY_BUFFER"; + mockGL.DYNAMIC_DRAW = "DYNAMIC_DRAW"; + mockGL.TRIANGLE_FAN = "TRIANGLE_FAN"; + mockGL.LINE_STRIP = "LINE_STRIP"; + + // Echo back names for uniform locations, so we can + // test which of these are set for certain operations. + mockGL.getUniformLocation.andCallFake(function (a, name) { + return name; + }); + + mockElement.find.andReturn([mockCanvas]); + mockCanvas.getContext.andCallFake(function (type) { + return { webgl: mockGL, '2d': mockC2d }[type]; + }); + mockInterval.andReturn(mockPromise); + + mctChart = new MCTTimelineChart(mockInterval, mockLog); + }); + + it("is applicable at the element level", function () { + expect(mctChart.restrict).toEqual("E"); + }); + + it("places a 'draw' attribute in-scope", function () { + // Should ask Angular for the draw attribute + expect(mctChart.scope.draw).toEqual("="); + }); + + it("watches for changes in the drawn object", function () { + mctChart.link(mockScope, mockElement); + expect(mockScope.$watchCollection) + .toHaveBeenCalledWith("draw", jasmine.any(Function)); + }); + + it("issues one draw call per line", function () { + mctChart.link(mockScope, mockElement); + mockScope.$watchCollection.mostRecentCall.args[1]({ + lines: [{}, {}, {}] + }); + expect(mockGL.drawArrays.calls.length).toEqual(3); + }); + + it("issues one draw call per box", function () { + mctChart.link(mockScope, mockElement); + mockScope.$watchCollection.mostRecentCall.args[1]({ + boxes: [ + { start: [0, 0], end: [1, 1] }, + { start: [0, 0], end: [1, 1] }, + { start: [0, 0], end: [1, 1] }, + { start: [0, 0], end: [1, 1] } + ] + }); + expect(mockGL.drawArrays.calls.length).toEqual(4); + }); + + it("does not fail if no draw object is in scope", function () { + mctChart.link(mockScope, mockElement); + expect(mockScope.$watchCollection.mostRecentCall.args[1]) + .not.toThrow(); + }); + + it("draws on canvas resize", function () { + mctChart.link(mockScope, mockElement); + + // Should track canvas size in an interval + expect(mockInterval).toHaveBeenCalledWith( + jasmine.any(Function), + jasmine.any(Number), + 0, + false + ); + + // Verify pre-condition + expect(mockGL.clear).not.toHaveBeenCalled(); + + mockCanvas.width = 100; + mockCanvas.offsetWidth = 150; + mockCanvas.height = 200; + mockCanvas.offsetHeight = 200; + mockInterval.mostRecentCall.args[0](); + + // Use clear as an indication that drawing has occurred + expect(mockGL.clear).toHaveBeenCalled(); + }); + + it("warns if no WebGL context is available", function () { + mockCanvas.getContext.andReturn(undefined); + mctChart.link(mockScope, mockElement); + expect(mockLog.warn).toHaveBeenCalled(); + }); + + it("falls back to Canvas 2d API if WebGL context is lost", function () { + mctChart.link(mockScope, mockElement); + expect(mockCanvas.addEventListener) + .toHaveBeenCalledWith("webglcontextlost", jasmine.any(Function)); + expect(mockCanvas.getContext).not.toHaveBeenCalledWith('2d'); + mockCanvas.addEventListener.mostRecentCall.args[1](); + expect(mockCanvas.getContext).toHaveBeenCalledWith('2d'); + }); + + it("logs nothing in nominal situations (WebGL available)", function () { + // Complement the previous test + mctChart.link(mockScope, mockElement); + expect(mockLog.warn).not.toHaveBeenCalled(); + }); + + // Avoid resource leaks + it("stops polling for size changes on destroy", function () { + mctChart.link(mockScope, mockElement); + + // Should be listening for a destroy event + expect(mockScope.$on).toHaveBeenCalledWith( + "$destroy", + jasmine.any(Function) + ); + + // Precondition - interval still active + expect(mockInterval.cancel).not.toHaveBeenCalled(); + + // Broadcast a $destroy + mockScope.$on.mostRecentCall.args[1](); + + // Should have stopped the interval + expect(mockInterval.cancel).toHaveBeenCalledWith(mockPromise); + }); + + }); + } +); From 9f9d28deef8d90bcf5c153a3b98e36e4232498ed Mon Sep 17 00:00:00 2001 From: Josh Baldwin Date: Sat, 21 Jan 2017 11:27:20 -0500 Subject: [PATCH 02/90] adding MCT name to README --- README.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/README.md b/README.md index 8761314a0b..490385ab9d 100644 --- a/README.md +++ b/README.md @@ -1,6 +1,6 @@ # Open MCT [![license](https://img.shields.io/badge/license-Apache%202.0-blue.svg)](http://www.apache.org/licenses/LICENSE-2.0) -Open MCT is a next-generation mission control framework for visualization of data on desktop and mobile devices. It is developed at NASA's Ames Research Center, and is being used by NASA for data analysis of spacecraft missions, as well as planning and operation of experimental rover systems. As a generalizable and open source framework, Open MCT could be used as the basis for building applications for planning, operation, and analysis of any systems producing telemetry data. +Open MCT (Open Mission Control Technologies) is a next-generation mission control framework for visualization of data on desktop and mobile devices. It is developed at NASA's Ames Research Center, and is being used by NASA for data analysis of spacecraft missions, as well as planning and operation of experimental rover systems. As a generalizable and open source framework, Open MCT could be used as the basis for building applications for planning, operation, and analysis of any systems producing telemetry data. Please visit our [Official Site](https://nasa.github.io/openmct/) and [Getting Started Guide](https://nasa.github.io/openmct/getting-started/) From df7d59bc9cdb92a79514ff2aac85e7d9ef60a492 Mon Sep 17 00:00:00 2001 From: Charles Hacskaylo Date: Mon, 9 Jan 2017 15:30:14 -0800 Subject: [PATCH 03/90] [Config] Fixes for example/msl Fixes #1386 --- example/msl/bundle.js | 137 ++++++++++++++++++++++-------------------- 1 file changed, 71 insertions(+), 66 deletions(-) diff --git a/example/msl/bundle.js b/example/msl/bundle.js index cb848d252d..12dbbb5157 100644 --- a/example/msl/bundle.js +++ b/example/msl/bundle.js @@ -37,74 +37,79 @@ define([ legacyRegistry.register("example/msl", { "name" : "Mars Science Laboratory Data Adapter", "extensions" : { - "types": [ - { - "name":"Mars Science Laboratory", - "key": "msl.curiosity", - "cssclass": "icon-object" - }, - { - "name": "Instrument", - "key": "msl.instrument", - "cssclass": "icon-object", - "model": {"composition": []} - }, - { - "name": "Measurement", - "key": "msl.measurement", - "cssclass": "icon-telemetry", - "model": {"telemetry": {}}, - "telemetry": { - "source": "rems.source", - "domains": [ - { - "name": "Time", - "key": "utc", - "format": "utc" - } - ] + "types": [ + { + "name":"Mars Science Laboratory", + "key": "msl.curiosity", + "cssclass": "icon-object" + }, + { + "name": "Instrument", + "key": "msl.instrument", + "cssclass": "icon-object", + "model": {"composition": []} + }, + { + "name": "Measurement", + "key": "msl.measurement", + "cssclass": "icon-telemetry", + "model": {"telemetry": {}}, + "telemetry": { + "source": "rems.source", + "domains": [ + { + "name": "Time", + "key": "utc", + "format": "utc" + } + ] + } } - } - ], - "constants": [ - { - "key": "REMS_WS_URL", - "value": "/proxyUrl?url=http://cab.inta-csic.es/rems/wp-content/plugins/marsweather-widget/api.php" - } - ], - "roots": [ - { - "id": "msl:curiosity", - "priority" : "preferred", - "model": { - "type": "msl.curiosity", - "name": "Mars Science Laboratory", - "composition": ["msl_tlm:rems"] + ], + "constants": [ + { + "key": "REMS_WS_URL", + "value": "/proxyUrl?url=http://cab.inta-csic.es/rems/wp-content/plugins/marsweather-widget/api.php" } - } - ], - "services": [ - { - "key":"rems.adapter", - "implementation": RemsTelemetryServerAdapter, - "depends": ["$q", "$http", "$log", "REMS_WS_URL"] - } - ], - "components": [ - { - "provides": "modelService", - "type": "provider", - "implementation": RemsTelemetryModelProvider, - "depends": ["rems.adapter"] - }, - { - "provides": "telemetryService", - "type": "provider", - "implementation": RemsTelemetryProvider, - "depends": ["rems.adapter", "$q"] - } - ] - } + ], + "roots": [ + { + "id": "msl:curiosity" + } + ], + "models": [ + { + "id": "msl:curiosity", + "priority": "preferred", + "model": { + "type": "msl.curiosity", + "name": "Mars Science Laboratory", + "composition": ["msl_tlm:rems"] + } + } + ], + "services": [ + { + "key":"rems.adapter", + "implementation": RemsTelemetryServerAdapter, + "depends": ["$q", "$http", "$log", "REMS_WS_URL"] + } + ], + "components": [ + { + "provides": "modelService", + "type": "provider", + "implementation": RemsTelemetryModelProvider, + "depends": ["rems.adapter"] + }, + { + "provides": "telemetryService", + "type": "provider", + "implementation": RemsTelemetryProvider, + "depends": ["rems.adapter", "$q"] + } + ] + } }); }); From 65bf38d5e6f2c7d98f6ff56544d1908f187fed58 Mon Sep 17 00:00:00 2001 From: Charles Hacskaylo Date: Fri, 27 Jan 2017 11:16:06 -0800 Subject: [PATCH 04/90] [Frontend] Add grab affordance styling on hover Fixes #1415 WIP --- .../core/res/sass/_time-conductor-base.scss | 21 +++++++++++++++++++ 1 file changed, 21 insertions(+) diff --git a/platform/features/conductor/core/res/sass/_time-conductor-base.scss b/platform/features/conductor/core/res/sass/_time-conductor-base.scss index 629c17f726..f49e8c6669 100644 --- a/platform/features/conductor/core/res/sass/_time-conductor-base.scss +++ b/platform/features/conductor/core/res/sass/_time-conductor-base.scss @@ -346,7 +346,28 @@ content: $i; } .l-axis-holder { + $grabTicksH: 8px; + $grabTicksXSpace: 3px; + $grabTicksYOffset: (($r1H - $grabTicksH) / 2) - 2px; @include cursorGrab(); + &:hover { + $c0: rgba($colorBodyFg, 0.05); + $c2: transparent; // Bg + @include background-image(linear-gradient( + $c0 70%, $c2 100% + )); + svg { + $c1: rgba($colorBodyFg, 0.15); // Line + $angle: 90deg; + @include background-image(linear-gradient($angle, + $c1 1px, $c2 1px, + $c2 100% + )); + background-position: center $grabTicksYOffset; + background-repeat: repeat-x; + background-size: $grabTicksXSpace $grabTicksH; + } + } } } From d1e7e7894e1c02df3cf8c467843e3ac98b4785c9 Mon Sep 17 00:00:00 2001 From: Charles Hacskaylo Date: Fri, 27 Jan 2017 13:58:38 -0800 Subject: [PATCH 05/90] [Frontend] Add grab affordance styling on hover Fixes #1415 --- .../conductor/core/res/sass/_time-conductor-base.scss | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/platform/features/conductor/core/res/sass/_time-conductor-base.scss b/platform/features/conductor/core/res/sass/_time-conductor-base.scss index f49e8c6669..e1dad98dad 100644 --- a/platform/features/conductor/core/res/sass/_time-conductor-base.scss +++ b/platform/features/conductor/core/res/sass/_time-conductor-base.scss @@ -351,13 +351,13 @@ $grabTicksYOffset: (($r1H - $grabTicksH) / 2) - 2px; @include cursorGrab(); &:hover { - $c0: rgba($colorBodyFg, 0.05); + $c0: rgba($colorBodyFg, 0.1); $c2: transparent; // Bg @include background-image(linear-gradient( $c0 70%, $c2 100% )); svg { - $c1: rgba($colorBodyFg, 0.15); // Line + $c1: rgba($colorBodyFg, 0.2); // Line $angle: 90deg; @include background-image(linear-gradient($angle, $c1 1px, $c2 1px, From 784114e2561a95983f9acd6478c98e31e6c45d35 Mon Sep 17 00:00:00 2001 From: Charles Hacskaylo Date: Fri, 27 Jan 2017 14:07:36 -0800 Subject: [PATCH 06/90] [Frontend] Add grab affordance grippys Fixes #1415 --- .../core/res/sass/_time-conductor-base.scss | 32 +++++++++---------- 1 file changed, 16 insertions(+), 16 deletions(-) diff --git a/platform/features/conductor/core/res/sass/_time-conductor-base.scss b/platform/features/conductor/core/res/sass/_time-conductor-base.scss index e1dad98dad..96e45ad447 100644 --- a/platform/features/conductor/core/res/sass/_time-conductor-base.scss +++ b/platform/features/conductor/core/res/sass/_time-conductor-base.scss @@ -346,27 +346,27 @@ content: $i; } .l-axis-holder { - $grabTicksH: 8px; - $grabTicksXSpace: 3px; - $grabTicksYOffset: (($r1H - $grabTicksH) / 2) - 2px; + $c0: rgba($colorBodyFg, 0.1); + $c2: transparent; + $grabTicksH: 3px; + $grabTicksXSpace: 4px; + $grabTicksYOffset: 0; @include cursorGrab(); + svg { + $c1: rgba($colorBodyFg, 0.2); + $angle: 90deg; + @include background-image(linear-gradient($angle, + $c1 1px, $c2 1px, + $c2 100% + )); + background-position: center $grabTicksYOffset; + background-repeat: repeat-x; + background-size: $grabTicksXSpace $grabTicksH; + } &:hover { - $c0: rgba($colorBodyFg, 0.1); - $c2: transparent; // Bg @include background-image(linear-gradient( $c0 70%, $c2 100% )); - svg { - $c1: rgba($colorBodyFg, 0.2); // Line - $angle: 90deg; - @include background-image(linear-gradient($angle, - $c1 1px, $c2 1px, - $c2 100% - )); - background-position: center $grabTicksYOffset; - background-repeat: repeat-x; - background-size: $grabTicksXSpace $grabTicksH; - } } } } From 17564aa48913f2af2df93b27c28b1d1ac611f1b7 Mon Sep 17 00:00:00 2001 From: Alex M Date: Sun, 29 Jan 2017 18:16:36 +0200 Subject: [PATCH 07/90] [Tutorial] Replace glyph mentions with cssclass --- docs/src/tutorials/index.md | 5 ++--- 1 file changed, 2 insertions(+), 3 deletions(-) diff --git a/docs/src/tutorials/index.md b/docs/src/tutorials/index.md index 52263278ed..2206f18898 100644 --- a/docs/src/tutorials/index.md +++ b/docs/src/tutorials/index.md @@ -340,8 +340,7 @@ Going through the properties we've defined: domain objects of this type. * The `name` of "To-Do List" is the human-readable name for this type, and will be shown to users. -* The `glyph` refers to a special character in Open MCT's custom font set; -this will be used as an icon. +* The `cssclass` describes the icon that will be shown for each To-Do List. * The `description` is also human-readable, and will be used whenever a longer explanation of what this type is should be shown. * Finally, the `features` property describes some special features of objects of @@ -446,7 +445,7 @@ the domain object type, but could have chosen any unique name. domain objects of that type. This means that we'll see this view for To-do Lists that we create, but not for other domain objects (such as Folders.) -* The `glyph` and `name` properties describe the icon and human-readable name +* The `cssclass` and `name` properties describe the icon and human-readable name for this view to display in the UI where needed (if multiple views are available for To-do Lists, the user will be able to choose one.) From 35d8024aaab3ba8fbcb2e3f39ea44637c72aaf19 Mon Sep 17 00:00:00 2001 From: Bogdan Date: Tue, 31 Jan 2017 21:32:31 +0200 Subject: [PATCH 08/90] [Tutorial] Better describe cssclass --- docs/src/tutorials/index.md | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/docs/src/tutorials/index.md b/docs/src/tutorials/index.md index 2206f18898..ec4adef2b8 100644 --- a/docs/src/tutorials/index.md +++ b/docs/src/tutorials/index.md @@ -340,7 +340,9 @@ Going through the properties we've defined: domain objects of this type. * The `name` of "To-Do List" is the human-readable name for this type, and will be shown to users. -* The `cssclass` describes the icon that will be shown for each To-Do List. +* The `cssclass` maps to an icon that will be shown for each To-Do List. The icons +are defined in our [custom open MCT icon set](platform/commonUI/general/res/sass/_glyphs.scss). +A complete list of available icons will be provided in the future. * The `description` is also human-readable, and will be used whenever a longer explanation of what this type is should be shown. * Finally, the `features` property describes some special features of objects of From e712edba4e8d70b9b22ccb0db088b85134f44311 Mon Sep 17 00:00:00 2001 From: Bogdan Date: Tue, 31 Jan 2017 21:35:42 +0200 Subject: [PATCH 09/90] [Tutorial] Fix icon set url --- docs/src/tutorials/index.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/docs/src/tutorials/index.md b/docs/src/tutorials/index.md index ec4adef2b8..fc5f47971e 100644 --- a/docs/src/tutorials/index.md +++ b/docs/src/tutorials/index.md @@ -341,7 +341,7 @@ domain objects of this type. * The `name` of "To-Do List" is the human-readable name for this type, and will be shown to users. * The `cssclass` maps to an icon that will be shown for each To-Do List. The icons -are defined in our [custom open MCT icon set](platform/commonUI/general/res/sass/_glyphs.scss). +are defined in our [custom open MCT icon set](https://github.com/nasa/openmct/blob/master/platform/commonUI/general/res/sass/_glyphs.scss). A complete list of available icons will be provided in the future. * The `description` is also human-readable, and will be used whenever a longer explanation of what this type is should be shown. From 3d3baddd235e920aaa647b41254bdcbae3a95ba9 Mon Sep 17 00:00:00 2001 From: Henry Date: Thu, 2 Feb 2017 15:08:26 -0800 Subject: [PATCH 10/90] [Tables] Do not persist column configuration for non-editable objects --- platform/features/table/src/TableConfiguration.js | 4 +++- platform/features/table/test/TableConfigurationSpec.js | 10 +++++++++- 2 files changed, 12 insertions(+), 2 deletions(-) diff --git a/platform/features/table/src/TableConfiguration.js b/platform/features/table/src/TableConfiguration.js index 5b463ee7f1..fee22d47bc 100644 --- a/platform/features/table/src/TableConfiguration.js +++ b/platform/features/table/src/TableConfiguration.js @@ -187,7 +187,9 @@ define( }); //Synchronize column configuration with model - if (configChanged(configuration, defaultConfig)) { + if (this.domainObject.hasCapability('editor') && + this.domainObject.getCapability('editor').isEditContextRoot() && + configChanged(configuration, defaultConfig)) { this.saveColumnConfiguration(configuration); } diff --git a/platform/features/table/test/TableConfigurationSpec.js b/platform/features/table/test/TableConfigurationSpec.js index db90cc1c62..dcac2a9876 100644 --- a/platform/features/table/test/TableConfigurationSpec.js +++ b/platform/features/table/test/TableConfigurationSpec.js @@ -35,10 +35,18 @@ define( beforeEach(function () { mockDomainObject = jasmine.createSpyObj('domainObject', - ['getModel', 'useCapability', 'getCapability'] + ['getModel', 'useCapability', 'getCapability', 'hasCapability'] ); mockModel = {}; mockDomainObject.getModel.andReturn(mockModel); + mockDomainObject.getCapability.andCallFake(function (name) { + return name === 'editor' && { + isEditContextRoot: function () { + return true; + } + }; + }); + mockTelemetryFormatter = jasmine.createSpyObj('telemetryFormatter', [ 'formatDomainValue', From af3cbe9ed1cddbdffe757f86af7a00839052c0b4 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Toma=CC=81s=CC=8C=20Stankovic=CC=8C?= Date: Tue, 7 Feb 2017 23:11:23 +0100 Subject: [PATCH 11/90] [Optimization] Reduce D3 dependency size #1224 --- bower.json | 1 - karma.conf.js | 1 + openmct.js | 22 ++++++++++++++++--- package.json | 10 +++++++++ .../core/src/ui/ConductorAxisController.js | 18 ++++++++------- .../src/ui/ConductorAxisControllerSpec.js | 18 ++++++++------- test-main.js | 22 ++++++++++++++++--- 7 files changed, 69 insertions(+), 23 deletions(-) diff --git a/bower.json b/bower.json index 161ee04186..ed3dbaf899 100644 --- a/bower.json +++ b/bower.json @@ -22,7 +22,6 @@ "eventemitter3": "^1.2.0", "lodash": "3.10.1", "almond": "~0.3.2", - "d3": "~4.1.0", "html2canvas": "^0.4.1" } } diff --git a/karma.conf.js b/karma.conf.js index 84909b42a7..f4bd0a10b5 100644 --- a/karma.conf.js +++ b/karma.conf.js @@ -35,6 +35,7 @@ module.exports = function(config) { // By default, files are also included in a script tag. files: [ {pattern: 'bower_components/**/*.js', included: false}, + {pattern: 'node_modules/d3-*/**/*.js', included: false}, {pattern: 'src/**/*.js', included: false}, {pattern: 'example/**/*.js', included: false}, {pattern: 'example/**/*.json', included: false}, diff --git a/openmct.js b/openmct.js index 68151008cd..3084f96a4d 100644 --- a/openmct.js +++ b/openmct.js @@ -38,7 +38,16 @@ requirejs.config({ "uuid": "bower_components/node-uuid/uuid", "zepto": "bower_components/zepto/zepto.min", "lodash": "bower_components/lodash/lodash", - "d3": "bower_components/d3/d3.min" + "d3-selection": "node_modules/d3-selection/build/d3-selection.min", + "d3-scale": "node_modules/d3-scale/build/d3-scale.min", + "d3-axis": "node_modules/d3-axis/build/d3-axis.min", + "d3-array": "node_modules/d3-array/build/d3-array.min", + "d3-collection": "node_modules/d3-collection/build/d3-collection.min", + "d3-color": "node_modules/d3-color/build/d3-color.min", + "d3-format": "node_modules/d3-format/build/d3-format.min", + "d3-interpolate": "node_modules/d3-interpolate/build/d3-interpolate.min", + "d3-time": "node_modules/d3-time/build/d3-time.min", + "d3-time-format": "node_modules/d3-time-format/build/d3-time-format.min", }, "shim": { "angular": { @@ -65,8 +74,15 @@ requirejs.config({ "lodash": { "exports": "lodash" }, - "d3": { - "exports": "d3" + "d3-selection": { + "exports": "d3-selection" + }, + "d3-scale": { + "deps": ["d3-array", "d3-collection", "d3-color", "d3-format", "d3-interpolate", "d3-time", "d3-time-format"], + "exports": "d3-scale" + }, + "d3-axis": { + "exports": "d3-axis" } } }); diff --git a/package.json b/package.json index 65fabeb1f6..55bc7ab667 100644 --- a/package.json +++ b/package.json @@ -3,6 +3,16 @@ "version": "0.12.1-SNAPSHOT", "description": "The Open MCT core platform", "dependencies": { + "d3-array": "^1.0.2", + "d3-axis": "^1.0.4", + "d3-collection": "^1.0.2", + "d3-color": "^1.0.2", + "d3-format": "^1.0.2", + "d3-interpolate": "^1.1.3", + "d3-scale": "^1.0.4", + "d3-selection": "^1.0.3", + "d3-time": "^1.0.4", + "d3-time-format": "^2.0.3", "express": "^4.13.1", "minimist": "^1.1.1", "request": "^2.69.0" diff --git a/platform/features/conductor/core/src/ui/ConductorAxisController.js b/platform/features/conductor/core/src/ui/ConductorAxisController.js index b99eaa0e6e..7d4c2df7da 100644 --- a/platform/features/conductor/core/src/ui/ConductorAxisController.js +++ b/platform/features/conductor/core/src/ui/ConductorAxisController.js @@ -22,9 +22,11 @@ define( [ - "d3" + "d3-selection", + "d3-scale", + "d3-axis" ], - function (d3) { + function (d3Selection, d3Scale, d3Axis) { var PADDING = 1; /** @@ -70,12 +72,12 @@ define( ConductorAxisController.prototype.initialize = function (element) { this.target = element[0].firstChild; var height = this.target.offsetHeight; - var vis = d3.select(this.target) + var vis = d3Selection.select(this.target) .append("svg:svg") .attr("width", "100%") .attr("height", height); - this.xAxis = d3.axisTop(); + this.xAxis = d3Axis.axisTop(); // draw x axis with labels and move to the bottom of the chart area this.axisElement = vis.append("g") @@ -115,10 +117,10 @@ define( var bounds = this.bounds; if (timeSystem.isUTCBased()) { - this.xScale = this.xScale || d3.scaleUtc(); + this.xScale = this.xScale || d3Scale.scaleUtc(); this.xScale.domain([new Date(bounds.start), new Date(bounds.end)]); } else { - this.xScale = this.xScale || d3.scaleLinear(); + this.xScale = this.xScale || d3Scale.scaleLinear(); this.xScale.domain([bounds.start, bounds.end]); } @@ -145,9 +147,9 @@ define( //The D3 scale used depends on the type of time system as d3 // supports UTC out of the box. if (timeSystem.isUTCBased()) { - this.xScale = d3.scaleUtc(); + this.xScale = d3Scale.scaleUtc(); } else { - this.xScale = d3.scaleLinear(); + this.xScale = d3Scale.scaleLinear(); } this.xAxis.scale(this.xScale); diff --git a/platform/features/conductor/core/src/ui/ConductorAxisControllerSpec.js b/platform/features/conductor/core/src/ui/ConductorAxisControllerSpec.js index 2dbbb42c4b..6cc9677a16 100644 --- a/platform/features/conductor/core/src/ui/ConductorAxisControllerSpec.js +++ b/platform/features/conductor/core/src/ui/ConductorAxisControllerSpec.js @@ -23,11 +23,13 @@ define([ './ConductorAxisController', 'zepto', - 'd3' + 'd3-selection', + 'd3-scale' ], function ( ConductorAxisController, $, - d3 + d3Selection, + d3Scale ) { describe("The ConductorAxisController", function () { var controller, @@ -84,8 +86,8 @@ define([ "emit" ]); - spyOn(d3, 'scaleUtc').andCallThrough(); - spyOn(d3, 'scaleLinear').andCallThrough(); + spyOn(d3Scale, 'scaleUtc').andCallThrough(); + spyOn(d3Scale, 'scaleLinear').andCallThrough(); element = $('
'); $(document).find('body').append(element); @@ -122,15 +124,15 @@ define([ mockTimeSystem.isUTCBased.andReturn(true); controller.changeTimeSystem(mockTimeSystem); - expect(d3.scaleUtc).toHaveBeenCalled(); - expect(d3.scaleLinear).not.toHaveBeenCalled(); + expect(d3Scale.scaleUtc).toHaveBeenCalled(); + expect(d3Scale.scaleLinear).not.toHaveBeenCalled(); }); it("uses a linear scale for non-UTC time systems", function () { mockTimeSystem.isUTCBased.andReturn(false); controller.changeTimeSystem(mockTimeSystem); - expect(d3.scaleLinear).toHaveBeenCalled(); - expect(d3.scaleUtc).not.toHaveBeenCalled(); + expect(d3Scale.scaleLinear).toHaveBeenCalled(); + expect(d3Scale.scaleUtc).not.toHaveBeenCalled(); }); it("sets axis domain to time conductor bounds", function () { diff --git a/test-main.js b/test-main.js index b300560588..3f30247742 100644 --- a/test-main.js +++ b/test-main.js @@ -64,7 +64,16 @@ requirejs.config({ "uuid": "bower_components/node-uuid/uuid", "zepto": "bower_components/zepto/zepto.min", "lodash": "bower_components/lodash/lodash", - "d3": "bower_components/d3/d3.min" + "d3-selection": "node_modules/d3-selection/build/d3-selection.min", + "d3-scale": "node_modules/d3-scale/build/d3-scale.min", + "d3-axis": "node_modules/d3-axis/build/d3-axis.min", + "d3-array": "node_modules/d3-array/build/d3-array.min", + "d3-collection": "node_modules/d3-collection/build/d3-collection.min", + "d3-color": "node_modules/d3-color/build/d3-color.min", + "d3-format": "node_modules/d3-format/build/d3-format.min", + "d3-interpolate": "node_modules/d3-interpolate/build/d3-interpolate.min", + "d3-time": "node_modules/d3-time/build/d3-time.min", + "d3-time-format": "node_modules/d3-time-format/build/d3-time-format.min" }, "shim": { @@ -89,8 +98,15 @@ requirejs.config({ "lodash": { "exports": "lodash" }, - "d3": { - "exports": "d3" + "d3-selection": { + "exports": "d3-selection" + }, + "d3-scale": { + "deps": ["d3-array", "d3-collection", "d3-color", "d3-format", "d3-interpolate", "d3-time", "d3-time-format"], + "exports": "d3-scale" + }, + "d3-axis": { + "exports": "d3-axis" } }, From d3b4ad41c2ea98edc94ea387ed488491a8e43f46 Mon Sep 17 00:00:00 2001 From: Dhrubomoy Das Gupta Date: Thu, 9 Feb 2017 20:54:52 -0500 Subject: [PATCH 12/90] [Documentation] Fixed filename Fixed file name "Platform.md" to "platform.md". "Platform.md" was giving a 404 error when clicked, in github and in the official site as well. --- docs/src/architecture/framework.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/docs/src/architecture/framework.md b/docs/src/architecture/framework.md index e269e38f61..78adb98158 100644 --- a/docs/src/architecture/framework.md +++ b/docs/src/architecture/framework.md @@ -131,7 +131,7 @@ Keeping that in mind, there are a few useful patterns supported by the framework that are useful to keep in mind. The specific service infrastructure provided by the platform is described -in the [Platform Architecture](Platform.md). +in the [Platform Architecture](platform.md). ## Extension Categories From 77d0134e2e8b043edf270c675d7c84cdf4694cb1 Mon Sep 17 00:00:00 2001 From: Henry Date: Fri, 10 Feb 2017 10:03:05 -0800 Subject: [PATCH 13/90] [Build] Added Bourbon deprecation warning suppression system. --- platform/commonUI/general/res/sass/startup-base.scss | 1 + platform/commonUI/themes/espresso/res/sass/theme-espresso.scss | 2 +- platform/commonUI/themes/snow/res/sass/theme-snow.scss | 2 +- .../conductor/core/res/sass/time-conductor-espresso.scss | 1 + .../features/conductor/core/res/sass/time-conductor-snow.scss | 1 + platform/features/timeline/res/sass/timeline-espresso.scss | 1 + platform/features/timeline/res/sass/timeline-snow.scss | 1 + platform/features/timeline/res/sass/timeline.scss | 1 + 8 files changed, 8 insertions(+), 2 deletions(-) diff --git a/platform/commonUI/general/res/sass/startup-base.scss b/platform/commonUI/general/res/sass/startup-base.scss index 7b4d382ec6..ed29849920 100644 --- a/platform/commonUI/general/res/sass/startup-base.scss +++ b/platform/commonUI/general/res/sass/startup-base.scss @@ -19,6 +19,7 @@ * this source code distribution or the Licensing information page available * at runtime from the About dialog for additional information. *****************************************************************************/ +$output-bourbon-deprecation-warnings: false; @import "bourbon"; @import "logo-and-bg"; diff --git a/platform/commonUI/themes/espresso/res/sass/theme-espresso.scss b/platform/commonUI/themes/espresso/res/sass/theme-espresso.scss index 02f56f462a..514821f3db 100644 --- a/platform/commonUI/themes/espresso/res/sass/theme-espresso.scss +++ b/platform/commonUI/themes/espresso/res/sass/theme-espresso.scss @@ -19,7 +19,7 @@ * this source code distribution or the Licensing information page available * at runtime from the About dialog for additional information. *****************************************************************************/ - +$output-bourbon-deprecation-warnings: false; @import "bourbon"; @import "../../../../general/res/sass/_mixins"; diff --git a/platform/commonUI/themes/snow/res/sass/theme-snow.scss b/platform/commonUI/themes/snow/res/sass/theme-snow.scss index fed2d2c3e9..78a4af6908 100644 --- a/platform/commonUI/themes/snow/res/sass/theme-snow.scss +++ b/platform/commonUI/themes/snow/res/sass/theme-snow.scss @@ -19,7 +19,7 @@ * this source code distribution or the Licensing information page available * at runtime from the About dialog for additional information. *****************************************************************************/ - +$output-bourbon-deprecation-warnings: false; @import "bourbon"; @import "../../../../general/res/sass/_mixins"; diff --git a/platform/features/conductor/core/res/sass/time-conductor-espresso.scss b/platform/features/conductor/core/res/sass/time-conductor-espresso.scss index 03bb41dc7e..67624846fc 100644 --- a/platform/features/conductor/core/res/sass/time-conductor-espresso.scss +++ b/platform/features/conductor/core/res/sass/time-conductor-espresso.scss @@ -19,6 +19,7 @@ * this source code distribution or the Licensing information page available * at runtime from the About dialog for additional information. *****************************************************************************/ +$output-bourbon-deprecation-warnings: false; @import "bourbon"; @import "../../../../../commonUI/general/res/sass/constants"; @import "../../../../../commonUI/general/res/sass/mixins"; diff --git a/platform/features/conductor/core/res/sass/time-conductor-snow.scss b/platform/features/conductor/core/res/sass/time-conductor-snow.scss index a927791b5c..d989cd2551 100644 --- a/platform/features/conductor/core/res/sass/time-conductor-snow.scss +++ b/platform/features/conductor/core/res/sass/time-conductor-snow.scss @@ -19,6 +19,7 @@ * this source code distribution or the Licensing information page available * at runtime from the About dialog for additional information. *****************************************************************************/ +$output-bourbon-deprecation-warnings: false; @import "bourbon"; @import "../../../../../commonUI/general/res/sass/constants"; @import "../../../../../commonUI/general/res/sass/mixins"; diff --git a/platform/features/timeline/res/sass/timeline-espresso.scss b/platform/features/timeline/res/sass/timeline-espresso.scss index cc18bdce06..66ae9c94e6 100644 --- a/platform/features/timeline/res/sass/timeline-espresso.scss +++ b/platform/features/timeline/res/sass/timeline-espresso.scss @@ -19,6 +19,7 @@ * this source code distribution or the Licensing information page available * at runtime from the About dialog for additional information. *****************************************************************************/ +$output-bourbon-deprecation-warnings: false; @import "bourbon"; @import "../../../../commonUI/general/res/sass/constants"; diff --git a/platform/features/timeline/res/sass/timeline-snow.scss b/platform/features/timeline/res/sass/timeline-snow.scss index 4f18f64765..31a7ee9887 100644 --- a/platform/features/timeline/res/sass/timeline-snow.scss +++ b/platform/features/timeline/res/sass/timeline-snow.scss @@ -19,6 +19,7 @@ * this source code distribution or the Licensing information page available * at runtime from the About dialog for additional information. *****************************************************************************/ +$output-bourbon-deprecation-warnings: false; @import "bourbon"; @import "../../../../commonUI/general/res/sass/constants"; diff --git a/platform/features/timeline/res/sass/timeline.scss b/platform/features/timeline/res/sass/timeline.scss index f6fe978ef8..1f9a2c4694 100644 --- a/platform/features/timeline/res/sass/timeline.scss +++ b/platform/features/timeline/res/sass/timeline.scss @@ -19,6 +19,7 @@ * this source code distribution or the Licensing information page available * at runtime from the About dialog for additional information. *****************************************************************************/ +$output-bourbon-deprecation-warnings: false; @import "bourbon"; @import "../../../../commonUI/general/res/sass/constants"; From 6d5530ba9cd87351795b6b97b48efb9e6b927e32 Mon Sep 17 00:00:00 2001 From: Henry Date: Tue, 6 Dec 2016 12:08:52 -0800 Subject: [PATCH 14/90] [Tables] Using new composition API to fetch all telemetry objects --- .../controllers/TelemetryTableController.js | 47 ++++++++++++++++--- 1 file changed, 41 insertions(+), 6 deletions(-) diff --git a/platform/features/table/src/controllers/TelemetryTableController.js b/platform/features/table/src/controllers/TelemetryTableController.js index 7d6cbc2bec..d845c13d2f 100644 --- a/platform/features/table/src/controllers/TelemetryTableController.js +++ b/platform/features/table/src/controllers/TelemetryTableController.js @@ -26,9 +26,11 @@ */ define( [ - '../TableConfiguration' + '../TableConfiguration', + '../../../../../src/api/objects/object-utils' + ], - function (TableConfiguration) { + function (TableConfiguration, objectUtils) { /** * The TableController is responsible for getting data onto the page @@ -56,6 +58,8 @@ define( telemetryFormatter); this.changeListeners = []; this.conductor = openmct.conductor; + this.openmct = openmct; + this.newObject = objectUtils.toNewFormat($scope.domainObject.getModel(), $scope.domainObject.getId()); $scope.rows = []; @@ -156,16 +160,47 @@ define( only). */ TelemetryTableController.prototype.subscribe = function () { + var telemetryApi = this.openmct.telemetry; + if (this.handle) { this.handle.unsubscribe(); } this.$scope.loading = true; + function map(func){ + return function (objects) { + return Promise.all(objects.map(func)); + } + } + + function add(object){ + return function (objects) { + objects.unshift(object); + return objects; + } + } + + function subscribeTo(object) { + return telemetryApi.request(object, {}); + } + + function error() { + console.log("Unable to subscribe"); + } + + this.openmct.composition.get(this.newObject) + .load() + .then(add(this.newObject)) + .then(map(subscribeTo)) + .then(function (telemetry) { + console.log(telemetry.length); + }).catch(error); + this.handle = this.$scope.domainObject && this.telemetryHandler.handle( - this.$scope.domainObject, - this.addRealtimeData.bind(this), - true // Lossless - ); + this.$scope.domainObject, + this.addRealtimeData.bind(this), + true // Lossless + ); this.handle.request({}).then(this.addHistoricalData.bind(this)); From 976333d7f740be8d6eb5a43c1c98dc2c15757dd8 Mon Sep 17 00:00:00 2001 From: Henry Date: Tue, 6 Dec 2016 18:04:47 -0800 Subject: [PATCH 15/90] [Tables] Support for subscriptions from new Telemetry API Historical and real-time data flowing Added formatting, and limits. Support telemetry objects themselves and not just composition of telemetry objects Apply default time range if none supplied (15 minutes) --- example/generator/bundle.js | 9 +- example/generator/src/generatorWorker.js | 8 +- platform/features/table/bundle.js | 68 +----- .../table/res/templates/rt-table.html | 12 - ...orical-table.html => telemetry-table.html} | 2 +- platform/features/table/src/DomainColumn.js | 62 ------ platform/features/table/src/NameColumn.js | 52 ----- platform/features/table/src/RangeColumn.js | 65 ------ .../features/table/src/TableConfiguration.js | 50 +++-- .../controllers/HistoricalTableController.js | 141 ------------ .../controllers/RealtimeTableController.js | 76 ------- .../controllers/TelemetryTableController.js | 206 +++++++++--------- 12 files changed, 159 insertions(+), 592 deletions(-) delete mode 100644 platform/features/table/res/templates/rt-table.html rename platform/features/table/res/templates/{historical-table.html => telemetry-table.html} (82%) delete mode 100644 platform/features/table/src/DomainColumn.js delete mode 100644 platform/features/table/src/NameColumn.js delete mode 100644 platform/features/table/src/RangeColumn.js delete mode 100644 platform/features/table/src/controllers/HistoricalTableController.js delete mode 100644 platform/features/table/src/controllers/RealtimeTableController.js diff --git a/example/generator/bundle.js b/example/generator/bundle.js index 259c5cff15..f1c0f83224 100644 --- a/example/generator/bundle.js +++ b/example/generator/bundle.js @@ -75,8 +75,7 @@ define([ }, { "key": "delta", - "name": "Delta", - "format": "example.delta" + "name": "Delta" } ], "priority": -1 @@ -103,11 +102,13 @@ define([ "domains": [ { "key": "utc", - "name": "Time" + "name": "Time", + "format": "utc" }, { "key": "yesterday", - "name": "Yesterday" + "name": "Yesterday", + "format": "utc" }, { "key": "delta", diff --git a/example/generator/src/generatorWorker.js b/example/generator/src/generatorWorker.js index bb4e55ca4b..091297e185 100644 --- a/example/generator/src/generatorWorker.js +++ b/example/generator/src/generatorWorker.js @@ -24,6 +24,7 @@ (function () { + var FIFTEEN_MINUTES = 15 * 60 * 1000; var handlers = { subscribe: onSubscribe, @@ -82,8 +83,11 @@ function onRequest(message) { var data = message.data; - if (!data.start || !data.end) { - throw new Error('missing start and end!'); + if (data.end == undefined) { + data.end = Date.now(); + } + if (data.start == undefined){ + data.start = data.end - FIFTEEN_MINUTES; } var now = Date.now(); diff --git a/platform/features/table/bundle.js b/platform/features/table/bundle.js index 02b78f847f..c034677a02 100644 --- a/platform/features/table/bundle.js +++ b/platform/features/table/bundle.js @@ -22,25 +22,21 @@ define([ "./src/directives/MCTTable", - "./src/controllers/RealtimeTableController", - "./src/controllers/HistoricalTableController", + "./src/controllers/TelemetryTableController", "./src/controllers/TableOptionsController", '../../commonUI/regions/src/Region', '../../commonUI/browse/src/InspectorRegion', "text!./res/templates/table-options-edit.html", - "text!./res/templates/rt-table.html", - "text!./res/templates/historical-table.html", + "text!./res/templates/telemetry-table.html", "legacyRegistry" ], function ( MCTTable, - RealtimeTableController, - HistoricalTableController, + TelemetryTableController, TableOptionsController, Region, InspectorRegion, tableOptionsEditTemplate, - rtTableTemplate, - historicalTableTemplate, + telemetryTableTemplate, legacyRegistry ) { /** @@ -65,9 +61,9 @@ define([ "types": [ { "key": "table", - "name": "Historical Telemetry Table", - "cssclass": "icon-tabular", - "description": "A static table of all values over time for all included telemetry elements. Rows are timestamped data values for each telemetry element; columns are data fields. The number of rows is based on the range of your query. New incoming data must be manually re-queried for.", + "name": "Telemetry Table", + "cssclass": "icon-tabular-realtime", + "description": "A table of values over a given time period. The table will be automatically updated with new values as they become available", "priority": 861, "features": "creation", "delegates": [ @@ -85,42 +81,13 @@ define([ "views": [ "table" ] - }, - { - "key": "rttable", - "name": "Real-time Telemetry Table", - "cssclass": "icon-tabular-realtime", - "description": "A scrolling table of latest values for all included telemetry elements. Rows are timestamped data values for each telemetry element; columns are data fields. New incoming data is automatically added to the view.", - "priority": 860, - "features": "creation", - "delegates": [ - "telemetry" - ], - "inspector": tableInspector, - "contains": [ - { - "has": "telemetry" - } - ], - "model": { - "composition": [] - }, - "views": [ - "rt-table", - "scrolling-table" - ] } ], "controllers": [ { - "key": "HistoricalTableController", - "implementation": HistoricalTableController, - "depends": ["$scope", "telemetryHandler", "telemetryFormatter", "$timeout", "openmct"] - }, - { - "key": "RealtimeTableController", - "implementation": RealtimeTableController, - "depends": ["$scope", "telemetryHandler", "telemetryFormatter", "openmct"] + "key": "TelemetryTableController", + "implementation": TelemetryTableController, + "depends": ["$scope", "openmct"] }, { "key": "TableOptionsController", @@ -131,21 +98,10 @@ define([ ], "views": [ { - "name": "Historical Table", + "name": "Telemetry Table", "key": "table", - "template": historicalTableTemplate, - "cssclass": "icon-tabular", - "needs": [ - "telemetry" - ], - "delegation": true, - "editable": false - }, - { - "name": "Real-time Table", - "key": "rt-table", "cssclass": "icon-tabular-realtime", - "template": rtTableTemplate, + "template": telemetryTableTemplate, "needs": [ "telemetry" ], diff --git a/platform/features/table/res/templates/rt-table.html b/platform/features/table/res/templates/rt-table.html deleted file mode 100644 index da08b0ee8e..0000000000 --- a/platform/features/table/res/templates/rt-table.html +++ /dev/null @@ -1,12 +0,0 @@ -
- - -
\ No newline at end of file diff --git a/platform/features/table/res/templates/historical-table.html b/platform/features/table/res/templates/telemetry-table.html similarity index 82% rename from platform/features/table/res/templates/historical-table.html rename to platform/features/table/res/templates/telemetry-table.html index c2abbf5708..6dae139263 100644 --- a/platform/features/table/res/templates/historical-table.html +++ b/platform/features/table/res/templates/telemetry-table.html @@ -1,4 +1,4 @@ -
0) { - self.addColumn(new NameColumn(), 0); - } } return this; }; @@ -99,9 +102,8 @@ define( * @returns {Array} The titles of the columns */ TableConfiguration.prototype.getHeaders = function () { - var self = this; return this.columns.map(function (column, i) { - return self.getColumnTitle(column) || 'Column ' + (i + 1); + return column.getTitle()|| 'Column ' + (i + 1); }); }; @@ -113,11 +115,11 @@ define( * @returns {Object} Key value pairs where the key is the column * title, and the value is the formatted value from the provided datum. */ - TableConfiguration.prototype.getRowValues = function (telemetryObject, datum) { + TableConfiguration.prototype.getRowValues = function (limitEvaluator, datum) { var self = this; return this.columns.reduce(function (rowObject, column, i) { var columnTitle = self.getColumnTitle(column) || 'Column ' + (i + 1), - columnValue = column.getValue(telemetryObject, datum); + columnValue = column.getValue(datum, limitEvaluator); if (columnValue !== undefined && columnValue.text === undefined) { columnValue.text = ''; diff --git a/platform/features/table/src/controllers/HistoricalTableController.js b/platform/features/table/src/controllers/HistoricalTableController.js deleted file mode 100644 index 0f56f6b4ee..0000000000 --- a/platform/features/table/src/controllers/HistoricalTableController.js +++ /dev/null @@ -1,141 +0,0 @@ -/***************************************************************************** - * Open MCT, Copyright (c) 2014-2016, United States Government - * as represented by the Administrator of the National Aeronautics and Space - * Administration. All rights reserved. - * - * Open MCT is licensed under the Apache License, Version 2.0 (the - * "License"); you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * http://www.apache.org/licenses/LICENSE-2.0. - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT - * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the - * License for the specific language governing permissions and limitations - * under the License. - * - * Open MCT includes source code licensed under additional open source - * licenses. See the Open Source Licenses file (LICENSES.md) included with - * this source code distribution or the Licensing information page available - * at runtime from the About dialog for additional information. - *****************************************************************************/ - -define( - [ - './TelemetryTableController' - ], - function (TableController) { - var BATCH_SIZE = 1000; - - /** - * Extends TelemetryTableController and adds real-time streaming - * support. - * @memberof platform/features/table - * @param $scope - * @param telemetryHandler - * @param telemetryFormatter - * @constructor - */ - function HistoricalTableController($scope, telemetryHandler, telemetryFormatter, $timeout, openmct) { - var self = this; - - this.$timeout = $timeout; - this.timeoutHandle = undefined; - this.batchSize = BATCH_SIZE; - - $scope.$on("$destroy", function () { - if (self.timeoutHandle) { - self.$timeout.cancel(self.timeoutHandle); - } - }); - - TableController.call(this, $scope, telemetryHandler, telemetryFormatter, openmct); - } - - HistoricalTableController.prototype = Object.create(TableController.prototype); - - /** - * Set provided row data on scope, and cancel loading spinner - * @private - */ - HistoricalTableController.prototype.doneProcessing = function (rowData) { - this.$scope.rows = rowData; - this.$scope.loading = false; - }; - - /** - * @private - */ - HistoricalTableController.prototype.registerChangeListeners = function () { - TableController.prototype.registerChangeListeners.call(this); - //Change of bounds in time conductor - this.changeListeners.push(this.$scope.$on('telemetry:display:bounds', - this.boundsChange.bind(this)) - ); - }; - - /** - * @private - */ - HistoricalTableController.prototype.boundsChange = function (event, bounds, follow) { - // If in follow mode, don't bother re-subscribing, data will be - // received from existing subscription. - if (follow !== true) { - this.subscribe(); - } - }; - - /** - * Processes an array of objects, formatting the telemetry available - * for them and setting it on scope when done - * @private - */ - HistoricalTableController.prototype.processTelemetryObjects = function (objects, offset, start, rowData) { - var telemetryObject = objects[offset], - series, - i = start, - pointCount, - end; - - //No more objects to process - if (!telemetryObject) { - return this.doneProcessing(rowData); - } - - series = this.handle.getSeries(telemetryObject); - - pointCount = series.getPointCount(); - end = Math.min(start + this.batchSize, pointCount); - - //Process rows in a batch with size not exceeding a maximum length - for (; i < end; i++) { - rowData.push(this.table.getRowValues(telemetryObject, - this.handle.makeDatum(telemetryObject, series, i))); - } - - //Done processing all rows for this object. - if (end >= pointCount) { - offset++; - end = 0; - } - - // Done processing either a batch or an object, yield process - // before continuing processing - this.timeoutHandle = this.$timeout(this.processTelemetryObjects.bind(this, objects, offset, end, rowData)); - }; - - /** - * Populates historical data on scope when it becomes available from - * the telemetry API - */ - HistoricalTableController.prototype.addHistoricalData = function () { - if (this.timeoutHandle) { - this.$timeout.cancel(this.timeoutHandle); - } - - this.timeoutHandle = this.$timeout(this.processTelemetryObjects.bind(this, this.handle.getTelemetryObjects(), 0, 0, [])); - }; - - return HistoricalTableController; - } -); diff --git a/platform/features/table/src/controllers/RealtimeTableController.js b/platform/features/table/src/controllers/RealtimeTableController.js deleted file mode 100644 index c6ff7b8aee..0000000000 --- a/platform/features/table/src/controllers/RealtimeTableController.js +++ /dev/null @@ -1,76 +0,0 @@ -/***************************************************************************** - * Open MCT, Copyright (c) 2014-2016, United States Government - * as represented by the Administrator of the National Aeronautics and Space - * Administration. All rights reserved. - * - * Open MCT is licensed under the Apache License, Version 2.0 (the - * "License"); you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * http://www.apache.org/licenses/LICENSE-2.0. - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT - * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the - * License for the specific language governing permissions and limitations - * under the License. - * - * Open MCT includes source code licensed under additional open source - * licenses. See the Open Source Licenses file (LICENSES.md) included with - * this source code distribution or the Licensing information page available - * at runtime from the About dialog for additional information. - *****************************************************************************/ - -define( - [ - './TelemetryTableController' - ], - function (TableController) { - - /** - * Extends TelemetryTableController and adds real-time streaming - * support. - * @memberof platform/features/table - * @param $scope - * @param telemetryHandler - * @param telemetryFormatter - * @constructor - */ - function RealtimeTableController($scope, telemetryHandler, telemetryFormatter, openmct) { - TableController.call(this, $scope, telemetryHandler, telemetryFormatter, openmct); - - this.maxRows = 100000; - } - - RealtimeTableController.prototype = Object.create(TableController.prototype); - - /** - * Overrides method on TelemetryTableController providing handling - * for realtime data. - */ - RealtimeTableController.prototype.addRealtimeData = function () { - var self = this, - datum, - row; - this.handle.getTelemetryObjects().forEach(function (telemetryObject) { - datum = self.handle.getDatum(telemetryObject); - if (datum) { - //Populate row values from telemetry datum - row = self.table.getRowValues(telemetryObject, datum); - self.$scope.rows.push(row); - - //Inform table that a new row has been added - if (self.$scope.rows.length > self.maxRows) { - self.$scope.$broadcast('remove:row', 0); - self.$scope.rows.shift(); - } - - self.$scope.$broadcast('add:row', - self.$scope.rows.length - 1); - } - }); - this.$scope.loading = false; - }; - - return RealtimeTableController; - } -); diff --git a/platform/features/table/src/controllers/TelemetryTableController.js b/platform/features/table/src/controllers/TelemetryTableController.js index d845c13d2f..8eea6887dc 100644 --- a/platform/features/table/src/controllers/TelemetryTableController.js +++ b/platform/features/table/src/controllers/TelemetryTableController.js @@ -38,14 +38,10 @@ define( * configuration, and telemetry subscriptions. * @memberof platform/features/table * @param $scope - * @param telemetryHandler - * @param telemetryFormatter * @constructor */ function TelemetryTableController( $scope, - telemetryHandler, - telemetryFormatter, openmct ) { var self = this; @@ -53,9 +49,8 @@ define( this.$scope = $scope; this.columns = {}; //Range and Domain columns this.handle = undefined; - this.telemetryHandler = telemetryHandler; this.table = new TableConfiguration($scope.domainObject, - telemetryFormatter); + openmct); this.changeListeners = []; this.conductor = openmct.conductor; this.openmct = openmct; @@ -68,6 +63,9 @@ define( self.subscribe(); self.registerChangeListeners(); }); + this.mutationListener = openmct.objects.observe(this.newObject, "*", function (domainObject){ + self.newObject = domainObject; + }); this.destroy = this.destroy.bind(this); @@ -79,6 +77,8 @@ define( this.sortByTimeSystem = this.sortByTimeSystem.bind(this); this.conductor.on('timeSystem', this.sortByTimeSystem); this.conductor.off('timeSystem', this.sortByTimeSystem); + + this.subscriptions = []; } /** @@ -130,29 +130,12 @@ define( * Release the current subscription (called when scope is destroyed) */ TelemetryTableController.prototype.destroy = function () { - if (this.handle) { - this.handle.unsubscribe(); - this.handle = undefined; - } + this.subscriptions.forEach(function (subscription) { + subscription() + }); + this.mutationListener(); }; - /** - * Function for handling realtime data when it is available. This - * will be called by the telemetry framework when new data is - * available. - * - * Method should be overridden by specializing class. - */ - TelemetryTableController.prototype.addRealtimeData = function () { - }; - - /** - * Function for handling historical data. Will be called by - * telemetry framework when requested historical data is available. - * Should be overridden by specializing class. - */ - TelemetryTableController.prototype.addHistoricalData = function () { - }; /** Create a new subscription. This can be overridden by children to @@ -160,94 +143,123 @@ define( only). */ TelemetryTableController.prototype.subscribe = function () { + var self = this; var telemetryApi = this.openmct.telemetry; + var compositionApi = this.openmct.composition; + var subscriptions = this.subscriptions; + var tableConfiguration = this.table; + var scope = this.$scope; + var maxRows = 100000; + var conductor = this.conductor; + var newObject = this.newObject; - if (this.handle) { - this.handle.unsubscribe(); - } this.$scope.loading = true; - function map(func){ - return function (objects) { - return Promise.all(objects.map(func)); + function makeTableRows(object, historicalData){ + var limitEvaluator = telemetryApi.limitEvaluator(object); + return historicalData.map(tableConfiguration.getRowValues.bind(tableConfiguration, limitEvaluator)); + } + + function requestData(objects) { + var bounds = conductor.bounds(); + + return Promise.all( + objects.map(function (object) { + return telemetryApi.request(object, { + start: bounds.start, + end: bounds.end + }).then( + makeTableRows.bind(this, object) + ); + }) + ); + } + + function addHistoricalData(historicalData){ + scope.rows = Array.prototype.concat.apply([], historicalData); + scope.loading = false; + } + + function newData(domainObject, datum) { + scope.rows.push(tableConfiguration.getRowValues(datum, telemetryApi.limitEvaluator(domainObject))); + + //Inform table that a new row has been added + if (scope.rows.length > maxRows) { + scope.$broadcast('remove:row', 0); + scope.rows.shift(); } + + scope.$broadcast('add:row', + scope.rows.length - 1); + } - function add(object){ - return function (objects) { - objects.unshift(object); - return objects; - } + function subscribe(objects) { + objects.forEach(function (object){ + subscriptions.push(telemetryApi.subscribe(object, newData.bind(this, object), {})); + }); + return objects; } - function subscribeTo(object) { - return telemetryApi.request(object, {}); + function error(e) { + throw e; } - function error() { - console.log("Unable to subscribe"); + function loadColumns(objects) { + var metadatas = objects.map(telemetryApi.getMetadata.bind(telemetryApi)); + var allColumns = telemetryApi.commonValuesForHints(metadatas, []); + + tableConfiguration.populateColumns(allColumns); + + this.timeColumns = telemetryApi.commonValuesForHints(metadatas, ['x']).map(function (metadatum){ + return metadatum.name; + }); + + self.filterColumns(); + + return Promise.resolve(objects); } - this.openmct.composition.get(this.newObject) - .load() - .then(add(this.newObject)) - .then(map(subscribeTo)) - .then(function (telemetry) { - console.log(telemetry.length); - }).catch(error); - - this.handle = this.$scope.domainObject && this.telemetryHandler.handle( - this.$scope.domainObject, - this.addRealtimeData.bind(this), - true // Lossless - ); - - this.handle.request({}).then(this.addHistoricalData.bind(this)); - - this.setup(); - }; - - TelemetryTableController.prototype.populateColumns = function (telemetryMetadata) { - this.table.populateColumns(telemetryMetadata); - - //Identify time columns - telemetryMetadata.forEach(function (metadatum) { - //Push domains first - (metadatum.domains || []).forEach(function (domainMetadata) { - this.timeColumns.push(domainMetadata.name); - }.bind(this)); - }.bind(this)); - - var timeSystem = this.conductor.timeSystem(); - if (timeSystem) { - this.sortByTimeSystem(timeSystem); + function filterForTelemetry(objects){ + return objects.filter(telemetryApi.canProvideTelemetry.bind(telemetryApi)); } - }; - /** - * Setup table columns based on domain object metadata - */ - TelemetryTableController.prototype.setup = function () { - var handle = this.handle, - self = this; + function getDomainObjects() { + return new Promise(function (resolve, reject){ + var objects = [newObject]; + var composition = compositionApi.get(newObject); - if (handle) { - this.timeColumns = []; - handle.promiseTelemetryObjects().then(function () { - self.$scope.headers = []; - self.$scope.rows = []; - - self.populateColumns(handle.getMetadata()); - self.filterColumns(); - - // When table column configuration changes, (due to being - // selected or deselected), filter columns appropriately. - self.changeListeners.push(self.$scope.$watchCollection( - 'domainObject.getModel().configuration.table.columns', - self.filterColumns.bind(self) - )); + if (composition) { + composition + .load() + .then(function (children) { + return objects.concat(children); + }) + .then(resolve) + .catch(reject); + } else { + return resolve(objects); + } }); } + + scope.headers = []; + scope.rows = []; + + getDomainObjects() + .then(filterForTelemetry) + .catch(error) + .then(function (objects){ + if (objects.length > 0){ + return loadColumns(objects) + .then(subscribe) + .then(requestData) + .then(addHistoricalData) + .catch(error); + } else { + scope.loading = false; + } + }) }; /** From 3544caf4be5ae933548993c30c53813a24d706cd Mon Sep 17 00:00:00 2001 From: Henry Date: Thu, 15 Dec 2016 15:21:45 -0800 Subject: [PATCH 16/90] [API] Observer path was accessing object key incorrectly --- src/api/objects/MutableObject.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/api/objects/MutableObject.js b/src/api/objects/MutableObject.js index 1eb5fe4e0e..e4a1d477c9 100644 --- a/src/api/objects/MutableObject.js +++ b/src/api/objects/MutableObject.js @@ -41,7 +41,7 @@ define([ } function qualifiedEventName(object, eventName) { - return [object.key.identifier, eventName].join(':'); + return [object.identifier.key, eventName].join(':'); } MutableObject.prototype.stopListening = function () { From 2a4944d6ee44b732c98083075945b6de53bb2c25 Mon Sep 17 00:00:00 2001 From: Henry Date: Fri, 16 Dec 2016 16:34:41 -0800 Subject: [PATCH 17/90] [Tables] Refactoring for consolidation of historical and real-time tables Added batch processing of large historical queries. #1077 --- platform/features/plot/src/PlotController.js | 2 +- platform/features/table/bundle.js | 2 +- .../table/res/templates/mct-table.html | 8 +- .../table/res/templates/telemetry-table.html | 2 +- .../features/table/src/TableConfiguration.js | 1 + .../src/controllers/MCTTableController.js | 118 ++++-- .../controllers/TelemetryTableController.js | 379 ++++++++++++------ .../features/table/src/directives/MCTTable.js | 6 +- src/api/telemetry/TelemetryValueFormatter.js | 24 +- 9 files changed, 365 insertions(+), 177 deletions(-) diff --git a/platform/features/plot/src/PlotController.js b/platform/features/plot/src/PlotController.js index e4be264ea9..ea34e1b878 100644 --- a/platform/features/plot/src/PlotController.js +++ b/platform/features/plot/src/PlotController.js @@ -217,8 +217,8 @@ define( if (handle) { handle.unsubscribe(); handle = undefined; - conductor.off("timeOfInterest", changeTimeOfInterest); } + conductor.off("timeOfInterest", changeTimeOfInterest); } function requery() { diff --git a/platform/features/table/bundle.js b/platform/features/table/bundle.js index c034677a02..b5d67d626b 100644 --- a/platform/features/table/bundle.js +++ b/platform/features/table/bundle.js @@ -87,7 +87,7 @@ define([ { "key": "TelemetryTableController", "implementation": TelemetryTableController, - "depends": ["$scope", "openmct"] + "depends": ["$scope", "$timeout", "openmct"] }, { "key": "TableOptionsController", diff --git a/platform/features/table/res/templates/mct-table.html b/platform/features/table/res/templates/mct-table.html index 7e24be2c43..3a805bf4e0 100644 --- a/platform/features/table/res/templates/mct-table.html +++ b/platform/features/table/res/templates/mct-table.html @@ -5,7 +5,7 @@ Export
-
+
@@ -32,8 +32,7 @@ enableSort ? 'sortable' : '', sortColumn === header ? 'sort' : '', sortDirection || '' - ].join(' ')" - ng-click="toggleSort(header)"> + ].join(' ')"> {{ header }} @@ -59,8 +58,7 @@ + ng-style="{ top: visibleRow.offsetY + 'px' }">
\ No newline at end of file diff --git a/platform/features/table/src/TableConfiguration.js b/platform/features/table/src/TableConfiguration.js index f6d33d3269..a63ea569d6 100644 --- a/platform/features/table/src/TableConfiguration.js +++ b/platform/features/table/src/TableConfiguration.js @@ -53,6 +53,7 @@ define( var formatter = telemetryApi.getValueFormatter(metadatum); self.addColumn({ + metadata: metadatum, getTitle: function () { return metadatum.name; }, diff --git a/platform/features/table/src/controllers/MCTTableController.js b/platform/features/table/src/controllers/MCTTableController.js index 3ab1887c29..e9c18400ee 100644 --- a/platform/features/table/src/controllers/MCTTableController.js +++ b/platform/features/table/src/controllers/MCTTableController.js @@ -12,12 +12,12 @@ define( * @param element * @constructor */ - function MCTTableController($scope, $timeout, element, exportService, formatService, openmct) { + function MCTTableController($scope, $window, element, exportService, formatService, openmct) { var self = this; this.$scope = $scope; this.element = $(element[0]); - this.$timeout = $timeout; + this.$window = $window; this.maxDisplayRows = 50; this.scrollable = this.element.find('.l-view-section.scrolling').first(); @@ -27,15 +27,39 @@ define( this.conductor = openmct.conductor; this.toiFormatter = undefined; this.formatService = formatService; + this.callbacks = {}; //Bind all class functions to 'this' - Object.keys(MCTTableController.prototype).filter(function (key) { - return typeof MCTTableController.prototype[key] === 'function'; - }).forEach(function (key) { - this[key] = MCTTableController.prototype[key].bind(this); - }.bind(this)); + _.bindAll(this, [ + 'destroyConductorListeners', + 'changeTimeSystem', + 'scrollToBottom', + 'addRow', + 'removeRow', + 'onScroll', + 'firstVisible', + 'lastVisible', + 'setVisibleRows', + 'setHeaders', + 'setElementSizes', + 'binarySearch', + 'insertSorted', + 'sortComparator', + 'sortRows', + 'buildLargestRow', + 'resize', + 'filterAndSort', + 'setRows', + 'filterRows', + 'scrollToRow', + 'setTimeOfInterestRow', + 'changeTimeOfInterest', + 'changeBounds', + 'onRowClick', + 'digest' + ]); - this.scrollable.on('scroll', this.onScroll.bind(this)); + this.scrollable.on('scroll', this.onScroll); $scope.visibleRows = []; @@ -86,7 +110,7 @@ define( $scope.sortDirection = 'asc'; } self.setRows($scope.rows); - self.setTimeOfInterest(self.conductor.timeOfInterest()); + self.setTimeOfInterestRow(self.conductor.timeOfInterest()); }; /* @@ -108,7 +132,11 @@ define( * Populated from the default-sort attribute on MctTable * directive tag. */ - $scope.$watch('sortColumn', $scope.toggleSort); + $scope.$watch('defaultSort', function (newColumn, oldColumn) { + if (newColumn !== oldColumn) { + $scope.toggleSort(newColumn) + } + }); /* * Listen for resize events to trigger recalculation of table width @@ -125,7 +153,7 @@ define( this.destroyConductorListeners(); this.conductor.on('timeSystem', this.changeTimeSystem); - this.conductor.on('timeOfInterest', this.setTimeOfInterest); + this.conductor.on('timeOfInterest', this.changeTimeOfInterest); this.conductor.on('bounds', this.changeBounds); // If time system defined, set initially @@ -135,12 +163,22 @@ define( } }.bind(this)); - $scope.$on('$destroy', this.destroyConductorListeners); - } + console.log('constructed'); + + $scope.$on('$destroy', function() { + this.scrollable.off('scroll', this.onScroll); + this.destroyConductorListeners(); + + // In case for some reason this controller instance lingers around, + // destroy scope as it can be extremely large for large tables. + delete this.$scope; + + }.bind(this)); + }; MCTTableController.prototype.destroyConductorListeners = function () { this.conductor.off('timeSystem', this.changeTimeSystem); - this.conductor.off('timeOfInterest', this.setTimeOfInterest); + this.conductor.off('timeOfInterest', this.changeTimeOfInterest); this.conductor.off('bounds', this.changeBounds); }; @@ -160,7 +198,7 @@ define( //Use timeout to defer execution until next digest when any // pending UI changes have completed, eg. a new row in the table. if (this.$scope.autoScroll) { - this.$timeout(function () { + this.digest(function () { self.scrollable[0].scrollTop = self.scrollable[0].scrollHeight; }); } @@ -183,6 +221,12 @@ define( this.resize([this.$scope.sizingRow, row]) .then(this.setVisibleRows.bind(this)) .then(this.scrollToBottom.bind(this)); + + var toi = this.conductor.timeOfInterest(); + if (toi !== -1) { + this.setTimeOfInterestRow(toi); + } + } }; @@ -193,8 +237,8 @@ define( */ MCTTableController.prototype.removeRow = function (event, rowIndex) { var row = this.$scope.rows[rowIndex], - // Do a sequential search here. Only way of finding row is by - // object equality, so array is in effect unsorted. + // Do a sequential search here. Only way of finding row is by + // object equality, so array is in effect unsorted. indexInDisplayRows = this.$scope.displayRows.indexOf(row); if (indexInDisplayRows !== -1) { this.$scope.displayRows.splice(indexInDisplayRows, 1); @@ -522,6 +566,27 @@ define( return largestRow; }; + MCTTableController.prototype.digest = function (callback) { + var scope = this.$scope; + var callbacks = this.callbacks; + var requestAnimationFrame = this.$window.requestAnimationFrame; + + var promise = callbacks[callback]; + + if (!promise){ + promise = new Promise(function (resolve) { + requestAnimationFrame(function() { + scope.$digest(); + delete callbacks[callback]; + resolve(callback && callback()); + }); + }); + callbacks[callback] = promise; + } + + return promise; + }; + /** * Calculates the widest row in the table, and if necessary, resizes * the table accordingly @@ -533,7 +598,7 @@ define( */ MCTTableController.prototype.resize = function (rows) { this.$scope.sizingRow = this.buildLargestRow(rows); - return this.$timeout(this.setElementSizes.bind(this)); + return this.digest(this.setElementSizes); }; /** @@ -566,15 +631,15 @@ define( .then(this.setVisibleRows) //Timeout following setVisibleRows to allow digest to // perform DOM changes, otherwise scrollTo won't work. - .then(this.$timeout) + .then(this.digest) .then(function () { //If TOI specified, scroll to it var timeOfInterest = this.conductor.timeOfInterest(); if (timeOfInterest) { - this.setTimeOfInterest(timeOfInterest); + this.setTimeOfInterestRow(timeOfInterest); + this.scrollToRow(this.$scope.toiRowIndex); } }.bind(this)); - }; /** @@ -635,7 +700,7 @@ define( * Update rows with new data. If filtering is enabled, rows * will be sorted before display. */ - MCTTableController.prototype.setTimeOfInterest = function (newTOI) { + MCTTableController.prototype.setTimeOfInterestRow = function (newTOI) { var isSortedByTime = this.$scope.timeColumns && this.$scope.timeColumns.indexOf(this.$scope.sortColumn) !== -1; @@ -652,17 +717,22 @@ define( if (rowIndex > 0 && rowIndex < this.$scope.displayRows.length) { this.$scope.toiRowIndex = rowIndex; - this.scrollToRow(this.$scope.toiRowIndex); } } }; + MCTTableController.prototype.changeTimeOfInterest = function (newTOI) { + this.setTimeOfInterestRow(newTOI); + this.scrollToRow(this.$scope.toiRowIndex); + }; + /** * On zoom, pan, etc. reset TOI * @param bounds */ MCTTableController.prototype.changeBounds = function (bounds) { - this.setTimeOfInterest(this.conductor.timeOfInterest()); + this.setTimeOfInterestRow(this.conductor.timeOfInterest()); + this.scrollToRow(this.$scope.toiRowIndex); }; /** diff --git a/platform/features/table/src/controllers/TelemetryTableController.js b/platform/features/table/src/controllers/TelemetryTableController.js index 8eea6887dc..5aa89e8f1e 100644 --- a/platform/features/table/src/controllers/TelemetryTableController.js +++ b/platform/features/table/src/controllers/TelemetryTableController.js @@ -42,43 +42,47 @@ define( */ function TelemetryTableController( $scope, + $timeout, openmct ) { - var self = this; - this.$scope = $scope; + this.$timeout = $timeout; + this.openmct = openmct; + this.batchSize = 1000; + + /* + * Initialization block + */ this.columns = {}; //Range and Domain columns - this.handle = undefined; + this.deregisterListeners = []; + this.subscriptions = []; + this.timeColumns = []; + $scope.rows = []; this.table = new TableConfiguration($scope.domainObject, openmct); - this.changeListeners = []; - this.conductor = openmct.conductor; - this.openmct = openmct; - this.newObject = objectUtils.toNewFormat($scope.domainObject.getModel(), $scope.domainObject.getId()); + this.lastBounds = this.openmct.conductor.bounds(); + this.requestTime = 0; - $scope.rows = []; + /* + * Create a new format object from legacy object, and replace it + * when it changes + */ + this.newObject = objectUtils.toNewFormat($scope.domainObject.getModel(), + $scope.domainObject.getId()); - // Subscribe to telemetry when a domain object becomes available - this.$scope.$watch('domainObject', function () { - self.subscribe(); - self.registerChangeListeners(); - }); - this.mutationListener = openmct.objects.observe(this.newObject, "*", function (domainObject){ - self.newObject = domainObject; - }); + _.bindAll(this, [ + 'destroy', + 'sortByTimeSystem', + 'loadColumns', + 'getHistoricalData', + 'subscribeToNewData', + 'changeBounds' + ]); - this.destroy = this.destroy.bind(this); + this.getData(); + this.registerChangeListeners(); - // Unsubscribe when the plot is destroyed this.$scope.$on("$destroy", this.destroy); - this.timeColumns = []; - - - this.sortByTimeSystem = this.sortByTimeSystem.bind(this); - this.conductor.on('timeSystem', this.sortByTimeSystem); - this.conductor.off('timeSystem', this.sortByTimeSystem); - - this.subscriptions = []; } /** @@ -91,133 +95,254 @@ define( scope.defaultSort = undefined; if (timeSystem) { this.table.columns.forEach(function (column) { - if (column.domainMetadata && column.domainMetadata.key === timeSystem.metadata.key) { + if (column.metadata.key === timeSystem.metadata.key) { scope.defaultSort = column.getTitle(); } }); + this.$scope.rows = _.sortBy(this.$scope.rows, function (row) { + return row[this.$scope.defaultSort]; + }); } }; - TelemetryTableController.prototype.unregisterChangeListeners = function () { - this.changeListeners.forEach(function (listener) { - return listener && listener(); - }); - this.changeListeners = []; - }; - /** - * Defer registration of change listeners until domain object is - * available in order to avoid race conditions + * Attach listeners to domain object to respond to changes due to + * composition, etc. * @private */ TelemetryTableController.prototype.registerChangeListeners = function () { - var self = this; - this.unregisterChangeListeners(); + this.deregisterListeners.forEach(function (deregister){ + deregister(); + }); + this.deregisterListeners = []; - // When composition changes, re-subscribe to the various - // telemetry subscriptions - this.changeListeners.push(this.$scope.$watchCollection( - 'domainObject.getModel().composition', - function (newVal, oldVal) { - if (newVal !== oldVal) { - self.subscribe(); - } - }) + this.deregisterListeners.push( + this.openmct.objects.observe(this.newObject, "*", + function (domainObject){ + this.newObject = domainObject; + this.getData(); + }.bind(this) + ) ); + this.openmct.conductor.on('timeSystem', this.sortByTimeSystem); + this.openmct.conductor.on('bounds', this.changeBounds); + }; + + TelemetryTableController.prototype.tick = function (bounds) { + // Can't do ticking until we change how data is handled + // Pass raw values to table, with format function + + /*if (this.$scope.defaultSort) { + this.$scope.rows.filter(function (row){ + return row[] + }) + }*/ + }; + + TelemetryTableController.prototype.changeBounds = function (bounds) { + var follow = this.openmct.conductor.follow(); + var isTick = follow && + bounds.start !== this.lastBounds.start && + bounds.end !== this.lastBounds.end; + var isDeltaChange = follow && + !isTick && + (bounds.start !== this.lastBounds.start || + bounds.end !== this.lastBounds.end); + + if (isTick){ + // Treat it as a realtime tick + // Drop old data that falls outside of bounds + this.tick(bounds); + } else if (isDeltaChange){ + // No idea... + // Historical query for bounds, then tick on + this.getData(); + } else { + // Is fixed bounds change + this.getData(); + } + this.lastBounds = bounds; }; /** * Release the current subscription (called when scope is destroyed) */ TelemetryTableController.prototype.destroy = function () { + + this.openmct.conductor.off('timeSystem', this.sortByTimeSystem); + this.openmct.conductor.off('bounds', this.changeBounds); + this.subscriptions.forEach(function (subscription) { - subscription() + subscription(); }); - this.mutationListener(); + this.deregisterListeners.forEach(function (deregister){ + deregister(); + }); + this.subscriptions = []; + this.deregisterListeners = []; + + if (this.timeoutHandle) { + this.$timeout.cancel(this.timeoutHandle); + } + + // In case controller instance lingers around (currently there is a + // temporary memory leak with PlotController), clean up scope as it + // can be extremely large. + this.$scope = null; + this.table = null; }; + /** + * @private + * @param objects + * @returns {*} + */ + TelemetryTableController.prototype.loadColumns = function (objects) { + var telemetryApi = this.openmct.telemetry; + + if (objects.length > 0) { + var metadatas = objects.map(telemetryApi.getMetadata.bind(telemetryApi)); + var allColumns = telemetryApi.commonValuesForHints(metadatas, []); + + this.table.populateColumns(allColumns); + + this.timeColumns = telemetryApi.commonValuesForHints(metadatas, ['x']).map(function (metadatum) { + return metadatum.name; + }); + + this.filterColumns(); + + var timeSystem = this.openmct.conductor.timeSystem(); + if (timeSystem) { + this.sortByTimeSystem(timeSystem); + } + } + return objects; + }; /** - Create a new subscription. This can be overridden by children to - change default behaviour (which is to retrieve historical telemetry - only). + * @private + * @param objects The domain objects to request telemetry for + * @returns {*|{configFile}|app|boolean|Route|Object} */ - TelemetryTableController.prototype.subscribe = function () { - var self = this; + TelemetryTableController.prototype.getHistoricalData = function (objects) { + var openmct = this.openmct; + var bounds = openmct.conductor.bounds(); + var scope = this.$scope; + var processedObjects = 0; + var requestTime = this.lastRequestTime = Date.now(); + + return new Promise(function (resolve, reject){ + console.log('Created promise'); + function finishProcessing(tableRows){ + scope.rows = tableRows; + scope.loading = false; + console.log('Resolved promise'); + resolve(tableRows); + } + + function processData(historicalData, index, rowData, limitEvaluator){ + console.log("Processing batch"); + if (index >= historicalData.length) { + processedObjects++; + + if (processedObjects === objects.length) { + finishProcessing(rowData); + } + } else { + rowData = rowData.concat(historicalData.slice(index, index + this.batchSize) + .map(this.table.getRowValues.bind(this.table, limitEvaluator))); + this.timeoutHandle = this.$timeout(processData.bind( + this, + historicalData, + index + this.batchSize, + rowData, + limitEvaluator + )); + } + } + + function makeTableRows(object, historicalData) { + // Only process one request at a time + if (requestTime === this.lastRequestTime) { + console.log('Processing request'); + var limitEvaluator = openmct.telemetry.limitEvaluator(object); + processData.call(this, historicalData, 0, [], limitEvaluator); + } else { + console.log('Ignoring returned data because of staleness'); + resolve([]); + } + } + + function requestData (object) { + return openmct.telemetry.request(object, { + start: bounds.start, + end: bounds.end + }).then(makeTableRows.bind(this, object)) + .catch(reject); + } + this.$timeout.cancel(this.timeoutHandle); + + if (objects.length > 0){ + objects.forEach(requestData.bind(this)); + } else { + scope.loading = false; + console.log('Resolved promise'); + resolve([]); + } + }.bind(this)); + }; + + /** + * @private + * @param objects + * @returns {*} + */ + TelemetryTableController.prototype.subscribeToNewData = function (objects) { + var telemetryApi = this.openmct.telemetry; + //Set table max length to avoid unbounded growth. + var maxRows = 100000; + + this.subscriptions.forEach(function (subscription) { + subscription(); + }); + this.subscriptions = []; + + function newData(domainObject, datum) { + this.$scope.rows.push(this.table.getRowValues( + telemetryApi.limitEvaluator(domainObject), datum)); + + //Inform table that a new row has been added + if (this.$scope.rows.length > maxRows) { + this.$scope.$broadcast('remove:row', 0); + this.$scope.rows.shift(); + } + + this.$scope.$broadcast('add:row', + this.$scope.rows.length - 1); + + } + + objects.forEach(function (object){ + this.subscriptions.push( + telemetryApi.subscribe(object, newData.bind(this, object), {})); + console.log('subscribed'); + }.bind(this)); + + return objects; + }; + + TelemetryTableController.prototype.getData = function () { var telemetryApi = this.openmct.telemetry; var compositionApi = this.openmct.composition; - var subscriptions = this.subscriptions; - var tableConfiguration = this.table; var scope = this.$scope; - var maxRows = 100000; - var conductor = this.conductor; var newObject = this.newObject; this.$scope.loading = true; - function makeTableRows(object, historicalData){ - var limitEvaluator = telemetryApi.limitEvaluator(object); - return historicalData.map(tableConfiguration.getRowValues.bind(tableConfiguration, limitEvaluator)); - } - - function requestData(objects) { - var bounds = conductor.bounds(); - - return Promise.all( - objects.map(function (object) { - return telemetryApi.request(object, { - start: bounds.start, - end: bounds.end - }).then( - makeTableRows.bind(this, object) - ); - }) - ); - } - - function addHistoricalData(historicalData){ - scope.rows = Array.prototype.concat.apply([], historicalData); - scope.loading = false; - } - - function newData(domainObject, datum) { - scope.rows.push(tableConfiguration.getRowValues(datum, telemetryApi.limitEvaluator(domainObject))); - - //Inform table that a new row has been added - if (scope.rows.length > maxRows) { - scope.$broadcast('remove:row', 0); - scope.rows.shift(); - } - - scope.$broadcast('add:row', - scope.rows.length - 1); - - } - - function subscribe(objects) { - objects.forEach(function (object){ - subscriptions.push(telemetryApi.subscribe(object, newData.bind(this, object), {})); - }); - return objects; - } - function error(e) { - throw e; - } - - function loadColumns(objects) { - var metadatas = objects.map(telemetryApi.getMetadata.bind(telemetryApi)); - var allColumns = telemetryApi.commonValuesForHints(metadatas, []); - - tableConfiguration.populateColumns(allColumns); - - this.timeColumns = telemetryApi.commonValuesForHints(metadatas, ['x']).map(function (metadatum){ - return metadatum.name; - }); - - self.filterColumns(); - - return Promise.resolve(objects); + scope.loading = false; + console.error(e); } function filterForTelemetry(objects){ @@ -248,18 +373,10 @@ define( getDomainObjects() .then(filterForTelemetry) + .then(this.loadColumns) + //.then(this.subscribeToNewData) + .then(this.getHistoricalData) .catch(error) - .then(function (objects){ - if (objects.length > 0){ - return loadColumns(objects) - .then(subscribe) - .then(requestData) - .then(addHistoricalData) - .catch(error); - } else { - scope.loading = false; - } - }) }; /** diff --git a/platform/features/table/src/directives/MCTTable.js b/platform/features/table/src/directives/MCTTable.js index dad23c2eb5..b240fa7f43 100644 --- a/platform/features/table/src/directives/MCTTable.js +++ b/platform/features/table/src/directives/MCTTable.js @@ -77,13 +77,13 @@ define( * * @constructor */ - function MCTTable($timeout) { + function MCTTable() { return { restrict: "E", template: TableTemplate, controller: [ '$scope', - '$timeout', + '$window', '$element', 'exportService', 'formatService', @@ -104,7 +104,7 @@ define( timeColumns: "=?", // Indicate a column to sort on. Allows control of sort // via configuration (eg. for default sort column). - sortColumn: "=?" + defaultSort: "=?" } }; } diff --git a/src/api/telemetry/TelemetryValueFormatter.js b/src/api/telemetry/TelemetryValueFormatter.js index 801aee1176..a5e8cb8720 100644 --- a/src/api/telemetry/TelemetryValueFormatter.js +++ b/src/api/telemetry/TelemetryValueFormatter.js @@ -28,6 +28,18 @@ define([ // TODO: needs reference to formatService; function TelemetryValueFormatter(valueMetadata, formatService) { + var numberFormatter = { + parse: function (x) { + return Number(x); + }, + format: function (x) { + return x; + }, + validate: function (x) { + return true; + } + }; + this.valueMetadata = valueMetadata; this.parseCache = new WeakMap(); this.formatCache = new WeakMap(); @@ -36,17 +48,7 @@ define([ .getFormat(valueMetadata.format, valueMetadata); } catch (e) { // TODO: Better formatting - this.formatter = { - parse: function (x) { - return Number(x); - }, - format: function (x) { - return x; - }, - validate: function (x) { - return true; - } - }; + this.formatter = numberFormatter; } if (valueMetadata.type === 'enum') { From 50f303bbdc020c842731465696df1929b465168a Mon Sep 17 00:00:00 2001 From: Henry Date: Sun, 15 Jan 2017 10:59:28 -0800 Subject: [PATCH 18/90] [Tables] limit digests to increase performance --- .../table/res/templates/mct-table.html | 8 +- .../table/res/templates/telemetry-table.html | 5 +- .../src/controllers/MCTTableController.js | 78 ++++++++++--------- .../controllers/TelemetryTableController.js | 44 ++++++----- 4 files changed, 74 insertions(+), 61 deletions(-) diff --git a/platform/features/table/res/templates/mct-table.html b/platform/features/table/res/templates/mct-table.html index 3a805bf4e0..7e24be2c43 100644 --- a/platform/features/table/res/templates/mct-table.html +++ b/platform/features/table/res/templates/mct-table.html @@ -5,7 +5,7 @@ Export -
+
@@ -32,7 +32,8 @@ enableSort ? 'sortable' : '', sortColumn === header ? 'sort' : '', sortDirection || '' - ].join(' ')"> + ].join(' ')" + ng-click="toggleSort(header)"> {{ header }} @@ -58,7 +59,8 @@ + ng-style="{ top: visibleRow.offsetY + 'px' }" + ng-click="table.onRowClick($event, visibleRow.rowIndex) ">
+ Request time: {{tableController.lastRequestTime | date : 'HH:mm:ss:sss'}} diff --git a/platform/features/table/src/controllers/MCTTableController.js b/platform/features/table/src/controllers/MCTTableController.js index e9c18400ee..aef6e68240 100644 --- a/platform/features/table/src/controllers/MCTTableController.js +++ b/platform/features/table/src/controllers/MCTTableController.js @@ -163,8 +163,6 @@ define( } }.bind(this)); - console.log('constructed'); - $scope.$on('$destroy', function() { this.scrollable.off('scroll', this.onScroll); this.destroyConductorListeners(); @@ -193,15 +191,7 @@ define( * @private */ MCTTableController.prototype.scrollToBottom = function () { - var self = this; - - //Use timeout to defer execution until next digest when any - // pending UI changes have completed, eg. a new row in the table. - if (this.$scope.autoScroll) { - this.digest(function () { - self.scrollable[0].scrollTop = self.scrollable[0].scrollHeight; - }); - } + this.scrollable[0].scrollTop = this.scrollable[0].scrollHeight; }; /** @@ -219,8 +209,12 @@ define( //Resize the columns , then update the rows visible in the table this.resize([this.$scope.sizingRow, row]) - .then(this.setVisibleRows.bind(this)) - .then(this.scrollToBottom.bind(this)); + .then(this.setVisibleRows) + .then(function () { + if (this.$scope.autoScroll) { + this.scrollToBottom(); + } + }.bind(this)); var toi = this.conductor.timeOfInterest(); if (toi !== -1) { @@ -250,16 +244,24 @@ define( * @private */ MCTTableController.prototype.onScroll = function (event) { - //If user scrolls away from bottom, disable auto-scroll. - // Auto-scroll will be re-enabled if user scrolls to bottom again. - if (this.scrollable[0].scrollTop < - (this.scrollable[0].scrollHeight - this.scrollable[0].offsetHeight)) { - this.$scope.autoScroll = false; - } else { - this.$scope.autoScroll = true; - } - this.setVisibleRows(); - this.$scope.$digest(); + if (!this.scrolling) { + this.scrolling = true; + + requestAnimationFrame(function () { + this.setVisibleRows(); + this.digest(); + + // If user scrolls away from bottom, disable auto-scroll. + // Auto-scroll will be re-enabled if user scrolls to bottom again. + if (this.scrollable[0].scrollTop < + (this.scrollable[0].scrollHeight - this.scrollable[0].offsetHeight) - 20) { + this.$scope.autoScroll = false; + } else { + this.$scope.autoScroll = true; + } + this.scrolling = false; + }.bind(this)); + } }; /** @@ -338,7 +340,7 @@ define( this.$scope.visibleRows[this.$scope.visibleRows.length - 1] .rowIndex === end) { - return; // don't update if no changes are required. + return Promise.resolve(); // don't update if no changes are required. } } //Set visible rows from display rows, based on calculated offset. @@ -351,6 +353,7 @@ define( contents: row }; }); + return this.digest(); }; /** @@ -566,25 +569,25 @@ define( return largestRow; }; - MCTTableController.prototype.digest = function (callback) { + // Will effectively cap digests at 60Hz + // Also turns digest into a promise allowing code to force digest, then + // schedule something to happen afterwards + MCTTableController.prototype.digest = function () { var scope = this.$scope; - var callbacks = this.callbacks; + var self = this; var requestAnimationFrame = this.$window.requestAnimationFrame; - var promise = callbacks[callback]; - - if (!promise){ - promise = new Promise(function (resolve) { + if (!this.digestPromise) { + this.digestPromise = new Promise(function (resolve) { requestAnimationFrame(function() { scope.$digest(); - delete callbacks[callback]; - resolve(callback && callback()); + delete self.digestPromise; + resolve(); }); }); - callbacks[callback] = promise; } - return promise; + return this.digestPromise; }; /** @@ -598,7 +601,7 @@ define( */ MCTTableController.prototype.resize = function (rows) { this.$scope.sizingRow = this.buildLargestRow(rows); - return this.digest(this.setElementSizes); + return this.digest().then(this.setElementSizes); }; /** @@ -631,7 +634,6 @@ define( .then(this.setVisibleRows) //Timeout following setVisibleRows to allow digest to // perform DOM changes, otherwise scrollTo won't work. - .then(this.digest) .then(function () { //If TOI specified, scroll to it var timeOfInterest = this.conductor.timeOfInterest(); @@ -732,7 +734,9 @@ define( */ MCTTableController.prototype.changeBounds = function (bounds) { this.setTimeOfInterestRow(this.conductor.timeOfInterest()); - this.scrollToRow(this.$scope.toiRowIndex); + if (this.$scope.toiRowIndex !== -1) { + this.scrollToRow(this.$scope.toiRowIndex); + } }; /** diff --git a/platform/features/table/src/controllers/TelemetryTableController.js b/platform/features/table/src/controllers/TelemetryTableController.js index 5aa89e8f1e..f015d11cab 100644 --- a/platform/features/table/src/controllers/TelemetryTableController.js +++ b/platform/features/table/src/controllers/TelemetryTableController.js @@ -76,15 +76,23 @@ define( 'loadColumns', 'getHistoricalData', 'subscribeToNewData', - 'changeBounds' + 'changeBounds', + 'setScroll' ]); this.getData(); this.registerChangeListeners(); + this.openmct.conductor.on('follow', this.setScroll); + this.setScroll(this.openmct.conductor.follow()); + this.$scope.$on("$destroy", this.destroy); } + TelemetryTableController.prototype.setScroll = function (scroll){ + this.$scope.autoScroll = scroll; + }; + /** * Based on the selected time system, find a matching domain column * to sort by. By default will just match on key. @@ -171,6 +179,7 @@ define( this.openmct.conductor.off('timeSystem', this.sortByTimeSystem); this.openmct.conductor.off('bounds', this.changeBounds); + this.openmct.conductor.off('follow', this.setScroll); this.subscriptions.forEach(function (subscription) { subscription(); @@ -229,20 +238,18 @@ define( var openmct = this.openmct; var bounds = openmct.conductor.bounds(); var scope = this.$scope; + var rowData = []; var processedObjects = 0; var requestTime = this.lastRequestTime = Date.now(); return new Promise(function (resolve, reject){ - console.log('Created promise'); function finishProcessing(tableRows){ - scope.rows = tableRows; + scope.rows = tableRows.concat(scope.rows); scope.loading = false; - console.log('Resolved promise'); resolve(tableRows); } - function processData(historicalData, index, rowData, limitEvaluator){ - console.log("Processing batch"); + function processData(historicalData, index, limitEvaluator){ if (index >= historicalData.length) { processedObjects++; @@ -250,13 +257,14 @@ define( finishProcessing(rowData); } } else { - rowData = rowData.concat(historicalData.slice(index, index + this.batchSize) - .map(this.table.getRowValues.bind(this.table, limitEvaluator))); + rowData = rowData.concat( + historicalData.slice(index, index + this.batchSize).map( + this.table.getRowValues.bind(this.table, limitEvaluator)) + ); this.timeoutHandle = this.$timeout(processData.bind( this, historicalData, index + this.batchSize, - rowData, limitEvaluator )); } @@ -265,12 +273,10 @@ define( function makeTableRows(object, historicalData) { // Only process one request at a time if (requestTime === this.lastRequestTime) { - console.log('Processing request'); var limitEvaluator = openmct.telemetry.limitEvaluator(object); - processData.call(this, historicalData, 0, [], limitEvaluator); + processData.call(this, historicalData, 0, limitEvaluator); } else { - console.log('Ignoring returned data because of staleness'); - resolve([]); + resolve(rowData); } } @@ -287,7 +293,6 @@ define( objects.forEach(requestData.bind(this)); } else { scope.loading = false; - console.log('Resolved promise'); resolve([]); } }.bind(this)); @@ -317,16 +322,15 @@ define( this.$scope.$broadcast('remove:row', 0); this.$scope.rows.shift(); } - - this.$scope.$broadcast('add:row', - this.$scope.rows.length - 1); - + if (!this.$scope.loading) { + this.$scope.$broadcast('add:row', + this.$scope.rows.length - 1); + } } objects.forEach(function (object){ this.subscriptions.push( telemetryApi.subscribe(object, newData.bind(this, object), {})); - console.log('subscribed'); }.bind(this)); return objects; @@ -374,7 +378,7 @@ define( getDomainObjects() .then(filterForTelemetry) .then(this.loadColumns) - //.then(this.subscribeToNewData) + .then(this.subscribeToNewData) .then(this.getHistoricalData) .catch(error) }; From 0c3ff82cfefdff624f5352ac8192844a38385639 Mon Sep 17 00:00:00 2001 From: Henry Date: Tue, 17 Jan 2017 14:44:09 -0800 Subject: [PATCH 19/90] [Table] Added ticking to combined historical/real-time table Don't add duplicate telemetry data --- .../utcTimeSystem/src/UTCTimeSystem.js | 2 +- .../table/res/templates/telemetry-table.html | 2 +- .../features/table/src/TableConfiguration.js | 3 +- .../features/table/src/TelemetryCollection.js | 114 ++++++++++++++++++ .../src/controllers/MCTTableController.js | 70 ++++++----- .../controllers/TelemetryTableController.js | 62 +++++----- .../features/table/src/directives/MCTTable.js | 1 + 7 files changed, 195 insertions(+), 59 deletions(-) create mode 100644 platform/features/table/src/TelemetryCollection.js diff --git a/platform/features/conductor/utcTimeSystem/src/UTCTimeSystem.js b/platform/features/conductor/utcTimeSystem/src/UTCTimeSystem.js index 1c4e317682..671be1bfff 100644 --- a/platform/features/conductor/utcTimeSystem/src/UTCTimeSystem.js +++ b/platform/features/conductor/utcTimeSystem/src/UTCTimeSystem.js @@ -25,7 +25,7 @@ define([ '../../core/src/timeSystems/LocalClock' ], function (TimeSystem, LocalClock) { var FIFTEEN_MINUTES = 15 * 60 * 1000, - DEFAULT_PERIOD = 1000; + DEFAULT_PERIOD = 100; /** * This time system supports UTC dates and provides a ticking clock source. diff --git a/platform/features/table/res/templates/telemetry-table.html b/platform/features/table/res/templates/telemetry-table.html index 310225b47c..24c6a7702f 100644 --- a/platform/features/table/res/templates/telemetry-table.html +++ b/platform/features/table/res/templates/telemetry-table.html @@ -5,7 +5,7 @@ headers="headers" rows="rows" time-columns="tableController.timeColumns" - on-show-cell="" + format-cell="formatCell" enableFilter="true" enableSort="true" auto-scroll="autoScroll" diff --git a/platform/features/table/src/TableConfiguration.js b/platform/features/table/src/TableConfiguration.js index a63ea569d6..2ba908953d 100644 --- a/platform/features/table/src/TableConfiguration.js +++ b/platform/features/table/src/TableConfiguration.js @@ -66,7 +66,8 @@ define( return { cssClass: alarm && alarm.cssClass, text: formatter ? formatter.format(telemetryDatum[metadatum.key]) - : telemetryDatum[metadatum.key] + : telemetryDatum[metadatum.key], + value: telemetryDatum[metadatum.key] } } }); diff --git a/platform/features/table/src/TelemetryCollection.js b/platform/features/table/src/TelemetryCollection.js new file mode 100644 index 0000000000..16bf3a27ba --- /dev/null +++ b/platform/features/table/src/TelemetryCollection.js @@ -0,0 +1,114 @@ +/***************************************************************************** + * Open MCT, Copyright (c) 2014-2016, United States Government + * as represented by the Administrator of the National Aeronautics and Space + * Administration. All rights reserved. + * + * Open MCT is licensed under the Apache License, Version 2.0 (the + * "License"); you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * http://www.apache.org/licenses/LICENSE-2.0. + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT + * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the + * License for the specific language governing permissions and limitations + * under the License. + * + * Open MCT includes source code licensed under additional open source + * licenses. See the Open Source Licenses file (LICENSES.md) included with + * this source code distribution or the Licensing information page available + * at runtime from the About dialog for additional information. + *****************************************************************************/ + +define( + ['lodash'], + function (_) { + function TelemetryCollection() { + this.telemetry = []; + this.sortField = undefined; + this.lastBounds = {}; + + _.bindAll(this,[ + 'iteratee' + ]); + } + + TelemetryCollection.prototype.iteratee = function (element) { + return _.get(element, this.sortField); + }; + + TelemetryCollection.prototype.bounds = function (bounds) { + var startChanged = this.lastBounds.start !== bounds.start; + var endChanged = this.lastBounds.end !== bounds.end; + var fromStart = 0; + var fromEnd = 0; + var discarded = []; + + if (startChanged){ + var testValue = _.set({}, this.sortField, bounds.start); + fromStart = _.sortedIndex(this.telemetry, testValue, this.sortField); + discarded = this.telemetry.splice(0, fromStart); + } + if (endChanged) { + var testValue = _.set({}, this.sortField, bounds.end); + fromEnd = _.sortedLastIndex(this.telemetry, testValue, this.sortField); + discarded = discarded.concat(this.telemetry.splice(fromEnd, this.telemetry.length - fromEnd)); + } + this.lastBounds = bounds; + return discarded; + }; + + TelemetryCollection.prototype.isValid = function (element) { + var noBoundsDefined = !this.lastBounds || (!this.lastBounds.start && !this.lastBounds.end); + var withinBounds = _.get(element, this.sortField) >= this.lastBounds.start && + _.get(element, this.sortField) <= this.lastBounds.end; + + return noBoundsDefined || withinBounds; + }; + + TelemetryCollection.prototype.add = function (element) { + //console.log('data: ' + element.Time.value); + if (this.isValid(element)){ + // Going to check for duplicates. Bound the search problem to + // elements around the given time. Use sortedIndex because it + // employs a binary search which is O(log n). Can use binary search + // based on time stamp because the array is guaranteed ordered due + // to sorted insertion. + + var isDuplicate = false; + var startIx = _.sortedIndex(this.telemetry, element, this.sortField); + + if (startIx !== this.telemetry.length) { + var endIx = _.sortedLastIndex(this.telemetry, element, this.sortField); + + // Create an array of potential dupes, based on having the + // same time stamp + var potentialDupes = this.telemetry.slice(startIx, endIx + 1); + // Search potential dupes for exact dupe + isDuplicate = _.findIndex(potentialDupes, _.isEqual.bind(undefined, element)) > -1; + } + + if (!isDuplicate) { + this.telemetry.splice(startIx, 0, element); + return true; + } else { + return false; + } + + } else { + return false; + } + }; + + TelemetryCollection.prototype.clear = function () { + this.telemetry = undefined; + }; + + TelemetryCollection.prototype.sort = function (sortField){ + this.sortField = sortField; + this.telemetry = _.sortBy(this.telemetry, this.iteratee); + }; + + return TelemetryCollection; + } +); diff --git a/platform/features/table/src/controllers/MCTTableController.js b/platform/features/table/src/controllers/MCTTableController.js index aef6e68240..e347ca048f 100644 --- a/platform/features/table/src/controllers/MCTTableController.js +++ b/platform/features/table/src/controllers/MCTTableController.js @@ -35,7 +35,7 @@ define( 'changeTimeSystem', 'scrollToBottom', 'addRow', - 'removeRow', + 'removeRows', 'onScroll', 'firstVisible', 'lastVisible', @@ -126,7 +126,7 @@ define( * Listen for rows added individually (eg. for real-time tables) */ $scope.$on('add:row', this.addRow); - $scope.$on('remove:row', this.removeRow); + $scope.$on('remove:rows', this.removeRows); /** * Populated from the default-sort attribute on MctTable @@ -229,39 +229,47 @@ define( * `remove:row` broadcast event. * @private */ - MCTTableController.prototype.removeRow = function (event, rowIndex) { - var row = this.$scope.rows[rowIndex], - // Do a sequential search here. Only way of finding row is by - // object equality, so array is in effect unsorted. + MCTTableController.prototype.removeRows = function (event, rows) { + var indexInDisplayRows; + rows.forEach(function (row){ + // Do a sequential search here. Only way of finding row is by + // object equality, so array is in effect unsorted. indexInDisplayRows = this.$scope.displayRows.indexOf(row); - if (indexInDisplayRows !== -1) { - this.$scope.displayRows.splice(indexInDisplayRows, 1); - this.setVisibleRows(); - } + if (indexInDisplayRows !== -1) { + this.$scope.displayRows.splice(indexInDisplayRows, 1); + } + }, this); + + this.$scope.sizingRow = this.buildLargestRow([this.$scope.sizingRow].concat(rows)); + + this.setElementSizes(); + this.setVisibleRows() + .then(function () { + if (this.$scope.autoScroll) { + this.scrollToBottom(); + } + }.bind(this)); + }; /** * @private */ MCTTableController.prototype.onScroll = function (event) { - if (!this.scrolling) { - this.scrolling = true; + requestAnimationFrame(function () { + this.setVisibleRows(); + this.digest(); - requestAnimationFrame(function () { - this.setVisibleRows(); - this.digest(); - - // If user scrolls away from bottom, disable auto-scroll. - // Auto-scroll will be re-enabled if user scrolls to bottom again. - if (this.scrollable[0].scrollTop < - (this.scrollable[0].scrollHeight - this.scrollable[0].offsetHeight) - 20) { - this.$scope.autoScroll = false; - } else { - this.$scope.autoScroll = true; - } - this.scrolling = false; - }.bind(this)); + // If user scrolls away from bottom, disable auto-scroll. + // Auto-scroll will be re-enabled if user scrolls to bottom again. + if (this.scrollable[0].scrollTop < + (this.scrollable[0].scrollHeight - this.scrollable[0].offsetHeight) - 20) { + this.$scope.autoScroll = false; + } else { + this.$scope.autoScroll = true; } + this.scrolling = false; + }.bind(this)); }; /** @@ -339,13 +347,19 @@ define( this.$scope.visibleRows[0].rowIndex === start && this.$scope.visibleRows[this.$scope.visibleRows.length - 1] .rowIndex === end) { - - return Promise.resolve(); // don't update if no changes are required. + return this.digest(); + //return Promise.resolve(); // don't update if no changes are required. } } //Set visible rows from display rows, based on calculated offset. this.$scope.visibleRows = this.$scope.displayRows.slice(start, end) .map(function (row, i) { +/* var formattedRow = JSON.parse(JSON.stringify(row)); + if (self.$scope.formatCell) { + Object.keys(formattedRow).forEach(function (header) { + formattedRow[header].text = self.$scope.formatCell(header, row[header].text); + }); + } */ return { rowIndex: start + i, offsetY: ((start + i) * self.$scope.rowHeight) + diff --git a/platform/features/table/src/controllers/TelemetryTableController.js b/platform/features/table/src/controllers/TelemetryTableController.js index f015d11cab..bd98ad6976 100644 --- a/platform/features/table/src/controllers/TelemetryTableController.js +++ b/platform/features/table/src/controllers/TelemetryTableController.js @@ -27,10 +27,11 @@ define( [ '../TableConfiguration', - '../../../../../src/api/objects/object-utils' + '../../../../../src/api/objects/object-utils', + '../TelemetryCollection' ], - function (TableConfiguration, objectUtils) { + function (TableConfiguration, objectUtils, TelemetryCollection) { /** * The TableController is responsible for getting data onto the page @@ -62,6 +63,7 @@ define( openmct); this.lastBounds = this.openmct.conductor.bounds(); this.requestTime = 0; + this.telemetry = new TelemetryCollection(); /* * Create a new format object from legacy object, and replace it @@ -136,18 +138,8 @@ define( this.openmct.conductor.on('bounds', this.changeBounds); }; - TelemetryTableController.prototype.tick = function (bounds) { - // Can't do ticking until we change how data is handled - // Pass raw values to table, with format function - - /*if (this.$scope.defaultSort) { - this.$scope.rows.filter(function (row){ - return row[] - }) - }*/ - }; - TelemetryTableController.prototype.changeBounds = function (bounds) { + //console.log('bounds.end: ' + bounds.end); var follow = this.openmct.conductor.follow(); var isTick = follow && bounds.start !== this.lastBounds.start && @@ -157,10 +149,16 @@ define( (bounds.start !== this.lastBounds.start || bounds.end !== this.lastBounds.end); + var discarded = this.telemetry.bounds(bounds); + + if (discarded.length > 0){ + this.$scope.$broadcast('remove:rows', discarded); + } + if (isTick){ // Treat it as a realtime tick // Drop old data that falls outside of bounds - this.tick(bounds); + //this.tick(bounds); } else if (isDeltaChange){ // No idea... // Historical query for bounds, then tick on @@ -214,11 +212,13 @@ define( var allColumns = telemetryApi.commonValuesForHints(metadatas, []); this.table.populateColumns(allColumns); - this.timeColumns = telemetryApi.commonValuesForHints(metadatas, ['x']).map(function (metadatum) { return metadatum.name; }); + // For now, use first time field for time conductor + this.telemetry.sort(this.timeColumns[0] + '.value'); + this.filterColumns(); var timeSystem = this.openmct.conductor.timeSystem(); @@ -241,12 +241,13 @@ define( var rowData = []; var processedObjects = 0; var requestTime = this.lastRequestTime = Date.now(); + var telemetryCollection = this.telemetry; return new Promise(function (resolve, reject){ - function finishProcessing(tableRows){ - scope.rows = tableRows.concat(scope.rows); + function finishProcessing(){ + scope.rows = telemetryCollection.telemetry; scope.loading = false; - resolve(tableRows); + resolve(scope.rows); } function processData(historicalData, index, limitEvaluator){ @@ -254,13 +255,14 @@ define( processedObjects++; if (processedObjects === objects.length) { - finishProcessing(rowData); + finishProcessing(); } } else { - rowData = rowData.concat( - historicalData.slice(index, index + this.batchSize).map( - this.table.getRowValues.bind(this.table, limitEvaluator)) - ); + historicalData.slice(index, index + this.batchSize) + .forEach(function (datum) { + telemetryCollection.add(this.table.getRowValues( + limitEvaluator, datum)); + }.bind(this)); this.timeoutHandle = this.$timeout(processData.bind( this, historicalData, @@ -305,8 +307,12 @@ define( */ TelemetryTableController.prototype.subscribeToNewData = function (objects) { var telemetryApi = this.openmct.telemetry; + var telemetryCollection = this.telemetry; //Set table max length to avoid unbounded growth. - var maxRows = 100000; + //var maxRows = 100000; + var maxRows = Number.MAX_VALUE; + var limitEvaluator; + var added = false; this.subscriptions.forEach(function (subscription) { subscription(); @@ -314,15 +320,15 @@ define( this.subscriptions = []; function newData(domainObject, datum) { - this.$scope.rows.push(this.table.getRowValues( - telemetryApi.limitEvaluator(domainObject), datum)); + limitEvaluator = telemetryApi.limitEvaluator(domainObject); + added = telemetryCollection.add(this.table.getRowValues(limitEvaluator, datum)); //Inform table that a new row has been added if (this.$scope.rows.length > maxRows) { - this.$scope.$broadcast('remove:row', 0); + this.$scope.$broadcast('remove:rows', this.$scope.rows[0]); this.$scope.rows.shift(); } - if (!this.$scope.loading) { + if (!this.$scope.loading && added) { this.$scope.$broadcast('add:row', this.$scope.rows.length - 1); } diff --git a/platform/features/table/src/directives/MCTTable.js b/platform/features/table/src/directives/MCTTable.js index b240fa7f43..70a2b6665c 100644 --- a/platform/features/table/src/directives/MCTTable.js +++ b/platform/features/table/src/directives/MCTTable.js @@ -94,6 +94,7 @@ define( scope: { headers: "=", rows: "=", + formatCell: "=?", enableFilter: "=?", enableSort: "=?", autoScroll: "=?", From ae2b73a4f5a004bd05d62f00488ac9b8b81e2f6d Mon Sep 17 00:00:00 2001 From: Henry Date: Thu, 19 Jan 2017 21:18:53 -0800 Subject: [PATCH 20/90] [Tables] Increase default table size --- .../features/table/src/TelemetryCollection.js | 104 +++++++++++------- .../src/controllers/MCTTableController.js | 2 +- 2 files changed, 67 insertions(+), 39 deletions(-) diff --git a/platform/features/table/src/TelemetryCollection.js b/platform/features/table/src/TelemetryCollection.js index 16bf3a27ba..d5816b37b6 100644 --- a/platform/features/table/src/TelemetryCollection.js +++ b/platform/features/table/src/TelemetryCollection.js @@ -21,44 +21,72 @@ *****************************************************************************/ define( - ['lodash'], - function (_) { + [ + 'lodash', + 'eventEmitter' + ], + function (_, eventEmitter) { function TelemetryCollection() { + eventEmitter.call(this, arguments); this.telemetry = []; + this.forwardBuffer = []; this.sortField = undefined; this.lastBounds = {}; + this.lastStartIndex = 0; + this.lastEndIndex = 0; _.bindAll(this,[ 'iteratee' ]); } + TelemetryCollection.prototype = Object.create(eventEmitter.prototype); + TelemetryCollection.prototype.iteratee = function (element) { return _.get(element, this.sortField); }; - TelemetryCollection.prototype.bounds = function (bounds) { + /** + * This function is optimized for ticking - it assumes that start and end bounds + * will only increase and as such this cannot be used for decreasing bounds changes. + * For arbitrary bounds changes, it's assumed that a telemetry requery is performed anyway, and the + * collection is cleared and repopulated. + * @param bounds + */ + TelemetryCollection.prototype.tick = function (bounds) { var startChanged = this.lastBounds.start !== bounds.start; var endChanged = this.lastBounds.end !== bounds.end; - var fromStart = 0; - var fromEnd = 0; - var discarded = []; + var startIndex = 0; + var endIndex = 0; + var discarded = undefined; + var added = undefined; if (startChanged){ var testValue = _.set({}, this.sortField, bounds.start); - fromStart = _.sortedIndex(this.telemetry, testValue, this.sortField); - discarded = this.telemetry.splice(0, fromStart); + // Calculate the new index of the first element within the bounds + startIndex = _.sortedIndex(this.telemetry, testValue, this.sortField); + discarded = this.telemetry.slice(this.lastStartIndex, startIndex + 1); } if (endChanged) { var testValue = _.set({}, this.sortField, bounds.end); - fromEnd = _.sortedLastIndex(this.telemetry, testValue, this.sortField); - discarded = discarded.concat(this.telemetry.splice(fromEnd, this.telemetry.length - fromEnd)); + // Calculate the new index of the last element in bounds + endIndex = _.sortedLastIndex(this.telemetry, testValue, this.sortField); + added = this.telemetry.slice(this.lastEndIndex, endIndex + 1); + } + + if (discarded.length > 0){ + this.emit('discarded', discarded); + } + if (added.length > 0){ + this.emit('added', added); } this.lastBounds = bounds; - return discarded; }; - TelemetryCollection.prototype.isValid = function (element) { + /*collection.on('added'); + collection.on('discarded');*/ + + TelemetryCollection.prototype.inBounds = function (element) { var noBoundsDefined = !this.lastBounds || (!this.lastBounds.start && !this.lastBounds.end); var withinBounds = _.get(element, this.sortField) >= this.lastBounds.start && _.get(element, this.sortField) <= this.lastBounds.end; @@ -66,37 +94,37 @@ define( return noBoundsDefined || withinBounds; }; + //Todo: addAll for initial historical data TelemetryCollection.prototype.add = function (element) { - //console.log('data: ' + element.Time.value); - if (this.isValid(element)){ - // Going to check for duplicates. Bound the search problem to - // elements around the given time. Use sortedIndex because it - // employs a binary search which is O(log n). Can use binary search - // based on time stamp because the array is guaranteed ordered due - // to sorted insertion. + // Going to check for duplicates. Bound the search problem to + // elements around the given time. Use sortedIndex because it + // employs a binary search which is O(log n). Can use binary search + // based on time stamp because the array is guaranteed ordered due + // to sorted insertion. - var isDuplicate = false; - var startIx = _.sortedIndex(this.telemetry, element, this.sortField); + var isDuplicate = false; + var startIx = _.sortedIndex(this.telemetry, element, this.sortField); - if (startIx !== this.telemetry.length) { - var endIx = _.sortedLastIndex(this.telemetry, element, this.sortField); + if (startIx !== this.telemetry.length) { + var endIx = _.sortedLastIndex(this.telemetry, element, this.sortField); - // Create an array of potential dupes, based on having the - // same time stamp - var potentialDupes = this.telemetry.slice(startIx, endIx + 1); - // Search potential dupes for exact dupe - isDuplicate = _.findIndex(potentialDupes, _.isEqual.bind(undefined, element)) > -1; + // Create an array of potential dupes, based on having the + // same time stamp + var potentialDupes = this.telemetry.slice(startIx, endIx + 1); + // Search potential dupes for exact dupe + isDuplicate = _.findIndex(potentialDupes, _.isEqual.bind(undefined, element)) > -1; + } + + if (!isDuplicate) { + this.telemetry.splice(startIx, 0, element); + + if (this.inBounds(element)) { + // If new element is within bounds, then the index within the + // master of the last element in bounds has just increased by one. + this.lastEndIndex++; + //If the new element is within bounds, add it immediately + this.emit('added', [element]); } - - if (!isDuplicate) { - this.telemetry.splice(startIx, 0, element); - return true; - } else { - return false; - } - - } else { - return false; } }; diff --git a/platform/features/table/src/controllers/MCTTableController.js b/platform/features/table/src/controllers/MCTTableController.js index e347ca048f..7ae11eae0c 100644 --- a/platform/features/table/src/controllers/MCTTableController.js +++ b/platform/features/table/src/controllers/MCTTableController.js @@ -18,7 +18,7 @@ define( this.$scope = $scope; this.element = $(element[0]); this.$window = $window; - this.maxDisplayRows = 50; + this.maxDisplayRows = 100; this.scrollable = this.element.find('.l-view-section.scrolling').first(); this.resultsHeader = this.element.find('.mct-table>thead').first(); From 6cd99efbb972ef0476a08659d7357b4a2fdaf6ed Mon Sep 17 00:00:00 2001 From: Henry Date: Mon, 23 Jan 2017 12:43:59 -0800 Subject: [PATCH 21/90] [Tables] Added telemetry buffer so that subscription data is not discarded if it's beyond the end bounds --- example/generator/src/generatorWorker.js | 1 + .../features/table/src/TelemetryCollection.js | 118 +++++++++++------- .../src/controllers/MCTTableController.js | 58 ++++----- .../controllers/TelemetryTableController.js | 71 ++++++----- .../controllers/MCTTableControllerSpec.js | 18 +-- 5 files changed, 152 insertions(+), 114 deletions(-) diff --git a/example/generator/src/generatorWorker.js b/example/generator/src/generatorWorker.js index 091297e185..bbf1851346 100644 --- a/example/generator/src/generatorWorker.js +++ b/example/generator/src/generatorWorker.js @@ -52,6 +52,7 @@ function onSubscribe(message) { var data = message.data; + // Keep var start = Date.now(); var step = 1000 / data.dataRateInHz; var nextStep = start - (start % step) + step; diff --git a/platform/features/table/src/TelemetryCollection.js b/platform/features/table/src/TelemetryCollection.js index d5816b37b6..185c1549af 100644 --- a/platform/features/table/src/TelemetryCollection.js +++ b/platform/features/table/src/TelemetryCollection.js @@ -23,24 +23,23 @@ define( [ 'lodash', - 'eventEmitter' + 'EventEmitter' ], - function (_, eventEmitter) { + function (_, EventEmitter) { function TelemetryCollection() { - eventEmitter.call(this, arguments); + EventEmitter.call(this, arguments); this.telemetry = []; - this.forwardBuffer = []; + this.highBuffer = []; this.sortField = undefined; this.lastBounds = {}; - this.lastStartIndex = 0; - this.lastEndIndex = 0; _.bindAll(this,[ + 'addOne', 'iteratee' ]); } - TelemetryCollection.prototype = Object.create(eventEmitter.prototype); + TelemetryCollection.prototype = Object.create(EventEmitter.prototype); TelemetryCollection.prototype.iteratee = function (element) { return _.get(element, this.sortField); @@ -53,7 +52,7 @@ define( * collection is cleared and repopulated. * @param bounds */ - TelemetryCollection.prototype.tick = function (bounds) { + TelemetryCollection.prototype.bounds = function (bounds) { var startChanged = this.lastBounds.start !== bounds.start; var endChanged = this.lastBounds.end !== bounds.end; var startIndex = 0; @@ -65,71 +64,106 @@ define( var testValue = _.set({}, this.sortField, bounds.start); // Calculate the new index of the first element within the bounds startIndex = _.sortedIndex(this.telemetry, testValue, this.sortField); - discarded = this.telemetry.slice(this.lastStartIndex, startIndex + 1); + discarded = this.telemetry.splice(0, startIndex); } if (endChanged) { var testValue = _.set({}, this.sortField, bounds.end); // Calculate the new index of the last element in bounds - endIndex = _.sortedLastIndex(this.telemetry, testValue, this.sortField); - added = this.telemetry.slice(this.lastEndIndex, endIndex + 1); + endIndex = _.sortedLastIndex(this.highBuffer, testValue, this.sortField); + added = this.highBuffer.splice(0, endIndex); + this.telemetry = this.telemetry.concat(added); } - if (discarded.length > 0){ + if (discarded && discarded.length > 0){ this.emit('discarded', discarded); } - if (added.length > 0){ + if (added && added.length > 0) { this.emit('added', added); } this.lastBounds = bounds; }; - /*collection.on('added'); - collection.on('discarded');*/ - TelemetryCollection.prototype.inBounds = function (element) { var noBoundsDefined = !this.lastBounds || (!this.lastBounds.start && !this.lastBounds.end); - var withinBounds = _.get(element, this.sortField) >= this.lastBounds.start && + var withinBounds = + _.get(element, this.sortField) >= this.lastBounds.start && _.get(element, this.sortField) <= this.lastBounds.end; return noBoundsDefined || withinBounds; }; - //Todo: addAll for initial historical data - TelemetryCollection.prototype.add = function (element) { - // Going to check for duplicates. Bound the search problem to - // elements around the given time. Use sortedIndex because it - // employs a binary search which is O(log n). Can use binary search - // based on time stamp because the array is guaranteed ordered due - // to sorted insertion. - + /** + * @private + * @param element + */ + TelemetryCollection.prototype.addOne = function (element) { var isDuplicate = false; - var startIx = _.sortedIndex(this.telemetry, element, this.sortField); + var boundsDefined = this.lastBounds && (this.lastBounds.start && this.lastBounds.end); + var array; - if (startIx !== this.telemetry.length) { - var endIx = _.sortedLastIndex(this.telemetry, element, this.sortField); + // Insert into either in-bounds array, or the out of bounds high buffer. + // Data in the high buffer will be re-evaluated for possible insertion on next tick - // Create an array of potential dupes, based on having the - // same time stamp - var potentialDupes = this.telemetry.slice(startIx, endIx + 1); - // Search potential dupes for exact dupe - isDuplicate = _.findIndex(potentialDupes, _.isEqual.bind(undefined, element)) > -1; + if (boundsDefined) { + var boundsHigh = _.get(element, this.sortField) > this.lastBounds.end; + var boundsLow = _.get(element, this.sortField) < this.lastBounds.start; + + if (!boundsHigh && !boundsLow) { + array = this.telemetry; + } else if (boundsHigh) { + array = this.highBuffer; + } + } else { + array = this.highBuffer; } - if (!isDuplicate) { - this.telemetry.splice(startIx, 0, element); + // If out of bounds low, disregard data + if (!boundsLow) { + // Going to check for duplicates. Bound the search problem to + // elements around the given time. Use sortedIndex because it + // employs a binary search which is O(log n). Can use binary search + // based on time stamp because the array is guaranteed ordered due + // to sorted insertion. - if (this.inBounds(element)) { - // If new element is within bounds, then the index within the - // master of the last element in bounds has just increased by one. - this.lastEndIndex++; - //If the new element is within bounds, add it immediately - this.emit('added', [element]); + var startIx = _.sortedIndex(array, element, this.sortField); + + if (startIx !== array.length) { + var endIx = _.sortedLastIndex(array, element, this.sortField); + + // Create an array of potential dupes, based on having the + // same time stamp + var potentialDupes = array.slice(startIx, endIx + 1); + // Search potential dupes for exact dupe + isDuplicate = _.findIndex(potentialDupes, _.isEqual.bind(undefined, element)) > -1; } + + if (!isDuplicate) { + array.splice(startIx, 0, element); + + //Return true if it was added and in bounds + return array === this.telemetry; + } + } + return false; + }; + + TelemetryCollection.prototype.addAll = function (elements) { + var added = elements.filter(this.addOne); + this.emit('added', added); + }; + + //Todo: addAll for initial historical data + TelemetryCollection.prototype.add = function (element) { + if (this.addOne(element)){ + this.emit('added', [element]); + return true; + } else { + return false; } }; TelemetryCollection.prototype.clear = function () { - this.telemetry = undefined; + this.telemetry = []; }; TelemetryCollection.prototype.sort = function (sortField){ diff --git a/platform/features/table/src/controllers/MCTTableController.js b/platform/features/table/src/controllers/MCTTableController.js index 7ae11eae0c..af942f8b55 100644 --- a/platform/features/table/src/controllers/MCTTableController.js +++ b/platform/features/table/src/controllers/MCTTableController.js @@ -31,32 +31,32 @@ define( //Bind all class functions to 'this' _.bindAll(this, [ - 'destroyConductorListeners', - 'changeTimeSystem', - 'scrollToBottom', - 'addRow', - 'removeRows', - 'onScroll', - 'firstVisible', - 'lastVisible', - 'setVisibleRows', - 'setHeaders', - 'setElementSizes', + 'addRows', 'binarySearch', - 'insertSorted', - 'sortComparator', - 'sortRows', 'buildLargestRow', - 'resize', - 'filterAndSort', - 'setRows', - 'filterRows', - 'scrollToRow', - 'setTimeOfInterestRow', - 'changeTimeOfInterest', 'changeBounds', + 'changeTimeOfInterest', + 'changeTimeSystem', + 'destroyConductorListeners', + 'digest', + 'filterAndSort', + 'filterRows', + 'firstVisible', + 'insertSorted', + 'lastVisible', 'onRowClick', - 'digest' + 'onScroll', + 'removeRows', + 'resize', + 'scrollToBottom', + 'scrollToRow', + 'setElementSizes', + 'setHeaders', + 'setRows', + 'setTimeOfInterestRow', + 'setVisibleRows', + 'sortComparator', + 'sortRows' ]); this.scrollable.on('scroll', this.onScroll); @@ -125,7 +125,7 @@ define( /* * Listen for rows added individually (eg. for real-time tables) */ - $scope.$on('add:row', this.addRow); + $scope.$on('add:rows', this.addRows); $scope.$on('remove:rows', this.removeRows); /** @@ -199,16 +199,13 @@ define( * `add:row` broadcast event. * @private */ - MCTTableController.prototype.addRow = function (event, rowIndex) { - var row = this.$scope.rows[rowIndex]; - + MCTTableController.prototype.addRows = function (event, rows) { //Does the row pass the current filter? - if (this.filterRows([row]).length === 1) { - //Insert the row into the correct position in the array - this.insertSorted(this.$scope.displayRows, row); + if (this.filterRows(rows).length > 0) { + rows.forEach(this.insertSorted.bind(this, this.$scope.displayRows)); //Resize the columns , then update the rows visible in the table - this.resize([this.$scope.sizingRow, row]) + this.resize([this.$scope.sizingRow].concat(rows)) .then(this.setVisibleRows) .then(function () { if (this.$scope.autoScroll) { @@ -220,7 +217,6 @@ define( if (toi !== -1) { this.setTimeOfInterestRow(toi); } - } }; diff --git a/platform/features/table/src/controllers/TelemetryTableController.js b/platform/features/table/src/controllers/TelemetryTableController.js index bd98ad6976..d4414b1004 100644 --- a/platform/features/table/src/controllers/TelemetryTableController.js +++ b/platform/features/table/src/controllers/TelemetryTableController.js @@ -79,7 +79,9 @@ define( 'getHistoricalData', 'subscribeToNewData', 'changeBounds', - 'setScroll' + 'setScroll', + 'addRowsToTable', + 'removeRowsFromTable', ]); this.getData(); @@ -88,6 +90,9 @@ define( this.openmct.conductor.on('follow', this.setScroll); this.setScroll(this.openmct.conductor.follow()); + this.telemetry.on('added', this.addRowsToTable); + this.telemetry.on('discarded', this.removeRowsFromTable); + this.$scope.$on("$destroy", this.destroy); } @@ -102,16 +107,19 @@ define( */ TelemetryTableController.prototype.sortByTimeSystem = function (timeSystem) { var scope = this.$scope; + var sortColumn = undefined; scope.defaultSort = undefined; + if (timeSystem) { this.table.columns.forEach(function (column) { if (column.metadata.key === timeSystem.metadata.key) { - scope.defaultSort = column.getTitle(); + sortColumn = column; } }); - this.$scope.rows = _.sortBy(this.$scope.rows, function (row) { - return row[this.$scope.defaultSort]; - }); + if (sortColumn) { + scope.defaultSort = sortColumn.getTitle(); + this.telemetry.sort(sortColumn.getTitle() + '.value'); + } } }; @@ -138,31 +146,23 @@ define( this.openmct.conductor.on('bounds', this.changeBounds); }; + TelemetryTableController.prototype.addRowsToTable = function (rows) { + this.$scope.$broadcast('add:rows', rows); + }; + + TelemetryTableController.prototype.removeRowsFromTable = function (rows) { + this.$scope.$broadcast('remove:rows', rows); + }; + TelemetryTableController.prototype.changeBounds = function (bounds) { //console.log('bounds.end: ' + bounds.end); var follow = this.openmct.conductor.follow(); var isTick = follow && bounds.start !== this.lastBounds.start && bounds.end !== this.lastBounds.end; - var isDeltaChange = follow && - !isTick && - (bounds.start !== this.lastBounds.start || - bounds.end !== this.lastBounds.end); - - var discarded = this.telemetry.bounds(bounds); - - if (discarded.length > 0){ - this.$scope.$broadcast('remove:rows', discarded); - } if (isTick){ - // Treat it as a realtime tick - // Drop old data that falls outside of bounds - //this.tick(bounds); - } else if (isDeltaChange){ - // No idea... - // Historical query for bounds, then tick on - this.getData(); + this.telemetry.bounds(bounds); } else { // Is fixed bounds change this.getData(); @@ -212,19 +212,23 @@ define( var allColumns = telemetryApi.commonValuesForHints(metadatas, []); this.table.populateColumns(allColumns); - this.timeColumns = telemetryApi.commonValuesForHints(metadatas, ['x']).map(function (metadatum) { + + var domainColumns = telemetryApi.commonValuesForHints(metadatas, ['x']); + this.timeColumns = domainColumns.map(function (metadatum) { return metadatum.name; }); - // For now, use first time field for time conductor - this.telemetry.sort(this.timeColumns[0] + '.value'); - this.filterColumns(); var timeSystem = this.openmct.conductor.timeSystem(); if (timeSystem) { this.sortByTimeSystem(timeSystem); } + if (!this.telemetry.sortColumn && domainColumns.length > 0) { + this.telemetry.sort(domainColumns[0].name + '.value'); + } + + } return objects; }; @@ -245,6 +249,7 @@ define( return new Promise(function (resolve, reject){ function finishProcessing(){ + telemetryCollection.addAll(rowData); scope.rows = telemetryCollection.telemetry; scope.loading = false; resolve(scope.rows); @@ -258,11 +263,9 @@ define( finishProcessing(); } } else { - historicalData.slice(index, index + this.batchSize) - .forEach(function (datum) { - telemetryCollection.add(this.table.getRowValues( - limitEvaluator, datum)); - }.bind(this)); + rowData = rowData.concat(historicalData.slice(index, index + this.batchSize) + .map(this.table.getRowValues.bind(this.table, limitEvaluator))); + this.timeoutHandle = this.$timeout(processData.bind( this, historicalData, @@ -300,6 +303,7 @@ define( }.bind(this)); }; + /** * @private * @param objects @@ -348,6 +352,9 @@ define( var scope = this.$scope; var newObject = this.newObject; + this.telemetry.clear(); + this.telemetry.bounds(this.openmct.conductor.bounds()); + this.$scope.loading = true; function error(e) { @@ -384,7 +391,7 @@ define( getDomainObjects() .then(filterForTelemetry) .then(this.loadColumns) - .then(this.subscribeToNewData) + //.then(this.subscribeToNewData) .then(this.getHistoricalData) .catch(error) }; diff --git a/platform/features/table/test/controllers/MCTTableControllerSpec.js b/platform/features/table/test/controllers/MCTTableControllerSpec.js index 61e4a2eece..970ca72047 100644 --- a/platform/features/table/test/controllers/MCTTableControllerSpec.js +++ b/platform/features/table/test/controllers/MCTTableControllerSpec.js @@ -465,20 +465,20 @@ define( mockScope.displayRows = controller.sortRows(testRows.slice(0)); mockScope.rows.push(row4); - controller.addRow(undefined, mockScope.rows.length - 1); + controller.addRows(undefined, mockScope.rows.length - 1); expect(mockScope.displayRows[0].col2.text).toEqual('xyz'); mockScope.rows.push(row5); - controller.addRow(undefined, mockScope.rows.length - 1); + controller.addRows(undefined, mockScope.rows.length - 1); expect(mockScope.displayRows[4].col2.text).toEqual('aaa'); mockScope.rows.push(row6); - controller.addRow(undefined, mockScope.rows.length - 1); + controller.addRows(undefined, mockScope.rows.length - 1); expect(mockScope.displayRows[2].col2.text).toEqual('ggg'); //Add a duplicate row mockScope.rows.push(row6); - controller.addRow(undefined, mockScope.rows.length - 1); + controller.addRows(undefined, mockScope.rows.length - 1); expect(mockScope.displayRows[2].col2.text).toEqual('ggg'); expect(mockScope.displayRows[3].col2.text).toEqual('ggg'); }); @@ -494,12 +494,12 @@ define( mockScope.displayRows = controller.filterRows(testRows); mockScope.rows.push(row5); - controller.addRow(undefined, mockScope.rows.length - 1); + controller.addRows(undefined, mockScope.rows.length - 1); expect(mockScope.displayRows.length).toBe(2); expect(mockScope.displayRows[1].col2.text).toEqual('aaa'); mockScope.rows.push(row6); - controller.addRow(undefined, mockScope.rows.length - 1); + controller.addRows(undefined, mockScope.rows.length - 1); expect(mockScope.displayRows.length).toBe(2); //Row was not added because does not match filter }); @@ -513,11 +513,11 @@ define( mockScope.displayRows = testRows.slice(0); mockScope.rows.push(row5); - controller.addRow(undefined, mockScope.rows.length - 1); + controller.addRows(undefined, mockScope.rows.length - 1); expect(mockScope.displayRows[3].col2.text).toEqual('aaa'); mockScope.rows.push(row6); - controller.addRow(undefined, mockScope.rows.length - 1); + controller.addRows(undefined, mockScope.rows.length - 1); expect(mockScope.displayRows[4].col2.text).toEqual('ggg'); }); @@ -536,7 +536,7 @@ define( mockScope.displayRows = testRows.slice(0); mockScope.rows.push(row7); - controller.addRow(undefined, mockScope.rows.length - 1); + controller.addRows(undefined, mockScope.rows.length - 1); expect(controller.$scope.sizingRow.col2).toEqual({text: 'some longer string'}); }); From ef8efbd53dd70c25581ade706e669f0601d8426d Mon Sep 17 00:00:00 2001 From: Henry Date: Wed, 25 Jan 2017 15:41:08 -0800 Subject: [PATCH 22/90] [Tables] Default UTC time system if available and none others defined --- .../conductor/utcTimeSystem/bundle.js | 19 ++++++++++++++++++- .../features/table/src/TelemetryCollection.js | 4 ++-- .../controllers/TelemetryTableController.js | 2 +- 3 files changed, 21 insertions(+), 4 deletions(-) diff --git a/platform/features/conductor/utcTimeSystem/bundle.js b/platform/features/conductor/utcTimeSystem/bundle.js index df9a6c0d38..806087e2ea 100644 --- a/platform/features/conductor/utcTimeSystem/bundle.js +++ b/platform/features/conductor/utcTimeSystem/bundle.js @@ -22,7 +22,8 @@ define([ "./src/UTCTimeSystem", - 'legacyRegistry' + 'legacyRegistry', + 'openmct' ], function ( UTCTimeSystem, legacyRegistry @@ -34,7 +35,23 @@ define([ "implementation": UTCTimeSystem, "depends": ["$timeout"] } + ], + "runs": [ + { + "implementation": function (openmct, $timeout) { + // Temporary shim to initialize the time conductor to + // something + if (!openmct.conductor.timeSystem()) { + var utcTimeSystem = new UTCTimeSystem($timeout); + + openmct.conductor.timeSystem(utcTimeSystem, utcTimeSystem.defaults().bounds); + } + }, + "depends": ["openmct", "$timeout"], + "priority": "fallback" + } ] } }); + }); diff --git a/platform/features/table/src/TelemetryCollection.js b/platform/features/table/src/TelemetryCollection.js index 185c1549af..4a5f277b66 100644 --- a/platform/features/table/src/TelemetryCollection.js +++ b/platform/features/table/src/TelemetryCollection.js @@ -84,7 +84,7 @@ define( }; TelemetryCollection.prototype.inBounds = function (element) { - var noBoundsDefined = !this.lastBounds || (!this.lastBounds.start && !this.lastBounds.end); + var noBoundsDefined = !this.lastBounds || (this.lastBounds.start === undefined && this.lastBounds.end === undefined); var withinBounds = _.get(element, this.sortField) >= this.lastBounds.start && _.get(element, this.sortField) <= this.lastBounds.end; @@ -114,7 +114,7 @@ define( array = this.highBuffer; } } else { - array = this.highBuffer; + array = this.telemetry; } // If out of bounds low, disregard data diff --git a/platform/features/table/src/controllers/TelemetryTableController.js b/platform/features/table/src/controllers/TelemetryTableController.js index d4414b1004..7e8fe1fe29 100644 --- a/platform/features/table/src/controllers/TelemetryTableController.js +++ b/platform/features/table/src/controllers/TelemetryTableController.js @@ -391,7 +391,7 @@ define( getDomainObjects() .then(filterForTelemetry) .then(this.loadColumns) - //.then(this.subscribeToNewData) + .then(this.subscribeToNewData) .then(this.getHistoricalData) .catch(error) }; From a3311e4c5783467f9c692987a4091857b671c080 Mon Sep 17 00:00:00 2001 From: Henry Date: Thu, 26 Jan 2017 10:59:22 -0800 Subject: [PATCH 23/90] [Tables] Tests and style fixes --- .../conductor/utcTimeSystem/bundle.js | 3 +- .../table/res/templates/telemetry-table.html | 1 - .../features/table/src/TableConfiguration.js | 47 +-- .../features/table/src/TelemetryCollection.js | 165 ++++++-- .../src/controllers/MCTTableController.js | 44 +- .../controllers/TelemetryTableController.js | 231 +++++++---- .../features/table/test/DomainColumnSpec.js | 80 ---- .../features/table/test/NameColumnSpec.js | 56 --- .../features/table/test/RangeColumnSpec.js | 74 ---- .../table/test/TableConfigurationSpec.js | 142 +++---- .../table/test/TelemetryCollectionSpec.js | 191 +++++++++ .../HistoricalTableControllerSpec.js | 380 ------------------ .../controllers/MCTTableControllerSpec.js | 100 ++--- .../RealtimeTableControllerSpec.js | 171 -------- .../TelemetryTableControllerSpec.js | 364 +++++++++++++++++ 15 files changed, 970 insertions(+), 1079 deletions(-) delete mode 100644 platform/features/table/test/DomainColumnSpec.js delete mode 100644 platform/features/table/test/NameColumnSpec.js delete mode 100644 platform/features/table/test/RangeColumnSpec.js create mode 100644 platform/features/table/test/TelemetryCollectionSpec.js delete mode 100644 platform/features/table/test/controllers/HistoricalTableControllerSpec.js delete mode 100644 platform/features/table/test/controllers/RealtimeTableControllerSpec.js create mode 100644 platform/features/table/test/controllers/TelemetryTableControllerSpec.js diff --git a/platform/features/conductor/utcTimeSystem/bundle.js b/platform/features/conductor/utcTimeSystem/bundle.js index 806087e2ea..5db4bd968f 100644 --- a/platform/features/conductor/utcTimeSystem/bundle.js +++ b/platform/features/conductor/utcTimeSystem/bundle.js @@ -22,8 +22,7 @@ define([ "./src/UTCTimeSystem", - 'legacyRegistry', - 'openmct' + "legacyRegistry" ], function ( UTCTimeSystem, legacyRegistry diff --git a/platform/features/table/res/templates/telemetry-table.html b/platform/features/table/res/templates/telemetry-table.html index 24c6a7702f..5bda288f1c 100644 --- a/platform/features/table/res/templates/telemetry-table.html +++ b/platform/features/table/res/templates/telemetry-table.html @@ -1,6 +1,5 @@
- Request time: {{tableController.lastRequestTime | date : 'HH:mm:ss:sss'}} 0){ + if (discarded && discarded.length > 0) { + /** + * A `discarded` event is thrown when telemetry data fall out of + * bounds due to a bounds change event + * @type {object[]} discarded the telemetry data + * discarded as a result of the bounds change + */ this.emit('discarded', discarded); } if (added && added.length > 0) { + /** + * An `added` event is thrown when a bounds change results in + * received telemetry falling within the new bounds. + * @type {object[]} added the telemetry data that is now within bounds + */ this.emit('added', added); } this.lastBounds = bounds; }; - TelemetryCollection.prototype.inBounds = function (element) { + /** + * Determines is a given telemetry datum is within the bounds currently + * defined for this telemetry collection. + * @private + * @param datum + * @returns {boolean} + */ + TelemetryCollection.prototype.inBounds = function (datum) { var noBoundsDefined = !this.lastBounds || (this.lastBounds.start === undefined && this.lastBounds.end === undefined); var withinBounds = - _.get(element, this.sortField) >= this.lastBounds.start && - _.get(element, this.sortField) <= this.lastBounds.end; + _.get(datum, this.sortField) >= this.lastBounds.start && + _.get(datum, this.sortField) <= this.lastBounds.end; return noBoundsDefined || withinBounds; }; /** + * Adds an individual item to the collection. Used internally only * @private - * @param element + * @param item */ - TelemetryCollection.prototype.addOne = function (element) { + TelemetryCollection.prototype.addOne = function (item) { var isDuplicate = false; - var boundsDefined = this.lastBounds && (this.lastBounds.start && this.lastBounds.end); + var boundsDefined = this.lastBounds && + (this.lastBounds.start !== undefined && this.lastBounds.end !== undefined); var array; + var boundsLow; + var boundsHigh; + + // If collection is not sorted by a time field, we cannot respond to + // bounds events, so no bounds checking necessary + if (this.sortField === undefined) { + this.telemetry.push(item); + return true; + } // Insert into either in-bounds array, or the out of bounds high buffer. // Data in the high buffer will be re-evaluated for possible insertion on next tick if (boundsDefined) { - var boundsHigh = _.get(element, this.sortField) > this.lastBounds.end; - var boundsLow = _.get(element, this.sortField) < this.lastBounds.start; + boundsHigh = _.get(item, this.sortField) > this.lastBounds.end; + boundsLow = _.get(item, this.sortField) < this.lastBounds.start; if (!boundsHigh && !boundsLow) { array = this.telemetry; @@ -119,26 +166,26 @@ define( // If out of bounds low, disregard data if (!boundsLow) { + // Going to check for duplicates. Bound the search problem to - // elements around the given time. Use sortedIndex because it + // items around the given time. Use sortedIndex because it // employs a binary search which is O(log n). Can use binary search // based on time stamp because the array is guaranteed ordered due // to sorted insertion. - - var startIx = _.sortedIndex(array, element, this.sortField); + var startIx = _.sortedIndex(array, item, this.sortField); if (startIx !== array.length) { - var endIx = _.sortedLastIndex(array, element, this.sortField); + var endIx = _.sortedLastIndex(array, item, this.sortField); // Create an array of potential dupes, based on having the // same time stamp var potentialDupes = array.slice(startIx, endIx + 1); // Search potential dupes for exact dupe - isDuplicate = _.findIndex(potentialDupes, _.isEqual.bind(undefined, element)) > -1; + isDuplicate = _.findIndex(potentialDupes, _.isEqual.bind(undefined, item)) > -1; } if (!isDuplicate) { - array.splice(startIx, 0, element); + array.splice(startIx, 0, item); //Return true if it was added and in bounds return array === this.telemetry; @@ -147,28 +194,60 @@ define( return false; }; - TelemetryCollection.prototype.addAll = function (elements) { - var added = elements.filter(this.addOne); + /** + * Add an array of objects to this telemetry collection + * @fires TelemetryCollection#added + * @param {object[]} items + */ + TelemetryCollection.prototype.add = function (items) { + var added = items.filter(this.addOne); this.emit('added', added); }; - //Todo: addAll for initial historical data - TelemetryCollection.prototype.add = function (element) { - if (this.addOne(element)){ - this.emit('added', [element]); - return true; - } else { - return false; - } - }; - + /** + * Clears the contents of the telemetry collection + */ TelemetryCollection.prototype.clear = function () { this.telemetry = []; }; - TelemetryCollection.prototype.sort = function (sortField){ + /** + * Sorts the telemetry collection based on the provided sort field + * specifier. + * @example + * // First build some mock telemetry for the purpose of an example + * let now = Date.now(); + * let telemetry = [1, 2, 3, 4, 5, 6, 7, 8, 9, 10].map(function (value) { + * return { + * // define an object property to demonstrate nested paths + * timestamp: { + * ms: now - value * 1000, + * text: + * }, + * value: value + * } + * }); + * let collection = new TelemetryCollection(); + * + * collection.add(telemetry); + * + * // Sort by telemetry value + * collection.sort("value"); + * + * // Sort by ms since epoch + * collection.sort("timestamp.ms"); + * + * // Sort by formatted date text + * collection.sort("timestamp.text"); + * + * + * @param {string} sortField An object property path. + */ + TelemetryCollection.prototype.sort = function (sortField) { this.sortField = sortField; - this.telemetry = _.sortBy(this.telemetry, this.iteratee); + if (sortField !== undefined) { + this.telemetry = _.sortBy(this.telemetry, this.iteratee); + } }; return TelemetryCollection; diff --git a/platform/features/table/src/controllers/MCTTableController.js b/platform/features/table/src/controllers/MCTTableController.js index af942f8b55..c987a0c3b8 100644 --- a/platform/features/table/src/controllers/MCTTableController.js +++ b/platform/features/table/src/controllers/MCTTableController.js @@ -1,7 +1,10 @@ define( - ['zepto'], - function ($) { + [ + 'zepto', + 'lodash' + ], + function ($, _) { /** * A controller for the MCTTable directive. Populates scope with @@ -134,7 +137,7 @@ define( */ $scope.$watch('defaultSort', function (newColumn, oldColumn) { if (newColumn !== oldColumn) { - $scope.toggleSort(newColumn) + $scope.toggleSort(newColumn); } }); @@ -163,7 +166,7 @@ define( } }.bind(this)); - $scope.$on('$destroy', function() { + $scope.$on('$destroy', function () { this.scrollable.off('scroll', this.onScroll); this.destroyConductorListeners(); @@ -172,7 +175,7 @@ define( delete this.$scope; }.bind(this)); - }; + } MCTTableController.prototype.destroyConductorListeners = function () { this.conductor.off('timeSystem', this.changeTimeSystem); @@ -227,7 +230,7 @@ define( */ MCTTableController.prototype.removeRows = function (event, rows) { var indexInDisplayRows; - rows.forEach(function (row){ + rows.forEach(function (row) { // Do a sequential search here. Only way of finding row is by // object equality, so array is in effect unsorted. indexInDisplayRows = this.$scope.displayRows.indexOf(row); @@ -252,7 +255,7 @@ define( * @private */ MCTTableController.prototype.onScroll = function (event) { - requestAnimationFrame(function () { + this.$window.requestAnimationFrame(function () { this.setVisibleRows(); this.digest(); @@ -344,18 +347,11 @@ define( this.$scope.visibleRows[this.$scope.visibleRows.length - 1] .rowIndex === end) { return this.digest(); - //return Promise.resolve(); // don't update if no changes are required. } } //Set visible rows from display rows, based on calculated offset. this.$scope.visibleRows = this.$scope.displayRows.slice(start, end) .map(function (row, i) { -/* var formattedRow = JSON.parse(JSON.stringify(row)); - if (self.$scope.formatCell) { - Object.keys(formattedRow).forEach(function (header) { - formattedRow[header].text = self.$scope.formatCell(header, row[header].text); - }); - } */ return { rowIndex: start + i, offsetY: ((start + i) * self.$scope.rowHeight) + @@ -585,19 +581,20 @@ define( MCTTableController.prototype.digest = function () { var scope = this.$scope; var self = this; - var requestAnimationFrame = this.$window.requestAnimationFrame; + var raf = this.$window.requestAnimationFrame; + var promise = this.digestPromise; - if (!this.digestPromise) { - this.digestPromise = new Promise(function (resolve) { - requestAnimationFrame(function() { + if (!promise) { + self.digestPromise = promise = new Promise(function (resolve) { + raf(function () { scope.$digest(); - delete self.digestPromise; + self.digestPromise = undefined; resolve(); }); }); } - return this.digestPromise; + return promise; }; /** @@ -640,8 +637,10 @@ define( } this.$scope.displayRows = this.filterAndSort(newRows || []); - this.resize(newRows) - .then(this.setVisibleRows) + return this.resize(newRows) + .then(function (rows) { + return this.setVisibleRows(rows); + }.bind(this)) //Timeout following setVisibleRows to allow digest to // perform DOM changes, otherwise scrollTo won't work. .then(function () { @@ -692,6 +691,7 @@ define( }; /** + * Scroll the view to a given row index * @param displayRowIndex {number} The index in the displayed rows * to scroll to. */ diff --git a/platform/features/table/src/controllers/TelemetryTableController.js b/platform/features/table/src/controllers/TelemetryTableController.js index 7e8fe1fe29..5beb49cb7e 100644 --- a/platform/features/table/src/controllers/TelemetryTableController.js +++ b/platform/features/table/src/controllers/TelemetryTableController.js @@ -19,6 +19,7 @@ * this source code distribution or the Licensing information page available * at runtime from the About dialog for additional information. *****************************************************************************/ +/* global console*/ /** * This bundle adds a table view for displaying telemetry data. @@ -28,10 +29,11 @@ define( [ '../TableConfiguration', '../../../../../src/api/objects/object-utils', - '../TelemetryCollection' + '../TelemetryCollection', + 'lodash' ], - function (TableConfiguration, objectUtils, TelemetryCollection) { + function (TableConfiguration, objectUtils, TelemetryCollection, _) { /** * The TableController is responsible for getting data onto the page @@ -46,6 +48,7 @@ define( $timeout, openmct ) { + this.$scope = $scope; this.$timeout = $timeout; this.openmct = openmct; @@ -55,14 +58,14 @@ define( * Initialization block */ this.columns = {}; //Range and Domain columns - this.deregisterListeners = []; + this.unobserveObject = undefined; this.subscriptions = []; this.timeColumns = []; $scope.rows = []; this.table = new TableConfiguration($scope.domainObject, openmct); this.lastBounds = this.openmct.conductor.bounds(); - this.requestTime = 0; + this.lastRequestTime = 0; this.telemetry = new TelemetryCollection(); /* @@ -81,38 +84,45 @@ define( 'changeBounds', 'setScroll', 'addRowsToTable', - 'removeRowsFromTable', + 'removeRowsFromTable' ]); - this.getData(); - this.registerChangeListeners(); + // Retrieve data when domain object is available. + // Also deferring telemetry request makes testing easier as controller + // construction has no unintended consequences. + $scope.$watch("domainObject", function () { + this.getData(); + this.registerChangeListeners(); + }.bind(this)); - this.openmct.conductor.on('follow', this.setScroll); this.setScroll(this.openmct.conductor.follow()); - this.telemetry.on('added', this.addRowsToTable); - this.telemetry.on('discarded', this.removeRowsFromTable); - this.$scope.$on("$destroy", this.destroy); } - TelemetryTableController.prototype.setScroll = function (scroll){ + /** + * @private + * @param {boolean} scroll + */ + TelemetryTableController.prototype.setScroll = function (scroll) { this.$scope.autoScroll = scroll; }; /** * Based on the selected time system, find a matching domain column * to sort by. By default will just match on key. - * @param timeSystem + * + * @private + * @param {TimeSystem} timeSystem */ TelemetryTableController.prototype.sortByTimeSystem = function (timeSystem) { var scope = this.$scope; - var sortColumn = undefined; + var sortColumn; scope.defaultSort = undefined; if (timeSystem) { this.table.columns.forEach(function (column) { - if (column.metadata.key === timeSystem.metadata.key) { + if (column.getKey() === timeSystem.metadata.key) { sortColumn = column; } }); @@ -124,44 +134,66 @@ define( }; /** - * Attach listeners to domain object to respond to changes due to - * composition, etc. + * Attaches listeners that respond to state change in domain object, + * conductor, and receipt of telemetry + * * @private */ TelemetryTableController.prototype.registerChangeListeners = function () { - this.deregisterListeners.forEach(function (deregister){ - deregister(); - }); - this.deregisterListeners = []; + if (this.unobserveObject) { + this.unobserveObject(); + } - this.deregisterListeners.push( - this.openmct.objects.observe(this.newObject, "*", - function (domainObject){ + this.unobserveObject = this.openmct.objects.observe(this.newObject, "*", + function (domainObject) { this.newObject = domainObject; this.getData(); }.bind(this) - ) - ); + ); + this.openmct.conductor.on('timeSystem', this.sortByTimeSystem); this.openmct.conductor.on('bounds', this.changeBounds); + this.openmct.conductor.on('follow', this.setScroll); + + this.telemetry.on('added', this.addRowsToTable); + this.telemetry.on('discarded', this.removeRowsFromTable); }; + /** + * On receipt of new telemetry, informs mct-table directive that new rows + * are available and passes populated rows to it + * + * @private + * @param rows + */ TelemetryTableController.prototype.addRowsToTable = function (rows) { this.$scope.$broadcast('add:rows', rows); }; + /** + * When rows are to be removed, informs mct-table directive. Row removal + * happens when rows call outside the bounds of the time conductor + * + * @private + * @param rows + */ TelemetryTableController.prototype.removeRowsFromTable = function (rows) { this.$scope.$broadcast('remove:rows', rows); }; + /** + * On Time Conductor bounds change, update displayed telemetry. In the + * case of a tick, previously visible telemetry that is now out of band + * will be removed from the table. + * @param {openmct.TimeConductorBounds~TimeConductorBounds} bounds + */ TelemetryTableController.prototype.changeBounds = function (bounds) { - //console.log('bounds.end: ' + bounds.end); var follow = this.openmct.conductor.follow(); var isTick = follow && bounds.start !== this.lastBounds.start && bounds.end !== this.lastBounds.end; - if (isTick){ + if (isTick) { this.telemetry.bounds(bounds); } else { // Is fixed bounds change @@ -171,7 +203,7 @@ define( }; /** - * Release the current subscription (called when scope is destroyed) + * Clean controller, deregistering listeners etc. */ TelemetryTableController.prototype.destroy = function () { @@ -182,11 +214,11 @@ define( this.subscriptions.forEach(function (subscription) { subscription(); }); - this.deregisterListeners.forEach(function (deregister){ - deregister(); - }); + + if (this.unobserveObject) { + this.unobserveObject(); + } this.subscriptions = []; - this.deregisterListeners = []; if (this.timeoutHandle) { this.$timeout.cancel(this.timeoutHandle); @@ -200,9 +232,10 @@ define( }; /** + * For given objects, populate column metadata and table headers. * @private - * @param objects - * @returns {*} + * @param {module:openmct.DomainObject[]} objects the domain objects for + * which columns should be populated */ TelemetryTableController.prototype.loadColumns = function (objects) { var telemetryApi = this.openmct.telemetry; @@ -220,25 +253,28 @@ define( this.filterColumns(); + // Default to no sort on underlying telemetry collection. Sorting + // is necessary to do bounds filtering, but this is only possible + // if data matches selected time system + this.telemetry.sort(undefined); + var timeSystem = this.openmct.conductor.timeSystem(); if (timeSystem) { this.sortByTimeSystem(timeSystem); } - if (!this.telemetry.sortColumn && domainColumns.length > 0) { - this.telemetry.sort(domainColumns[0].name + '.value'); - } - } return objects; }; /** + * Request telemetry data from an historical store for given objects. * @private - * @param objects The domain objects to request telemetry for - * @returns {*|{configFile}|app|boolean|Route|Object} + * @param {object[]} The domain objects to request telemetry for + * @returns {Promise} resolved when historical data is available */ TelemetryTableController.prototype.getHistoricalData = function (objects) { + var self = this; var openmct = this.openmct; var bounds = openmct.conductor.bounds(); var scope = this.$scope; @@ -247,15 +283,22 @@ define( var requestTime = this.lastRequestTime = Date.now(); var telemetryCollection = this.telemetry; - return new Promise(function (resolve, reject){ - function finishProcessing(){ - telemetryCollection.addAll(rowData); + var promise = new Promise(function (resolve, reject) { + /* + * On completion of batched processing, set the rows on scope + */ + function finishProcessing() { + telemetryCollection.add(rowData); scope.rows = telemetryCollection.telemetry; scope.loading = false; + resolve(scope.rows); } - function processData(historicalData, index, limitEvaluator){ + /* + * Process a batch of historical data + */ + function processData(historicalData, index, limitEvaluator) { if (index >= historicalData.length) { processedObjects++; @@ -263,51 +306,57 @@ define( finishProcessing(); } } else { - rowData = rowData.concat(historicalData.slice(index, index + this.batchSize) - .map(this.table.getRowValues.bind(this.table, limitEvaluator))); + rowData = rowData.concat(historicalData.slice(index, index + self.batchSize) + .map(self.table.getRowValues.bind(self.table, limitEvaluator))); - this.timeoutHandle = this.$timeout(processData.bind( - this, - historicalData, - index + this.batchSize, - limitEvaluator - )); + /* + Use timeout to yield process to other UI activities. On + return, process next batch + */ + self.timeoutHandle = self.$timeout(function () { + processData(historicalData, index + self.batchSize, limitEvaluator); + }); } } function makeTableRows(object, historicalData) { - // Only process one request at a time - if (requestTime === this.lastRequestTime) { + // Only process the most recent request + if (requestTime === self.lastRequestTime) { var limitEvaluator = openmct.telemetry.limitEvaluator(object); - processData.call(this, historicalData, 0, limitEvaluator); + processData(historicalData, 0, limitEvaluator); } else { resolve(rowData); } } - function requestData (object) { + /* + Use the telemetry API to request telemetry for a given object + */ + function requestData(object) { return openmct.telemetry.request(object, { start: bounds.start, end: bounds.end - }).then(makeTableRows.bind(this, object)) + }).then(makeTableRows.bind(undefined, object)) .catch(reject); } this.$timeout.cancel(this.timeoutHandle); - if (objects.length > 0){ - objects.forEach(requestData.bind(this)); + if (objects.length > 0) { + objects.forEach(requestData); } else { scope.loading = false; resolve([]); } }.bind(this)); + + return promise; }; /** + * Subscribe to real-time data for the given objects. * @private - * @param objects - * @returns {*} + * @param {object[]} objects The objects to subscribe to. */ TelemetryTableController.prototype.subscribeToNewData = function (objects) { var telemetryApi = this.openmct.telemetry; @@ -317,6 +366,8 @@ define( var maxRows = Number.MAX_VALUE; var limitEvaluator; var added = false; + var scope = this.$scope; + var table = this.table; this.subscriptions.forEach(function (subscription) { subscription(); @@ -325,20 +376,20 @@ define( function newData(domainObject, datum) { limitEvaluator = telemetryApi.limitEvaluator(domainObject); - added = telemetryCollection.add(this.table.getRowValues(limitEvaluator, datum)); + added = telemetryCollection.add([table.getRowValues(limitEvaluator, datum)]); //Inform table that a new row has been added - if (this.$scope.rows.length > maxRows) { - this.$scope.$broadcast('remove:rows', this.$scope.rows[0]); - this.$scope.rows.shift(); + if (scope.rows.length > maxRows) { + scope.$broadcast('remove:rows', scope.rows[0]); + scope.rows.shift(); } - if (!this.$scope.loading && added) { - this.$scope.$broadcast('add:row', - this.$scope.rows.length - 1); + if (!scope.loading && added) { + scope.$broadcast('add:row', + scope.rows.length - 1); } } - objects.forEach(function (object){ + objects.forEach(function (object) { this.subscriptions.push( telemetryApi.subscribe(object, newData.bind(this, object), {})); }.bind(this)); @@ -346,6 +397,12 @@ define( return objects; }; + /** + * Request historical data, and subscribe to for real-time data. + * @private + * @returns {Promise} A promise that is resolved once subscription is + * established, and historical telemetry is received and processed. + */ TelemetryTableController.prototype.getData = function () { var telemetryApi = this.openmct.telemetry; var compositionApi = this.openmct.composition; @@ -359,41 +416,37 @@ define( function error(e) { scope.loading = false; - console.error(e); + console.error(e.stack); } - function filterForTelemetry(objects){ + function filterForTelemetry(objects) { return objects.filter(telemetryApi.canProvideTelemetry.bind(telemetryApi)); } function getDomainObjects() { - return new Promise(function (resolve, reject){ - var objects = [newObject]; - var composition = compositionApi.get(newObject); + var objects = [newObject]; + var composition = compositionApi.get(newObject); - if (composition) { - composition - .load() - .then(function (children) { - return objects.concat(children); - }) - .then(resolve) - .catch(reject); - } else { - return resolve(objects); - } - }); + if (composition) { + return composition + .load() + .then(function (children) { + return objects.concat(children); + }); + } else { + return Promise.resolve(objects); + } } scope.headers = []; scope.rows = []; - getDomainObjects() + return getDomainObjects() .then(filterForTelemetry) .then(this.loadColumns) .then(this.subscribeToNewData) .then(this.getHistoricalData) - .catch(error) + .catch(error); }; /** diff --git a/platform/features/table/test/DomainColumnSpec.js b/platform/features/table/test/DomainColumnSpec.js deleted file mode 100644 index 3c144b8427..0000000000 --- a/platform/features/table/test/DomainColumnSpec.js +++ /dev/null @@ -1,80 +0,0 @@ -/***************************************************************************** - * Open MCT, Copyright (c) 2014-2016, United States Government - * as represented by the Administrator of the National Aeronautics and Space - * Administration. All rights reserved. - * - * Open MCT is licensed under the Apache License, Version 2.0 (the - * "License"); you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * http://www.apache.org/licenses/LICENSE-2.0. - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT - * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the - * License for the specific language governing permissions and limitations - * under the License. - * - * Open MCT includes source code licensed under additional open source - * licenses. See the Open Source Licenses file (LICENSES.md) included with - * this source code distribution or the Licensing information page available - * at runtime from the About dialog for additional information. - *****************************************************************************/ - -/** - * MergeModelsSpec. Created by vwoeltje on 11/6/14. - */ -define( - ["../src/DomainColumn"], - function (DomainColumn) { - - var TEST_DOMAIN_VALUE = "some formatted domain value"; - - describe("A domain column", function () { - var mockDatum, - testMetadata, - mockFormatter, - column; - - beforeEach(function () { - - mockFormatter = jasmine.createSpyObj( - "formatter", - ["formatDomainValue", "formatRangeValue"] - ); - testMetadata = { - key: "testKey", - name: "Test Name", - format: "Test Format" - }; - mockFormatter.formatDomainValue.andReturn(TEST_DOMAIN_VALUE); - - column = new DomainColumn(testMetadata, mockFormatter); - }); - - it("reports a column header from domain metadata", function () { - expect(column.getTitle()).toEqual("Test Name"); - }); - - describe("when given a datum", function () { - beforeEach(function () { - mockDatum = { - testKey: "testKeyValue" - }; - }); - - it("looks up data from the given datum", function () { - expect(column.getValue(undefined, mockDatum)) - .toEqual({ text: TEST_DOMAIN_VALUE }); - }); - - it("uses formatter to format domain values as requested", function () { - column.getValue(undefined, mockDatum); - expect(mockFormatter.formatDomainValue) - .toHaveBeenCalledWith("testKeyValue", "Test Format"); - }); - - }); - - }); - } -); diff --git a/platform/features/table/test/NameColumnSpec.js b/platform/features/table/test/NameColumnSpec.js deleted file mode 100644 index 13e858c2ed..0000000000 --- a/platform/features/table/test/NameColumnSpec.js +++ /dev/null @@ -1,56 +0,0 @@ -/***************************************************************************** - * Open MCT, Copyright (c) 2014-2016, United States Government - * as represented by the Administrator of the National Aeronautics and Space - * Administration. All rights reserved. - * - * Open MCT is licensed under the Apache License, Version 2.0 (the - * "License"); you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * http://www.apache.org/licenses/LICENSE-2.0. - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT - * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the - * License for the specific language governing permissions and limitations - * under the License. - * - * Open MCT includes source code licensed under additional open source - * licenses. See the Open Source Licenses file (LICENSES.md) included with - * this source code distribution or the Licensing information page available - * at runtime from the About dialog for additional information. - *****************************************************************************/ - -/** - * MergeModelsSpec. Created by vwoeltje on 11/6/14. - */ -define( - ["../src/NameColumn"], - function (NameColumn) { - - describe("A name column", function () { - var mockDomainObject, - column; - - beforeEach(function () { - mockDomainObject = jasmine.createSpyObj( - "domainObject", - ["getModel"] - ); - mockDomainObject.getModel.andReturn({ - name: "Test object name" - }); - column = new NameColumn(); - }); - - it("reports a column header", function () { - expect(column.getTitle()).toEqual("Name"); - }); - - it("looks up name from an object's model", function () { - expect(column.getValue(mockDomainObject).text) - .toEqual("Test object name"); - }); - - }); - } -); diff --git a/platform/features/table/test/RangeColumnSpec.js b/platform/features/table/test/RangeColumnSpec.js deleted file mode 100644 index 473f26ae56..0000000000 --- a/platform/features/table/test/RangeColumnSpec.js +++ /dev/null @@ -1,74 +0,0 @@ -/***************************************************************************** - * Open MCT, Copyright (c) 2014-2016, United States Government - * as represented by the Administrator of the National Aeronautics and Space - * Administration. All rights reserved. - * - * Open MCT is licensed under the Apache License, Version 2.0 (the - * "License"); you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * http://www.apache.org/licenses/LICENSE-2.0. - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT - * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the - * License for the specific language governing permissions and limitations - * under the License. - * - * Open MCT includes source code licensed under additional open source - * licenses. See the Open Source Licenses file (LICENSES.md) included with - * this source code distribution or the Licensing information page available - * at runtime from the About dialog for additional information. - *****************************************************************************/ - -/** - * MergeModelsSpec. Created by vwoeltje on 11/6/14. - */ -define( - ["../src/RangeColumn"], - function (RangeColumn) { - - var TEST_RANGE_VALUE = "some formatted range value"; - - describe("A range column", function () { - var testDatum, - testMetadata, - mockFormatter, - mockDomainObject, - column; - - beforeEach(function () { - testDatum = { testKey: 123, otherKey: 456 }; - mockFormatter = jasmine.createSpyObj( - "formatter", - ["formatDomainValue", "formatRangeValue"] - ); - testMetadata = { - key: "testKey", - name: "Test Name" - }; - mockDomainObject = jasmine.createSpyObj( - "domainObject", - ["getModel", "getCapability"] - ); - mockFormatter.formatRangeValue.andReturn(TEST_RANGE_VALUE); - - column = new RangeColumn(testMetadata, mockFormatter); - }); - - it("reports a column header from range metadata", function () { - expect(column.getTitle()).toEqual("Test Name"); - }); - - it("formats range values as numbers", function () { - expect(column.getValue(mockDomainObject, testDatum).text) - .toEqual(TEST_RANGE_VALUE); - - // Make sure that service interactions were as expected - expect(mockFormatter.formatRangeValue) - .toHaveBeenCalledWith(testDatum.testKey); - expect(mockFormatter.formatDomainValue) - .not.toHaveBeenCalled(); - }); - }); - } -); diff --git a/platform/features/table/test/TableConfigurationSpec.js b/platform/features/table/test/TableConfigurationSpec.js index db90cc1c62..af4f2ee2a8 100644 --- a/platform/features/table/test/TableConfigurationSpec.js +++ b/platform/features/table/test/TableConfigurationSpec.js @@ -22,13 +22,14 @@ define( [ - "../src/TableConfiguration", - "../src/DomainColumn" + "../src/TableConfiguration" ], - function (Table, DomainColumn) { + function (Table) { describe("A table", function () { var mockDomainObject, + mockAPI, + mockTelemetryAPI, mockTelemetryFormatter, table, mockModel; @@ -41,90 +42,63 @@ define( mockDomainObject.getModel.andReturn(mockModel); mockTelemetryFormatter = jasmine.createSpyObj('telemetryFormatter', [ - 'formatDomainValue', - 'formatRangeValue' + 'format' ]); - mockTelemetryFormatter.formatDomainValue.andCallFake(function (valueIn) { - return valueIn; - }); - mockTelemetryFormatter.formatRangeValue.andCallFake(function (valueIn) { + mockTelemetryFormatter.format.andCallFake(function (valueIn) { return valueIn; }); - table = new Table(mockDomainObject, mockTelemetryFormatter); - }); + mockTelemetryAPI = jasmine.createSpyObj('telemetryAPI', [ + 'getValueFormatter' + ]); + mockAPI = { + telemetry: mockTelemetryAPI + }; + mockTelemetryAPI.getValueFormatter.andReturn(mockTelemetryFormatter); - it("Add column with no index adds new column to the end", function () { - var firstColumn = {title: 'First Column'}, - secondColumn = {title: 'Second Column'}, - thirdColumn = {title: 'Third Column'}; - - table.addColumn(firstColumn); - table.addColumn(secondColumn); - table.addColumn(thirdColumn); - - expect(table.columns).toBeDefined(); - expect(table.columns.length).toBe(3); - expect(table.columns[0]).toBe(firstColumn); - expect(table.columns[1]).toBe(secondColumn); - expect(table.columns[2]).toBe(thirdColumn); - }); - - it("Add column with index adds new column at the specified" + - " position", function () { - var firstColumn = {title: 'First Column'}, - secondColumn = {title: 'Second Column'}, - thirdColumn = {title: 'Third Column'}; - - table.addColumn(firstColumn); - table.addColumn(thirdColumn); - table.addColumn(secondColumn, 1); - - expect(table.columns).toBeDefined(); - expect(table.columns.length).toBe(3); - expect(table.columns[0]).toBe(firstColumn); - expect(table.columns[1]).toBe(secondColumn); - expect(table.columns[2]).toBe(thirdColumn); + table = new Table(mockDomainObject, mockAPI); }); describe("Building columns from telemetry metadata", function () { - var metadata = [{ - ranges: [ - { - name: 'Range 1', - key: 'range1' - }, - { - name: 'Range 2', - key: 'range2' + var metadata = [ + { + name: 'Range 1', + key: 'range1', + hints: { + y: 1 } - ], - domains: [ - { - name: 'Domain 1', - key: 'domain1', - format: 'utc' - }, - { - name: 'Domain 2', - key: 'domain2', - format: 'utc' + }, + { + name: 'Range 2', + key: 'range2', + hints: { + y: 2 } - ] - }]; + }, + { + name: 'Domain 1', + key: 'domain1', + format: 'utc', + hints: { + x: 1 + } + }, + { + name: 'Domain 2', + key: 'domain2', + format: 'utc', + hints: { + x: 2 + } + } + ]; beforeEach(function () { table.populateColumns(metadata); }); it("populates columns", function () { - expect(table.columns.length).toBe(5); - }); - - it("Build columns populates columns with domains to the left", function () { - expect(table.columns[1] instanceof DomainColumn).toBeTruthy(); - expect(table.columns[2] instanceof DomainColumn).toBeTruthy(); - expect(table.columns[3] instanceof DomainColumn).toBeFalsy(); + expect(table.columns.length).toBe(4); }); it("Produces headers for each column based on title", function () { @@ -133,7 +107,7 @@ define( spyOn(firstColumn, 'getTitle'); headers = table.getHeaders(); - expect(headers.length).toBe(5); + expect(headers.length).toBe(4); expect(firstColumn.getTitle).toHaveBeenCalled(); }); @@ -170,23 +144,33 @@ define( beforeEach(function () { datum = { - 'range1': 'range 1 value', - 'range2': 'range 2 value', + 'range1': 10, + 'range2': 20, 'domain1': 0, 'domain2': 1 }; - rowValues = table.getRowValues(mockDomainObject, datum); + var limitEvaluator = { + evaluate: function () { + return { + "cssClass": "alarm-class" + }; + } + }; + rowValues = table.getRowValues(limitEvaluator, datum); }); it("Returns a value for every column", function () { expect(rowValues['Range 1'].text).toBeDefined(); - expect(rowValues['Range 1'].text).toEqual('range 1' + - ' value'); + expect(rowValues['Range 1'].text).toEqual(10); }); - it("Uses the telemetry formatter to appropriately format" + + it("Applies appropriate css class if limit violated.", function () { + expect(rowValues['Range 1'].cssClass).toEqual("alarm-class"); + }); + + it("Uses telemetry formatter to appropriately format" + " telemetry values", function () { - expect(mockTelemetryFormatter.formatRangeValue).toHaveBeenCalled(); + expect(mockTelemetryFormatter.format).toHaveBeenCalled(); }); }); }); diff --git a/platform/features/table/test/TelemetryCollectionSpec.js b/platform/features/table/test/TelemetryCollectionSpec.js new file mode 100644 index 0000000000..014e5c684e --- /dev/null +++ b/platform/features/table/test/TelemetryCollectionSpec.js @@ -0,0 +1,191 @@ +/***************************************************************************** + * Open MCT, Copyright (c) 2014-2016, United States Government + * as represented by the Administrator of the National Aeronautics and Space + * Administration. All rights reserved. + * + * Open MCT is licensed under the Apache License, Version 2.0 (the + * "License"); you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * http://www.apache.org/licenses/LICENSE-2.0. + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT + * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the + * License for the specific language governing permissions and limitations + * under the License. + * + * Open MCT includes source code licensed under additional open source + * licenses. See the Open Source Licenses file (LICENSES.md) included with + * this source code distribution or the Licensing information page available + * at runtime from the About dialog for additional information. + *****************************************************************************/ + +define( + [ + "../src/TelemetryCollection" + ], + function (TelemetryCollection) { + + describe("A telemetry collection", function () { + + var collection; + var telemetryObjects; + var ms; + var integerTextMap = ["ZERO", "ONE", "TWO", "THREE", "FOUR", "FIVE", + "SIX", "SEVEN", "EIGHT", "NINE", "TEN", "ELEVEN"]; + + beforeEach(function () { + telemetryObjects = [0,9,2,4,7,8,5,1,3,6].map(function (number) { + ms = number * 1000; + return { + timestamp: ms, + value: { + integer: number, + text: integerTextMap[number] + } + }; + }); + collection = new TelemetryCollection(); + }); + + it("Sorts inserted telemetry by specified field", + function () { + collection.sort('value.integer'); + collection.add(telemetryObjects); + expect(collection.telemetry[0].value.integer).toBe(0); + expect(collection.telemetry[1].value.integer).toBe(1); + expect(collection.telemetry[2].value.integer).toBe(2); + expect(collection.telemetry[3].value.integer).toBe(3); + + collection.sort('value.text'); + expect(collection.telemetry[0].value.text).toBe("EIGHT"); + expect(collection.telemetry[1].value.text).toBe("FIVE"); + expect(collection.telemetry[2].value.text).toBe("FOUR"); + expect(collection.telemetry[3].value.text).toBe("NINE"); + } + ); + + describe("on bounds change", function () { + var discardedCallback; + + beforeEach(function () { + discardedCallback = jasmine.createSpy("discarded"); + collection.on("discarded", discardedCallback); + collection.sort("timestamp"); + collection.add(telemetryObjects); + collection.bounds({start: 5000, end: 8000}); + }); + + + it("emits an event indicating that telemetry has " + + "been discarded", function () { + expect(discardedCallback).toHaveBeenCalled(); + }); + + it("discards telemetry data with a time stamp " + + "before specified start bound", function () { + var discarded = discardedCallback.mostRecentCall.args[0]; + + // Expect 5 because as an optimization, the TelemetryCollection + // will not consider telemetry values that exceed the upper + // bounds. Arbitrary bounds changes in which the end bound is + // decreased is assumed to require a new historical query, and + // hence re-population of the collection anyway + expect(discarded.length).toBe(5); + expect(discarded[0].value.integer).toBe(0); + expect(discarded[1].value.integer).toBe(1); + expect(discarded[4].value.integer).toBe(4); + }); + }); + + describe("when adding telemetry to a collection", function () { + var addedCallback; + beforeEach(function () { + collection.sort("timestamp"); + collection.add(telemetryObjects); + addedCallback = jasmine.createSpy("added"); + collection.on("added", addedCallback); + }); + + it("emits an event", + function () { + var addedObject = { + timestamp: 10000, + value: { + integer: 10, + text: integerTextMap[10] + } + }; + collection.add([addedObject]); + expect(addedCallback).toHaveBeenCalledWith([addedObject]); + } + ); + it("inserts in the correct order", + function () { + var addedObjectA = { + timestamp: 10000, + value: { + integer: 10, + text: integerTextMap[10] + } + }; + var addedObjectB = { + timestamp: 11000, + value: { + integer: 11, + text: integerTextMap[11] + } + }; + collection.add([addedObjectB, addedObjectA]); + + expect(collection.telemetry[11]).toBe(addedObjectB); + } + ); + }); + + describe("buffers telemetry", function () { + var addedObjectA; + var addedObjectB; + + beforeEach(function () { + collection.sort("timestamp"); + collection.add(telemetryObjects); + + addedObjectA = { + timestamp: 10000, + value: { + integer: 10, + text: integerTextMap[10] + } + }; + addedObjectB = { + timestamp: 11000, + value: { + integer: 11, + text: integerTextMap[11] + } + }; + + collection.bounds({start: 0, end: 10000}); + collection.add([addedObjectA, addedObjectB]); + }); + it("when it falls outside of bounds", function () { + expect(collection.highBuffer).toBeDefined(); + expect(collection.highBuffer.length).toBe(1); + expect(collection.highBuffer[0]).toBe(addedObjectB); + }); + it("and adds it to collection when it falls within bounds", function () { + expect(collection.telemetry.length).toBe(11); + collection.bounds({start: 0, end: 11000}); + expect(collection.telemetry.length).toBe(12); + expect(collection.telemetry[11]).toBe(addedObjectB); + }); + it("and removes it from the buffer when it falls within bounds", function () { + expect(collection.highBuffer.length).toBe(1); + collection.bounds({start: 0, end: 11000}); + expect(collection.highBuffer.length).toBe(0); + }); + }); + }); + } +); diff --git a/platform/features/table/test/controllers/HistoricalTableControllerSpec.js b/platform/features/table/test/controllers/HistoricalTableControllerSpec.js deleted file mode 100644 index 39f7d1a8f5..0000000000 --- a/platform/features/table/test/controllers/HistoricalTableControllerSpec.js +++ /dev/null @@ -1,380 +0,0 @@ -/***************************************************************************** - * Open MCT, Copyright (c) 2014-2016, United States Government - * as represented by the Administrator of the National Aeronautics and Space - * Administration. All rights reserved. - * - * Open MCT is licensed under the Apache License, Version 2.0 (the - * "License"); you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * http://www.apache.org/licenses/LICENSE-2.0. - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT - * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the - * License for the specific language governing permissions and limitations - * under the License. - * - * Open MCT includes source code licensed under additional open source - * licenses. See the Open Source Licenses file (LICENSES.md) included with - * this source code distribution or the Licensing information page available - * at runtime from the About dialog for additional information. - *****************************************************************************/ - -define( - [ - "../../src/controllers/HistoricalTableController" - ], - function (TableController) { - - describe('The Table Controller', function () { - var mockScope, - mockTelemetryHandler, - mockTelemetryHandle, - mockTelemetryFormatter, - mockDomainObject, - mockTable, - mockConfiguration, - mockAngularTimeout, - mockTimeoutHandle, - watches, - mockConductor, - controller; - - function promise(value) { - return { - then: function (callback) { - return promise(callback(value)); - } - }; - } - - function getCallback(target, event) { - return target.calls.filter(function (call) { - return call.args[0] === event; - })[0].args[1]; - } - - beforeEach(function () { - watches = {}; - mockScope = jasmine.createSpyObj('scope', [ - '$on', - '$watch', - '$watchCollection' - ]); - - mockScope.$on.andCallFake(function (expression, callback) { - watches[expression] = callback; - }); - mockScope.$watch.andCallFake(function (expression, callback) { - watches[expression] = callback; - }); - mockScope.$watchCollection.andCallFake(function (expression, callback) { - watches[expression] = callback; - }); - - mockTimeoutHandle = jasmine.createSpy("timeoutHandle"); - mockAngularTimeout = jasmine.createSpy("$timeout"); - mockAngularTimeout.andReturn(mockTimeoutHandle); - mockAngularTimeout.cancel = jasmine.createSpy("cancelTimeout"); - - mockConfiguration = { - 'range1': true, - 'range2': true, - 'domain1': true - }; - - mockTable = jasmine.createSpyObj('table', - [ - 'populateColumns', - 'buildColumnConfiguration', - 'getRowValues', - 'saveColumnConfiguration' - ] - ); - mockTable.columns = []; - mockTable.buildColumnConfiguration.andReturn(mockConfiguration); - - mockDomainObject = jasmine.createSpyObj('domainObject', [ - 'getCapability', - 'useCapability', - 'getModel' - ]); - mockDomainObject.getModel.andReturn({}); - - mockScope.domainObject = mockDomainObject; - - mockTelemetryHandle = jasmine.createSpyObj('telemetryHandle', [ - 'request', - 'promiseTelemetryObjects', - 'getTelemetryObjects', - 'getMetadata', - 'getSeries', - 'unsubscribe', - 'makeDatum' - ]); - mockTelemetryHandle.promiseTelemetryObjects.andReturn(promise(undefined)); - mockTelemetryHandle.request.andReturn(promise(undefined)); - mockTelemetryHandle.getTelemetryObjects.andReturn([]); - mockTelemetryHandle.getMetadata.andReturn([]); - - mockTelemetryHandler = jasmine.createSpyObj('telemetryHandler', [ - 'handle' - ]); - mockTelemetryHandler.handle.andReturn(mockTelemetryHandle); - - mockConductor = jasmine.createSpyObj("conductor", [ - "timeSystem", - "on", - "off" - ]); - - controller = new TableController(mockScope, mockTelemetryHandler, - mockTelemetryFormatter, mockAngularTimeout, {conductor: mockConductor}); - - controller.table = mockTable; - controller.handle = mockTelemetryHandle; - }); - - it('subscribes to telemetry handler for telemetry updates', function () { - controller.subscribe(); - expect(mockTelemetryHandler.handle).toHaveBeenCalled(); - expect(mockTelemetryHandle.request).toHaveBeenCalled(); - }); - - it('Unsubscribes from telemetry when scope is destroyed', function () { - controller.handle = mockTelemetryHandle; - watches.$destroy(); - expect(mockTelemetryHandle.unsubscribe).toHaveBeenCalled(); - }); - - describe('makes use of the table', function () { - - it('to create column definitions from telemetry' + - ' metadata', function () { - controller.setup(); - expect(mockTable.populateColumns).toHaveBeenCalled(); - }); - - it('to create column configuration, which is written to the' + - ' object model', function () { - controller.setup(); - expect(mockTable.buildColumnConfiguration).toHaveBeenCalled(); - }); - }); - - it('updates the rows on scope when historical telemetry is received', function () { - var mockSeries = { - getPointCount: function () { - return 5; - }, - getDomainValue: function () { - return 'Domain Value'; - }, - getRangeValue: function () { - return 'Range Value'; - } - }, - mockRow = {'domain': 'Domain Value', 'range': 'Range' + - ' Value'}; - - mockTelemetryHandle.makeDatum.andCallFake(function () { - return mockRow; - }); - mockTable.getRowValues.andReturn(mockRow); - mockTelemetryHandle.getTelemetryObjects.andReturn([mockDomainObject]); - mockTelemetryHandle.getSeries.andReturn(mockSeries); - - controller.addHistoricalData(mockDomainObject, mockSeries); - - // Angular timeout is called a minumum of twice, regardless - // of batch size used. - expect(mockAngularTimeout).toHaveBeenCalled(); - mockAngularTimeout.mostRecentCall.args[0](); - expect(mockAngularTimeout.calls.length).toEqual(2); - mockAngularTimeout.mostRecentCall.args[0](); - - expect(controller.$scope.rows.length).toBe(5); - expect(controller.$scope.rows[0]).toBe(mockRow); - }); - - it('filters the visible columns based on configuration', function () { - controller.filterColumns(); - expect(controller.$scope.headers.length).toBe(3); - expect(controller.$scope.headers[2]).toEqual('domain1'); - - mockConfiguration.domain1 = false; - controller.filterColumns(); - expect(controller.$scope.headers.length).toBe(2); - expect(controller.$scope.headers[2]).toBeUndefined(); - }); - - describe('creates event listeners', function () { - beforeEach(function () { - spyOn(controller, 'subscribe'); - spyOn(controller, 'filterColumns'); - }); - - it('triggers telemetry subscription update when domain' + - ' object changes', function () { - controller.registerChangeListeners(); - //'watches' object is populated by fake scope watch and - // watchCollection functions defined above - expect(watches.domainObject).toBeDefined(); - watches.domainObject(mockDomainObject); - expect(controller.subscribe).toHaveBeenCalled(); - }); - - it('triggers telemetry subscription update when domain' + - ' object composition changes', function () { - controller.registerChangeListeners(); - expect(watches['domainObject.getModel().composition']).toBeDefined(); - watches['domainObject.getModel().composition']([], []); - expect(controller.subscribe).toHaveBeenCalled(); - }); - - it('triggers telemetry subscription update when time' + - ' conductor bounds change', function () { - controller.registerChangeListeners(); - expect(watches['telemetry:display:bounds']).toBeDefined(); - watches['telemetry:display:bounds'](); - expect(controller.subscribe).toHaveBeenCalled(); - }); - - it('triggers refiltering of the columns when configuration' + - ' changes', function () { - controller.setup(); - expect(watches['domainObject.getModel().configuration.table.columns']).toBeDefined(); - watches['domainObject.getModel().configuration.table.columns'](); - expect(controller.filterColumns).toHaveBeenCalled(); - }); - - }); - describe('After populating columns', function () { - var metadata; - beforeEach(function () { - metadata = [{domains: [{name: 'time domain 1'}, {name: 'time domain 2'}]}, {domains: [{name: 'time domain 3'}, {name: 'time domain 4'}]}]; - controller.populateColumns(metadata); - }); - - it('Automatically identifies time columns', function () { - expect(controller.timeColumns.length).toBe(4); - expect(controller.timeColumns[0]).toBe('time domain 1'); - }); - - it('Automatically sorts by time column that matches current' + - ' time system', function () { - var key = 'time_domain_1', - name = 'time domain 1', - mockTimeSystem = { - metadata: { - key: key - } - }; - - mockTable.columns = [ - { - domainMetadata: { - key: key - }, - getTitle: function () { - return name; - } - }, - { - domainMetadata: { - key: 'anotherColumn' - }, - getTitle: function () { - return 'some other column'; - } - }, - { - domainMetadata: { - key: 'thirdColumn' - }, - getTitle: function () { - return 'a third column'; - } - } - ]; - - expect(mockConductor.on).toHaveBeenCalledWith('timeSystem', jasmine.any(Function)); - getCallback(mockConductor.on, 'timeSystem')(mockTimeSystem); - expect(controller.$scope.defaultSort).toBe(name); - }); - }); - describe('Yields thread', function () { - var mockSeries, - mockRow; - - beforeEach(function () { - mockSeries = { - getPointCount: function () { - return 5; - }, - getDomainValue: function () { - return 'Domain Value'; - }, - getRangeValue: function () { - return 'Range Value'; - } - }; - mockRow = {'domain': 'Domain Value', 'range': 'Range Value'}; - - mockTelemetryHandle.makeDatum.andCallFake(function () { - return mockRow; - }); - mockTable.getRowValues.andReturn(mockRow); - mockTelemetryHandle.getTelemetryObjects.andReturn([mockDomainObject]); - mockTelemetryHandle.getSeries.andReturn(mockSeries); - }); - it('when row count exceeds batch size', function () { - controller.batchSize = 3; - controller.addHistoricalData(mockDomainObject, mockSeries); - - //Timeout is called a minimum of two times - expect(mockAngularTimeout).toHaveBeenCalled(); - mockAngularTimeout.mostRecentCall.args[0](); - - expect(mockAngularTimeout.calls.length).toEqual(2); - mockAngularTimeout.mostRecentCall.args[0](); - - //Because it yields, timeout will have been called a - // third time for the batch. - expect(mockAngularTimeout.calls.length).toEqual(3); - mockAngularTimeout.mostRecentCall.args[0](); - - expect(controller.$scope.rows.length).toBe(5); - expect(controller.$scope.rows[0]).toBe(mockRow); - }); - it('cancelling any outstanding timeouts', function () { - controller.batchSize = 3; - controller.addHistoricalData(mockDomainObject, mockSeries); - - expect(mockAngularTimeout).toHaveBeenCalled(); - mockAngularTimeout.mostRecentCall.args[0](); - - controller.addHistoricalData(mockDomainObject, mockSeries); - - expect(mockAngularTimeout.cancel).toHaveBeenCalledWith(mockTimeoutHandle); - }); - it('cancels timeout on scope destruction', function () { - controller.batchSize = 3; - controller.addHistoricalData(mockDomainObject, mockSeries); - - //Destroy is used by parent class as well, so multiple - // calls are made to scope.$on - var destroyCalls = mockScope.$on.calls.filter(function (call) { - return call.args[0] === '$destroy'; - }); - //Call destroy function - expect(destroyCalls.length).toEqual(2); - - destroyCalls[0].args[1](); - expect(mockAngularTimeout.cancel).toHaveBeenCalledWith(mockTimeoutHandle); - - }); - }); - }); - } -); diff --git a/platform/features/table/test/controllers/MCTTableControllerSpec.js b/platform/features/table/test/controllers/MCTTableControllerSpec.js index 970ca72047..f57b981c50 100644 --- a/platform/features/table/test/controllers/MCTTableControllerSpec.js +++ b/platform/features/table/test/controllers/MCTTableControllerSpec.js @@ -39,21 +39,13 @@ define( var controller, mockScope, watches, - mockTimeout, + mockWindow, mockElement, mockExportService, mockConductor, mockFormatService, mockFormat; - function promise(value) { - return { - then: function (callback) { - return promise(callback(value)); - } - }; - } - function getCallback(target, event) { return target.calls.filter(function (call) { return call.args[0] === event; @@ -66,7 +58,8 @@ define( mockScope = jasmine.createSpyObj('scope', [ '$watch', '$on', - '$watchCollection' + '$watchCollection', + '$digest' ]); mockScope.$watchCollection.andCallFake(function (event, callback) { watches[event] = callback; @@ -86,8 +79,11 @@ define( ]); mockScope.displayHeaders = true; - mockTimeout = jasmine.createSpy('$timeout'); - mockTimeout.andReturn(promise(undefined)); + mockWindow = jasmine.createSpyObj('$window', ['requestAnimationFrame']); + mockWindow.requestAnimationFrame.andCallFake(function (f) { + return f(); + }); + mockFormat = jasmine.createSpyObj('formatter', [ 'parse', 'format' @@ -99,7 +95,7 @@ define( controller = new MCTTableController( mockScope, - mockTimeout, + mockWindow, mockElement, mockExportService, mockFormatService, @@ -114,12 +110,12 @@ define( expect(mockScope.$watch).toHaveBeenCalledWith('rows', jasmine.any(Function)); }); - it('destroys listeners on destruction', function () { - expect(mockScope.$on).toHaveBeenCalledWith('$destroy', controller.destroyConductorListeners); + it('unregisters listeners on destruction', function () { + expect(mockScope.$on).toHaveBeenCalledWith('$destroy', jasmine.any(Function)); getCallback(mockScope.$on, '$destroy')(); expect(mockConductor.off).toHaveBeenCalledWith('timeSystem', controller.changeTimeSystem); - expect(mockConductor.off).toHaveBeenCalledWith('timeOfInterest', controller.setTimeOfInterest); + expect(mockConductor.off).toHaveBeenCalledWith('timeOfInterest', controller.changeTimeOfInterest); expect(mockConductor.off).toHaveBeenCalledWith('bounds', controller.changeBounds); }); @@ -233,9 +229,20 @@ define( //Mock setting the rows on scope var rowsCallback = getCallback(mockScope.$watch, 'rows'); - rowsCallback(rowsAsc); + var setRowsPromise = rowsCallback(rowsAsc); + var promiseResolved = false; + setRowsPromise.then(function () { + promiseResolved = true; + }); + + waitsFor(function () { + return promiseResolved; + }, "promise to resolve", 100); + + runs(function () { + expect(mockScope.toiRowIndex).toBe(2); + }); - expect(mockScope.toiRowIndex).toBe(2); }); }); @@ -287,7 +294,7 @@ define( }); it('Supports adding rows individually', function () { - var addRowFunc = getCallback(mockScope.$on, 'add:row'), + var addRowFunc = getCallback(mockScope.$on, 'add:rows'), row4 = { 'col1': {'text': 'row3 col1'}, 'col2': {'text': 'ghi'}, @@ -296,15 +303,15 @@ define( controller.setRows(testRows); expect(mockScope.displayRows.length).toBe(3); testRows.push(row4); - addRowFunc(undefined, 3); + addRowFunc(undefined, [row4]); expect(mockScope.displayRows.length).toBe(4); }); it('Supports removing rows individually', function () { - var removeRowFunc = getCallback(mockScope.$on, 'remove:row'); + var removeRowFunc = getCallback(mockScope.$on, 'remove:rows'); controller.setRows(testRows); expect(mockScope.displayRows.length).toBe(3); - removeRowFunc(undefined, 2); + removeRowFunc(undefined, [testRows[2]]); expect(mockScope.displayRows.length).toBe(2); expect(controller.setVisibleRows).toHaveBeenCalled(); }); @@ -366,7 +373,7 @@ define( it('Allows sort column to be changed externally by ' + 'setting or changing sortBy attribute', function () { mockScope.displayRows = testRows; - var sortByCB = getCallback(mockScope.$watch, 'sortColumn'); + var sortByCB = getCallback(mockScope.$watch, 'defaultSort'); sortByCB('col2'); expect(mockScope.sortDirection).toEqual('asc'); @@ -381,10 +388,21 @@ define( it('updates visible rows in scope', function () { var oldRows; mockScope.rows = testRows; - controller.setRows(testRows); + var setRowsPromise = controller.setRows(testRows); + var promiseResolved = false; + setRowsPromise.then(function () { + promiseResolved = true; + }); oldRows = mockScope.visibleRows; mockScope.toggleSort('col2'); - expect(mockScope.visibleRows).not.toEqual(oldRows); + + waitsFor(function () { + return promiseResolved; + }, "promise to resolve", 100); + + runs(function () { + expect(mockScope.visibleRows).not.toEqual(oldRows); + }); }); it('correctly sorts rows of differing types', function () { @@ -464,21 +482,10 @@ define( mockScope.displayRows = controller.sortRows(testRows.slice(0)); - mockScope.rows.push(row4); - controller.addRows(undefined, mockScope.rows.length - 1); + controller.addRows(undefined, [row4, row5, row6, row6]); expect(mockScope.displayRows[0].col2.text).toEqual('xyz'); - - mockScope.rows.push(row5); - controller.addRows(undefined, mockScope.rows.length - 1); - expect(mockScope.displayRows[4].col2.text).toEqual('aaa'); - - mockScope.rows.push(row6); - controller.addRows(undefined, mockScope.rows.length - 1); - expect(mockScope.displayRows[2].col2.text).toEqual('ggg'); - - //Add a duplicate row - mockScope.rows.push(row6); - controller.addRows(undefined, mockScope.rows.length - 1); + expect(mockScope.displayRows[6].col2.text).toEqual('aaa'); + //Added a duplicate row expect(mockScope.displayRows[2].col2.text).toEqual('ggg'); expect(mockScope.displayRows[3].col2.text).toEqual('ggg'); }); @@ -493,13 +500,11 @@ define( mockScope.displayRows = controller.sortRows(testRows.slice(0)); mockScope.displayRows = controller.filterRows(testRows); - mockScope.rows.push(row5); - controller.addRows(undefined, mockScope.rows.length - 1); + controller.addRows(undefined, [row5]); expect(mockScope.displayRows.length).toBe(2); expect(mockScope.displayRows[1].col2.text).toEqual('aaa'); - mockScope.rows.push(row6); - controller.addRows(undefined, mockScope.rows.length - 1); + controller.addRows(undefined, [row6]); expect(mockScope.displayRows.length).toBe(2); //Row was not added because does not match filter }); @@ -512,12 +517,10 @@ define( mockScope.displayRows = testRows.slice(0); - mockScope.rows.push(row5); - controller.addRows(undefined, mockScope.rows.length - 1); + controller.addRows(undefined, [row5]); expect(mockScope.displayRows[3].col2.text).toEqual('aaa'); - mockScope.rows.push(row6); - controller.addRows(undefined, mockScope.rows.length - 1); + controller.addRows(undefined, [row6]); expect(mockScope.displayRows[4].col2.text).toEqual('ggg'); }); @@ -535,8 +538,7 @@ define( mockScope.displayRows = testRows.slice(0); - mockScope.rows.push(row7); - controller.addRows(undefined, mockScope.rows.length - 1); + controller.addRows(undefined, [row7]); expect(controller.$scope.sizingRow.col2).toEqual({text: 'some longer string'}); }); diff --git a/platform/features/table/test/controllers/RealtimeTableControllerSpec.js b/platform/features/table/test/controllers/RealtimeTableControllerSpec.js deleted file mode 100644 index bf29c3d7bd..0000000000 --- a/platform/features/table/test/controllers/RealtimeTableControllerSpec.js +++ /dev/null @@ -1,171 +0,0 @@ -/***************************************************************************** - * Open MCT, Copyright (c) 2014-2016, United States Government - * as represented by the Administrator of the National Aeronautics and Space - * Administration. All rights reserved. - * - * Open MCT is licensed under the Apache License, Version 2.0 (the - * "License"); you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * http://www.apache.org/licenses/LICENSE-2.0. - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT - * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the - * License for the specific language governing permissions and limitations - * under the License. - * - * Open MCT includes source code licensed under additional open source - * licenses. See the Open Source Licenses file (LICENSES.md) included with - * this source code distribution or the Licensing information page available - * at runtime from the About dialog for additional information. - *****************************************************************************/ - -define( - [ - "../../src/controllers/RealtimeTableController" - ], - function (TableController) { - - describe('The real-time table controller', function () { - var mockScope, - mockTelemetryHandler, - mockTelemetryHandle, - mockTelemetryFormatter, - mockDomainObject, - mockTable, - mockConfiguration, - watches, - mockTableRow, - mockConductor, - controller; - - function promise(value) { - return { - then: function (callback) { - return promise(callback(value)); - } - }; - } - - beforeEach(function () { - watches = {}; - mockTableRow = {'col1': 'val1', 'col2': 'row2'}; - - mockScope = jasmine.createSpyObj('scope', [ - '$on', - '$watch', - '$watchCollection', - '$digest', - '$broadcast' - ]); - mockScope.$on.andCallFake(function (expression, callback) { - watches[expression] = callback; - }); - mockScope.$watch.andCallFake(function (expression, callback) { - watches[expression] = callback; - }); - mockScope.$watchCollection.andCallFake(function (expression, callback) { - watches[expression] = callback; - }); - - mockConfiguration = { - 'range1': true, - 'range2': true, - 'domain1': true - }; - - mockTable = jasmine.createSpyObj('table', - [ - 'populateColumns', - 'buildColumnConfiguration', - 'getRowValues', - 'saveColumnConfiguration' - ] - ); - mockTable.columns = []; - mockTable.buildColumnConfiguration.andReturn(mockConfiguration); - mockTable.getRowValues.andReturn(mockTableRow); - - mockDomainObject = jasmine.createSpyObj('domainObject', [ - 'getCapability', - 'useCapability', - 'getModel' - ]); - mockDomainObject.getModel.andReturn({}); - mockDomainObject.getCapability.andReturn( - { - getMetadata: function () { - return {ranges: [{format: 'string'}]}; - } - }); - - mockScope.domainObject = mockDomainObject; - - mockTelemetryHandle = jasmine.createSpyObj('telemetryHandle', [ - 'getMetadata', - 'unsubscribe', - 'getDatum', - 'promiseTelemetryObjects', - 'getTelemetryObjects', - 'request', - 'getMetadata' - ]); - - // Arbitrary array with non-zero length, contents are not - // used by mocks - mockTelemetryHandle.getTelemetryObjects.andReturn([{}]); - mockTelemetryHandle.promiseTelemetryObjects.andReturn(promise(undefined)); - mockTelemetryHandle.getDatum.andReturn({}); - mockTelemetryHandle.request.andReturn(promise(undefined)); - mockTelemetryHandle.getMetadata.andReturn([]); - - mockTelemetryHandler = jasmine.createSpyObj('telemetryHandler', [ - 'handle' - ]); - mockTelemetryHandler.handle.andReturn(mockTelemetryHandle); - - mockConductor = jasmine.createSpyObj('conductor', [ - 'on', - 'off', - 'bounds', - 'timeSystem', - 'timeOfInterest' - ]); - - controller = new TableController(mockScope, mockTelemetryHandler, mockTelemetryFormatter, {conductor: mockConductor}); - controller.table = mockTable; - controller.handle = mockTelemetryHandle; - }); - - it('registers for streaming telemetry', function () { - controller.subscribe(); - expect(mockTelemetryHandler.handle).toHaveBeenCalledWith(jasmine.any(Object), jasmine.any(Function), true); - }); - - describe('receives new telemetry', function () { - beforeEach(function () { - controller.subscribe(); - mockScope.rows = []; - }); - - it('updates table with new streaming telemetry', function () { - mockTelemetryHandler.handle.mostRecentCall.args[1](); - expect(mockScope.$broadcast).toHaveBeenCalledWith('add:row', 0); - }); - it('observes the row limit', function () { - var i = 0; - controller.maxRows = 10; - - //Fill rows array with elements - for (; i < 10; i++) { - mockScope.rows.push({row: i}); - } - mockTelemetryHandler.handle.mostRecentCall.args[1](); - expect(mockScope.rows.length).toBe(controller.maxRows); - expect(mockScope.rows[mockScope.rows.length - 1]).toBe(mockTableRow); - expect(mockScope.rows[0].row).toBe(1); - }); - }); - }); - } -); diff --git a/platform/features/table/test/controllers/TelemetryTableControllerSpec.js b/platform/features/table/test/controllers/TelemetryTableControllerSpec.js new file mode 100644 index 0000000000..4f403edc32 --- /dev/null +++ b/platform/features/table/test/controllers/TelemetryTableControllerSpec.js @@ -0,0 +1,364 @@ +/***************************************************************************** + * Open MCT, Copyright (c) 2014-2016, United States Government + * as represented by the Administrator of the National Aeronautics and Space + * Administration. All rights reserved. + * + * Open MCT is licensed under the Apache License, Version 2.0 (the + * "License"); you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * http://www.apache.org/licenses/LICENSE-2.0. + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT + * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the + * License for the specific language governing permissions and limitations + * under the License. + * + * Open MCT includes source code licensed under additional open source + * licenses. See the Open Source Licenses file (LICENSES.md) included with + * this source code distribution or the Licensing information page available + * at runtime from the About dialog for additional information. + *****************************************************************************/ + +define( + [ + '../../src/controllers/TelemetryTableController', + '../../../../../src/api/objects/object-utils', + 'lodash' + ], + function (TelemetryTableController, objectUtils, _) { + + describe('The TelemetryTableController', function () { + + var controller, + mockScope, + mockTimeout, + mockConductor, + mockAPI, + mockDomainObject, + mockTelemetryAPI, + mockObjectAPI, + mockCompositionAPI, + unobserve, + mockBounds; + + function getCallback(target, event) { + return target.calls.filter(function (call) { + return call.args[0] === event; + })[0].args[1]; + } + + beforeEach(function () { + mockBounds = { + start: 0, + end: 10 + }; + mockConductor = jasmine.createSpyObj("conductor", [ + "bounds", + "follow", + "on", + "off", + "timeSystem" + ]); + mockConductor.bounds.andReturn(mockBounds); + mockConductor.follow.andReturn(false); + + mockDomainObject = jasmine.createSpyObj("domainObject", [ + "getModel", + "getId", + "useCapability" + ]); + mockDomainObject.getModel.andReturn({}); + mockDomainObject.getId.andReturn("mockId"); + mockDomainObject.useCapability.andReturn(true); + + mockCompositionAPI = jasmine.createSpyObj("compositionAPI", [ + "get" + ]); + + mockObjectAPI = jasmine.createSpyObj("objectAPI", [ + "observe" + ]); + unobserve = jasmine.createSpy("unobserve"); + mockObjectAPI.observe.andReturn(unobserve); + + mockScope = jasmine.createSpyObj("scope", [ + "$on", + "$watch", + "$broadcast" + ]); + mockScope.domainObject = mockDomainObject; + + mockTelemetryAPI = jasmine.createSpyObj("telemetryAPI", [ + "canProvideTelemetry", + "subscribe", + "getMetadata", + "commonValuesForHints", + "request", + "limitEvaluator", + "getValueFormatter" + ]); + mockTelemetryAPI.commonValuesForHints.andReturn([]); + mockTelemetryAPI.request.andReturn(Promise.resolve([])); + + + mockTelemetryAPI.canProvideTelemetry.andReturn(false); + + mockTimeout = jasmine.createSpy("timeout"); + mockTimeout.andReturn(1); // Return something + mockTimeout.cancel = jasmine.createSpy("cancel"); + + mockAPI = { + conductor: mockConductor, + objects: mockObjectAPI, + telemetry: mockTelemetryAPI, + composition: mockCompositionAPI + }; + controller = new TelemetryTableController(mockScope, mockTimeout, mockAPI); + }); + + describe('listens for', function () { + beforeEach(function () { + controller.registerChangeListeners(); + }); + it('object mutation', function () { + var calledObject = mockObjectAPI.observe.mostRecentCall.args[0]; + + expect(mockObjectAPI.observe).toHaveBeenCalled(); + expect(calledObject.identifier.key).toEqual(mockDomainObject.getId()); + }); + it('conductor changes', function () { + expect(mockConductor.on).toHaveBeenCalledWith("timeSystem", jasmine.any(Function)); + expect(mockConductor.on).toHaveBeenCalledWith("bounds", jasmine.any(Function)); + expect(mockConductor.on).toHaveBeenCalledWith("follow", jasmine.any(Function)); + }); + }); + + describe('deregisters all listeners on scope destruction', function () { + var timeSystemListener, + boundsListener, + followListener; + + beforeEach(function () { + controller.registerChangeListeners(); + + timeSystemListener = getCallback(mockConductor.on, "timeSystem"); + boundsListener = getCallback(mockConductor.on, "bounds"); + followListener = getCallback(mockConductor.on, "follow"); + + var destroy = getCallback(mockScope.$on, "$destroy"); + destroy(); + }); + + it('object mutation', function () { + expect(unobserve).toHaveBeenCalled(); + }); + it('conductor changes', function () { + expect(mockConductor.off).toHaveBeenCalledWith("timeSystem", timeSystemListener); + expect(mockConductor.off).toHaveBeenCalledWith("bounds", boundsListener); + expect(mockConductor.off).toHaveBeenCalledWith("follow", followListener); + }); + }); + + describe ('Subscribes to new data', function () { + var mockComposition, + mockTelemetryObject, + mockChildren, + unsubscribe, + done; + + beforeEach(function () { + mockComposition = jasmine.createSpyObj("composition", [ + "load" + ]); + + mockTelemetryObject = jasmine.createSpyObj("mockTelemetryObject", [ + "something" + ]); + mockTelemetryObject.identifier = { + key: "mockTelemetryObject" + }; + + unsubscribe = jasmine.createSpy("unsubscribe"); + mockTelemetryAPI.subscribe.andReturn(unsubscribe); + + mockChildren = [mockTelemetryObject]; + mockComposition.load.andReturn(Promise.resolve(mockChildren)); + mockCompositionAPI.get.andReturn(mockComposition); + + mockTelemetryAPI.canProvideTelemetry.andCallFake(function (obj) { + return obj.identifier.key === mockTelemetryObject.identifier.key; + }); + + done = false; + controller.getData().then(function () { + done = true; + }); + }); + + it('fetches historical data', function () { + waitsFor(function () { + return done; + }, "getData to return", 100); + + runs(function () { + expect(mockTelemetryAPI.request).toHaveBeenCalledWith(mockTelemetryObject, jasmine.any(Object)); + }); + }); + + it('fetches historical data for the time period specified by the conductor bounds', function () { + waitsFor(function () { + return done; + }, "getData to return", 100); + + runs(function () { + expect(mockTelemetryAPI.request).toHaveBeenCalledWith(mockTelemetryObject, mockBounds); + }); + }); + + it('subscribes to new data', function () { + waitsFor(function () { + return done; + }, "getData to return", 100); + + runs(function () { + expect(mockTelemetryAPI.subscribe).toHaveBeenCalledWith(mockTelemetryObject, jasmine.any(Function), {}); + }); + + }); + it('and unsubscribes on view destruction', function () { + waitsFor(function () { + return done; + }, "getData to return", 100); + + runs(function () { + var destroy = getCallback(mockScope.$on, "$destroy"); + destroy(); + + expect(unsubscribe).toHaveBeenCalled(); + }); + }); + }); + + it('When in real-time mode, enables auto-scroll', function () { + controller.registerChangeListeners(); + + var followCallback = getCallback(mockConductor.on, "follow"); + //Confirm pre-condition + expect(mockScope.autoScroll).toBeFalsy(); + + //Mock setting the conductor to 'follow' mode + followCallback(true); + expect(mockScope.autoScroll).toBe(true); + }); + + describe('populates table columns', function () { + var domainMetadata; + var allMetadata; + var mockTimeSystem; + + beforeEach(function () { + domainMetadata = [{ + key: "column1", + name: "Column 1", + hints: {} + }]; + + allMetadata = [{ + key: "column1", + name: "Column 1", + hints: {} + }, { + key: "column2", + name: "Column 2", + hints: {} + }, { + key: "column3", + name: "Column 3", + hints: {} + }]; + + mockTimeSystem = { + metadata: { + key: "column1" + } + }; + + mockTelemetryAPI.commonValuesForHints.andCallFake(function (metadata, hints) { + if (_.eq(hints, ["x"])) { + return domainMetadata; + } else if (_.eq(hints, [])) { + return allMetadata; + } + }); + + controller.loadColumns([mockDomainObject]); + }); + + it('based on metadata for given objects', function () { + expect(mockScope.headers).toBeDefined(); + expect(mockScope.headers.length).toBeGreaterThan(0); + expect(mockScope.headers.indexOf(allMetadata[0].name)).not.toBe(-1); + expect(mockScope.headers.indexOf(allMetadata[1].name)).not.toBe(-1); + expect(mockScope.headers.indexOf(allMetadata[2].name)).not.toBe(-1); + }); + + it('and sorts by column matching time system', function () { + expect(mockScope.defaultSort).not.toEqual("Column 1"); + controller.sortByTimeSystem(mockTimeSystem); + expect(mockScope.defaultSort).toEqual("Column 1"); + }); + + it('batches processing of rows for performance when receiving historical telemetry', function () { + var mockHistoricalData = [ + { + "column1": 1, + "column2": 2, + "column3": 3 + },{ + "column1": 4, + "column2": 5, + "column3": 6 + }, { + "column1": 7, + "column2": 8, + "column3": 9 + } + ]; + controller.batchSize = 2; + mockTelemetryAPI.request.andReturn(Promise.resolve(mockHistoricalData)); + controller.getHistoricalData([mockDomainObject]); + + waitsFor(function () { + return !!controller.timeoutHandle; + }, "first batch to be processed", 100); + + runs(function () { + //Verify that timeout is being used to yield process + expect(mockTimeout).toHaveBeenCalled(); + mockTimeout.mostRecentCall.args[0](); + expect(mockTimeout.calls.length).toBe(2); + mockTimeout.mostRecentCall.args[0](); + expect(mockScope.rows.length).toBe(3); + }); + }); + }); + + it('Removes telemetry rows from table when they fall out of bounds', function () { + var discardedRows = [ + {"column1": "value 1"}, + {"column2": "value 2"}, + {"column3": "value 3"} + ]; + + spyOn(controller.telemetry, "on").andCallThrough(); + + controller.registerChangeListeners(); + expect(controller.telemetry.on).toHaveBeenCalledWith("discarded", jasmine.any(Function)); + var onDiscard = getCallback(controller.telemetry.on, "discarded"); + onDiscard(discardedRows); + expect(mockScope.$broadcast).toHaveBeenCalledWith("remove:rows", discardedRows); + }); + + }); + }); From 34dc457affc4cf8b7a2f1f736dccd8f7314abcd6 Mon Sep 17 00:00:00 2001 From: Henry Date: Fri, 10 Feb 2017 15:35:17 -0800 Subject: [PATCH 24/90] [Tables] Restored telemetry datum field 'name'. Fixed bug with default sort not working --- .../src/controllers/MCTTableController.js | 6 ++- .../controllers/TelemetryTableController.js | 4 +- src/api/telemetry/LegacyTelemetryProvider.js | 43 +++++++++++-------- 3 files changed, 33 insertions(+), 20 deletions(-) diff --git a/platform/features/table/src/controllers/MCTTableController.js b/platform/features/table/src/controllers/MCTTableController.js index c987a0c3b8..d9f3a9f680 100644 --- a/platform/features/table/src/controllers/MCTTableController.js +++ b/platform/features/table/src/controllers/MCTTableController.js @@ -122,7 +122,11 @@ define( $scope.$watchCollection('filters', function () { self.setRows($scope.rows); }); - $scope.$watch('headers', this.setHeaders); + $scope.$watch('headers', function (newHeaders, oldHeaders) { + if (newHeaders !== oldHeaders) { + this.setHeaders(newHeaders); + } + }.bind(this)); $scope.$watch('rows', this.setRows); /* diff --git a/platform/features/table/src/controllers/TelemetryTableController.js b/platform/features/table/src/controllers/TelemetryTableController.js index 5beb49cb7e..daf042893e 100644 --- a/platform/features/table/src/controllers/TelemetryTableController.js +++ b/platform/features/table/src/controllers/TelemetryTableController.js @@ -240,6 +240,8 @@ define( TelemetryTableController.prototype.loadColumns = function (objects) { var telemetryApi = this.openmct.telemetry; + this.$scope.headers = []; + if (objects.length > 0) { var metadatas = objects.map(telemetryApi.getMetadata.bind(telemetryApi)); var allColumns = telemetryApi.commonValuesForHints(metadatas, []); @@ -437,8 +439,6 @@ define( return Promise.resolve(objects); } } - - scope.headers = []; scope.rows = []; return getDomainObjects() diff --git a/src/api/telemetry/LegacyTelemetryProvider.js b/src/api/telemetry/LegacyTelemetryProvider.js index f76fe99d10..1e8fe50deb 100644 --- a/src/api/telemetry/LegacyTelemetryProvider.js +++ b/src/api/telemetry/LegacyTelemetryProvider.js @@ -29,7 +29,8 @@ define([ * @implements module:openmct.TelemetryAPI~TelemetryProvider * @constructor */ - function LegacyTelemetryProvider(instantiate) { + function LegacyTelemetryProvider(openmct, instantiate) { + this.telemetryApi = openmct.telemetry; this.instantiate = instantiate; } @@ -45,22 +46,28 @@ define([ }; function createDatum(domainObject, metadata, legacySeries, i) { + var datum; + if (legacySeries.getDatum) { - return legacySeries.getDatum(i); + datum = legacySeries.getDatum(i); + } else { + datum = {}; + metadata.valuesForHints(['x']).forEach(function (metadatum) { + datum[metadatum.key] = legacySeries.getDomainValue(i, metadatum.key); + }); + + metadata.valuesForHints(['y']).forEach(function (metadatum) { + datum[metadatum.key] = legacySeries.getRangeValue(i, metadatum.key); + }); } - var datum = {}; - metadata.domains.reduce(function (d, domain) { - d[domain.key] = legacySeries.getDomainValue(i, domain.key); - return d; - }, datum); - - metadata.ranges.reduce(function (d, range) { - d[range.key] = legacySeries.getRangeValue(i, range.key); - return d; - }, datum); - - datum.name = domainObject.name; + /** + * If telemetry metadata defines a 'name' field, and one is not present + * on the datum, add it. + */ + if (metadata.value('name') !== undefined && datum.name === undefined) { + datum.name = domainObject.name; + } return datum; } @@ -93,11 +100,12 @@ define([ * telemetry data. */ LegacyTelemetryProvider.prototype.request = function (domainObject, request) { + var metadata = this.telemetryApi.getMetadata(domainObject); var oldObject = this.instantiate(utils.toOldFormat(domainObject), utils.makeKeyString(domainObject.identifier)); var capability = oldObject.getCapability("telemetry"); return capability.requestData(request).then(function (telemetrySeries) { - return Promise.resolve(adaptSeries(domainObject, capability.getMetadata(), telemetrySeries)); + return Promise.resolve(adaptSeries(domainObject, metadata, telemetrySeries)); }).catch(function (error) { return Promise.reject(error); }); @@ -118,11 +126,12 @@ define([ * @returns {platform|telemetry.TelemetrySubscription|*} */ LegacyTelemetryProvider.prototype.subscribe = function (domainObject, callback, request) { + var metadata = this.telemetryApi.getMetadata(domainObject); var oldObject = this.instantiate(utils.toOldFormat(domainObject), utils.makeKeyString(domainObject.identifier)); var capability = oldObject.getCapability("telemetry"); function callbackWrapper(series) { - callback(createDatum(domainObject, capability.getMetadata(), series, series.getPointCount() - 1)); + callback(createDatum(domainObject, metadata, series, series.getPointCount() - 1)); } return capability.subscribe(callbackWrapper, request); @@ -145,7 +154,7 @@ define([ // Push onto the start of the default providers array so that it's // always the last resort openmct.telemetry.defaultProviders.unshift( - new LegacyTelemetryProvider(instantiate)); + new LegacyTelemetryProvider(openmct, instantiate)); }; }); From fa962b42bc8c68c9e3181e4ce950cb88cd87e4e2 Mon Sep 17 00:00:00 2001 From: Pete Richards Date: Mon, 13 Feb 2017 13:12:58 -0800 Subject: [PATCH 25/90] [API] Use proper key format --- src/api/objects/LegacyObjectAPIInterceptor.js | 2 +- src/api/objects/MutableObject.js | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/src/api/objects/LegacyObjectAPIInterceptor.js b/src/api/objects/LegacyObjectAPIInterceptor.js index 666dcfd5c0..18bfaaac79 100644 --- a/src/api/objects/LegacyObjectAPIInterceptor.js +++ b/src/api/objects/LegacyObjectAPIInterceptor.js @@ -43,7 +43,7 @@ define([ var handleLegacyMutation; var handleMutation = function (newStyleObject) { - var keyString = utils.makeKeyString(newStyleObject.key); + var keyString = utils.makeKeyString(newStyleObject.identifier); var oldStyleObject = this.instantiate(utils.toOldFormat(newStyleObject), keyString); // Don't trigger self diff --git a/src/api/objects/MutableObject.js b/src/api/objects/MutableObject.js index 1eb5fe4e0e..e4a1d477c9 100644 --- a/src/api/objects/MutableObject.js +++ b/src/api/objects/MutableObject.js @@ -41,7 +41,7 @@ define([ } function qualifiedEventName(object, eventName) { - return [object.key.identifier, eventName].join(':'); + return [object.identifier.key, eventName].join(':'); } MutableObject.prototype.stopListening = function () { From 970acbd56e85cd50485f38cb8b76a270df075ed4 Mon Sep 17 00:00:00 2001 From: Pete Richards Date: Wed, 15 Feb 2017 12:15:49 -0800 Subject: [PATCH 26/90] [Build] Skip optimize in dev environment Skip optimize in dev environment to speed up project rebuilds. Very helpful when integration testing openmct.js build artifact. --- gulpfile.js | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/gulpfile.js b/gulpfile.js index 380f233758..d5e1435395 100644 --- a/gulpfile.js +++ b/gulpfile.js @@ -69,6 +69,11 @@ var gulp = require('gulp'), } }; +if (process.env.NODE_ENV === 'development') { + options.requirejsOptimize.optimize = 'none'; +} + + gulp.task('scripts', function () { var requirejsOptimize = require('gulp-requirejs-optimize'); var replace = require('gulp-replace-task'); From af9ffaf02dedf95fc80dee1c713592723cf36390 Mon Sep 17 00:00:00 2001 From: Pete Richards Date: Wed, 15 Feb 2017 10:51:36 -0800 Subject: [PATCH 27/90] Stop loading bundles.json Stop application from requesting bundles.json at first load. This was confusing to some external developers who would see an error in the log and not know the cause. --- platform/framework/src/Constants.js | 1 - platform/framework/src/FrameworkInitializer.js | 4 ++-- platform/framework/src/FrameworkLayer.js | 2 +- 3 files changed, 3 insertions(+), 4 deletions(-) diff --git a/platform/framework/src/Constants.js b/platform/framework/src/Constants.js index 663fde6d72..0ba19f4cd8 100644 --- a/platform/framework/src/Constants.js +++ b/platform/framework/src/Constants.js @@ -25,7 +25,6 @@ */ define({ MODULE_NAME: "OpenMCTWeb", - BUNDLE_LISTING_FILE: "bundles.json", BUNDLE_FILE: "bundle.json", SEPARATOR: "/", EXTENSION_SUFFIX: "[]", diff --git a/platform/framework/src/FrameworkInitializer.js b/platform/framework/src/FrameworkInitializer.js index cea0a6ddb7..4a56998b61 100644 --- a/platform/framework/src/FrameworkInitializer.js +++ b/platform/framework/src/FrameworkInitializer.js @@ -61,8 +61,8 @@ define( * @param bundleList * @returns {*} */ - FrameworkInitializer.prototype.runApplication = function (bundleList) { - return this.loader.loadBundles(bundleList) + FrameworkInitializer.prototype.runApplication = function () { + return this.loader.loadBundles([]) .then(bind(this.resolver.resolveBundles, this.resolver)) .then(bind(this.registrar.registerExtensions, this.registrar)) .then(bind(this.bootstrapper.bootstrap, this.bootstrapper)); diff --git a/platform/framework/src/FrameworkLayer.js b/platform/framework/src/FrameworkLayer.js index 067bf763bc..e84b1e29e0 100644 --- a/platform/framework/src/FrameworkLayer.js +++ b/platform/framework/src/FrameworkLayer.js @@ -98,7 +98,7 @@ define([ // Initialize the application $log.info("Initializing application."); - initializer.runApplication(Constants.BUNDLE_LISTING_FILE); + initializer.runApplication(); }; return FrameworkLayer; From aaedf5d57620e3a9e0b1ff657b4d16b31b9e5c83 Mon Sep 17 00:00:00 2001 From: Pete Richards Date: Wed, 15 Feb 2017 15:02:39 -0800 Subject: [PATCH 28/90] cssclass is now cssClass Make property name consistent with standard camelCase naming. --- docs/src/tutorials/index.md | 62 +++++++++---------- example/eventGenerator/bundle.js | 2 +- example/export/bundle.js | 2 +- example/generator/bundle.js | 10 +-- example/imagery/bundle.js | 2 +- example/localTimeSystem/src/LADTickSource.js | 2 +- example/msl/bundle.js | 6 +- example/plotOptions/bundle.js | 4 +- example/profiling/src/DigestIndicator.js | 2 +- platform/commonUI/browse/bundle.js | 4 +- .../browse/src/windowing/FullscreenAction.js | 4 +- .../test/windowing/FullscreenActionSpec.js | 2 +- platform/commonUI/edit/bundle.js | 14 ++--- .../res/templates/create/create-menu.html | 4 +- .../res/templates/edit-action-buttons.html | 6 +- .../src/controllers/EditActionController.js | 2 +- .../commonUI/edit/src/creation/AddAction.js | 2 +- .../edit/src/creation/CreateAction.js | 2 +- .../edit/src/creation/CreateWizard.js | 2 +- .../controllers/EditActionControllerSpec.js | 4 +- .../edit/test/creation/CreateActionSpec.js | 2 +- .../res/templates/controls/action-button.html | 2 +- .../res/templates/controls/switcher.html | 4 +- .../res/templates/menu/context-menu.html | 2 +- platform/core/bundle.js | 12 ++-- platform/core/src/actions/ActionAggregator.js | 2 +- platform/core/src/types/TypeImpl.js | 6 +- platform/core/test/types/TypeImplSpec.js | 2 +- platform/core/test/types/TypeProviderSpec.js | 6 +- platform/entanglement/bundle.js | 10 +-- .../src/services/LocationService.js | 2 +- platform/features/clock/bundle.js | 12 ++-- .../clock/src/controllers/TimerController.js | 4 +- .../test/controllers/TimerControllerSpec.js | 6 +- .../templates/mode-selector/mode-menu.html | 4 +- .../core/src/timeSystems/LocalClock.js | 2 +- .../core/src/ui/TimeConductorViewService.js | 6 +- .../utcTimeSystem/src/UTCTimeSystem.js | 4 +- platform/features/fixed/bundle.js | 46 +++++++------- platform/features/imagery/bundle.js | 2 +- platform/features/layout/bundle.js | 54 ++++++++-------- .../layout/src/elements/ElementFactory.js | 2 +- platform/features/pages/bundle.js | 4 +- platform/features/plot/bundle.js | 2 +- .../features/plot/res/templates/plot.html | 4 +- platform/features/plot/src/PlotController.js | 2 +- .../plot/src/modes/PlotModeOptions.js | 6 +- platform/features/static-markup/bundle.js | 2 +- platform/features/table/bundle.js | 8 +-- platform/features/timeline/bundle.js | 26 ++++---- .../forms/res/templates/controls/button.html | 2 +- .../forms/res/templates/controls/color.html | 2 +- .../res/templates/controls/composite.html | 2 +- .../res/templates/controls/menu-button.html | 4 +- .../res/templates/controls/textarea.html | 2 +- .../res/templates/controls/textfield.html | 2 +- platform/forms/res/templates/form.html | 4 +- .../src/controllers/DialogButtonController.js | 2 +- .../controllers/DialogButtonControllerSpec.js | 4 +- .../search/res/templates/search-menu.html | 2 +- .../test/controllers/SearchControllerSpec.js | 2 +- .../controllers/SearchMenuControllerSpec.js | 4 +- src/api/telemetry/TelemetryAPI.js | 2 +- src/api/types/Type.js | 2 +- src/api/types/TypeRegistry.js | 2 +- src/ui/ViewRegistry.js | 2 +- 66 files changed, 212 insertions(+), 212 deletions(-) diff --git a/docs/src/tutorials/index.md b/docs/src/tutorials/index.md index fc5f47971e..e8f4417890 100644 --- a/docs/src/tutorials/index.md +++ b/docs/src/tutorials/index.md @@ -320,7 +320,7 @@ define([ + { + "key": "example.todo", + "name": "To-Do List", -+ "cssclass": "icon-check", ++ "cssClass": "icon-check", + "description": "A list of things that need to be done.", + "features": ["creation"] + } @@ -340,7 +340,7 @@ Going through the properties we've defined: domain objects of this type. * The `name` of "To-Do List" is the human-readable name for this type, and will be shown to users. -* The `cssclass` maps to an icon that will be shown for each To-Do List. The icons +* The `cssClass` maps to an icon that will be shown for each To-Do List. The icons are defined in our [custom open MCT icon set](https://github.com/nasa/openmct/blob/master/platform/commonUI/general/res/sass/_glyphs.scss). A complete list of available icons will be provided in the future. * The `description` is also human-readable, and will be used whenever a longer @@ -416,7 +416,7 @@ define([ { "key": "example.todo", "name": "To-Do List", - "cssclass": "icon-check", + "cssClass": "icon-check", "description": "A list of things that need to be done.", "features": ["creation"] } @@ -425,7 +425,7 @@ define([ + { + "key": "example.todo", + "type": "example.todo", -+ "cssclass": "icon-check", ++ "cssClass": "icon-check", + "name": "List", + "templateUrl": "templates/todo.html", + "editable": true @@ -447,7 +447,7 @@ the domain object type, but could have chosen any unique name. domain objects of that type. This means that we'll see this view for To-do Lists that we create, but not for other domain objects (such as Folders.) -* The `cssclass` and `name` properties describe the icon and human-readable name +* The `cssClass` and `name` properties describe the icon and human-readable name for this view to display in the UI where needed (if multiple views are available for To-do Lists, the user will be able to choose one.) @@ -473,7 +473,7 @@ define([ { "key": "example.todo", "name": "To-Do List", - "cssclass": "icon-check", + "cssClass": "icon-check", "description": "A list of things that need to be done.", "features": ["creation"], + "model": { @@ -488,7 +488,7 @@ define([ { "key": "example.todo", "type": "example.todo", - "cssclass": "icon-check", + "cssClass": "icon-check", "name": "List", "templateUrl": "templates/todo.html", "editable": true @@ -647,7 +647,7 @@ define([ { "key": "example.todo", "name": "To-Do List", - "cssclass": "icon-check", + "cssClass": "icon-check", "description": "A list of things that need to be done.", "features": ["creation"], "model": { @@ -662,7 +662,7 @@ define([ { "key": "example.todo", "type": "example.todo", - "cssclass": "icon-check", + "cssClass": "icon-check", "name": "List", "templateUrl": "templates/todo.html", "editable": true @@ -741,7 +741,7 @@ define([ { "key": "example.todo", "name": "To-Do List", - "cssclass": "icon-check", + "cssClass": "icon-check", "description": "A list of things that need to be done.", "features": ["creation"], "model": { @@ -756,7 +756,7 @@ define([ { "key": "example.todo", "type": "example.todo", - "cssclass": "icon-check", + "cssClass": "icon-check", "name": "List", "templateUrl": "templates/todo.html", "editable": true, @@ -766,7 +766,7 @@ define([ + "items": [ + { + "text": "Add Task", -+ "cssclass": "icon-plus", ++ "cssClass": "icon-plus", + "method": "addTask", + "control": "button" + } @@ -775,7 +775,7 @@ define([ + { + "items": [ + { -+ "cssclass": "icon-trash", ++ "cssClass": "icon-trash", + "method": "removeTask", + "control": "button" + } @@ -971,7 +971,7 @@ define([ { "key": "example.todo", "name": "To-Do List", - "cssclass": "icon-check", + "cssClass": "icon-check", "description": "A list of things that need to be done.", "features": ["creation"], "model": { @@ -986,7 +986,7 @@ define([ { "key": "example.todo", "type": "example.todo", - "cssclass": "icon-check", + "cssClass": "icon-check", "name": "List", "templateUrl": "templates/todo.html", "editable": true, @@ -996,7 +996,7 @@ define([ "items": [ { "text": "Add Task", - "cssclass": "icon-plus", + "cssClass": "icon-plus", "method": "addTask", "control": "button" } @@ -1005,7 +1005,7 @@ define([ { "items": [ { - "cssclass": "icon-trash", + "cssClass": "icon-trash", "method": "removeTask", "control": "button" } @@ -1236,7 +1236,7 @@ define([ { "key": "example.todo", "name": "To-Do List", - "cssclass": "icon-check", + "cssClass": "icon-check", "description": "A list of things that need to be done.", "features": ["creation"], "model": { @@ -1248,7 +1248,7 @@ define([ { "key": "example.todo", "type": "example.todo", - "cssclass": "icon-check", + "cssClass": "icon-check", "name": "List", "templateUrl": "templates/todo.html", "editable": true, @@ -1258,7 +1258,7 @@ define([ "items": [ { "text": "Add Task", - "cssclass": "icon-plus", + "cssClass": "icon-plus", "method": "addTask", "control": "button" } @@ -1267,7 +1267,7 @@ define([ { "items": [ { - "cssclass": "icon-trash", + "cssClass": "icon-trash", "method": "removeTask", "control": "button" } @@ -1374,7 +1374,7 @@ define([ { "name": "Bar Graph", "key": "example.bargraph", - "cssclass": "icon-autoflow-tabular", + "cssClass": "icon-autoflow-tabular", "templateUrl": "templates/bargraph.html", "needs": [ "telemetry" ], "delegation": true @@ -1677,7 +1677,7 @@ define([ { "name": "Bar Graph", "key": "example.bargraph", - "cssclass": "icon-autoflow-tabular", + "cssClass": "icon-autoflow-tabular", "templateUrl": "templates/bargraph.html", "needs": [ "telemetry" ], "delegation": true @@ -1843,7 +1843,7 @@ define([ { "name": "Bar Graph", "key": "example.bargraph", - "cssclass": "icon-autoflow-tabular", + "cssClass": "icon-autoflow-tabular", "templateUrl": "templates/bargraph.html", "needs": [ "telemetry" ], "delegation": true, @@ -2320,7 +2320,7 @@ define([ { "name": "Spacecraft", "key": "example.spacecraft", - "cssclass": "icon-object" + "cssClass": "icon-object" } ], "roots": [ @@ -2706,18 +2706,18 @@ define([ { "name": "Spacecraft", "key": "example.spacecraft", - "cssclass": "icon-object" + "cssClass": "icon-object" }, + { + "name": "Subsystem", + "key": "example.subsystem", -+ "cssclass": "icon-object", ++ "cssClass": "icon-object", + "model": { "composition": [] } + }, + { + "name": "Measurement", + "key": "example.measurement", -+ "cssclass": "icon-telemetry", ++ "cssClass": "icon-telemetry", + "model": { "telemetry": {} }, + "telemetry": { + "source": "example.source", @@ -3031,18 +3031,18 @@ define([ { "name": "Spacecraft", "key": "example.spacecraft", - "cssclass": "icon-object" + "cssClass": "icon-object" }, { "name": "Subsystem", "key": "example.subsystem", - "cssclass": "icon-object", + "cssClass": "icon-object", "model": { "composition": [] } }, { "name": "Measurement", "key": "example.measurement", - "cssclass": "icon-telemetry", + "cssClass": "icon-telemetry", "model": { "telemetry": {} }, "telemetry": { "source": "example.source", diff --git a/example/eventGenerator/bundle.js b/example/eventGenerator/bundle.js index 157fa62542..425de02157 100644 --- a/example/eventGenerator/bundle.js +++ b/example/eventGenerator/bundle.js @@ -49,7 +49,7 @@ define([ { "key": "eventGenerator", "name": "Event Message Generator", - "cssclass": "icon-folder-new", + "cssClass": "icon-folder-new", "description": "For development use. Creates sample event message data that mimics a live data stream.", "priority": 10, "features": "creation", diff --git a/example/export/bundle.js b/example/export/bundle.js index 5322d53a83..bcafee4050 100644 --- a/example/export/bundle.js +++ b/example/export/bundle.js @@ -36,7 +36,7 @@ define([ "name": "Export Telemetry as CSV", "implementation": ExportTelemetryAsCSVAction, "category": "contextual", - "cssclass": "icon-download", + "cssClass": "icon-download", "depends": [ "exportService" ] } ] diff --git a/example/generator/bundle.js b/example/generator/bundle.js index 259c5cff15..c2edb09f23 100644 --- a/example/generator/bundle.js +++ b/example/generator/bundle.js @@ -86,7 +86,7 @@ define([ { "key": "generator", "name": "Sine Wave Generator", - "cssclass": "icon-telemetry", + "cssClass": "icon-telemetry", "description": "For development use. Generates example streaming telemetry data using a simple sine wave algorithm.", "priority": 10, "features": "creation", @@ -130,7 +130,7 @@ define([ { "name": "Period", "control": "textfield", - "cssclass": "l-input-sm l-numeric", + "cssClass": "l-input-sm l-numeric", "key": "period", "required": true, "property": [ @@ -142,7 +142,7 @@ define([ { "name": "Amplitude", "control": "textfield", - "cssclass": "l-input-sm l-numeric", + "cssClass": "l-input-sm l-numeric", "key": "amplitude", "required": true, "property": [ @@ -154,7 +154,7 @@ define([ { "name": "Offset", "control": "textfield", - "cssclass": "l-input-sm l-numeric", + "cssClass": "l-input-sm l-numeric", "key": "offset", "required": true, "property": [ @@ -166,7 +166,7 @@ define([ { "name": "Data Rate (hz)", "control": "textfield", - "cssclass": "l-input-sm l-numeric", + "cssClass": "l-input-sm l-numeric", "key": "dataRateInHz", "required": true, "property": [ diff --git a/example/imagery/bundle.js b/example/imagery/bundle.js index 6232229e40..2e16b1bb3e 100644 --- a/example/imagery/bundle.js +++ b/example/imagery/bundle.js @@ -49,7 +49,7 @@ define([ { "key": "imagery", "name": "Example Imagery", - "cssclass": "icon-image", + "cssClass": "icon-image", "features": "creation", "description": "For development use. Creates example imagery data that mimics a live imagery stream.", "priority": 10, diff --git a/example/localTimeSystem/src/LADTickSource.js b/example/localTimeSystem/src/LADTickSource.js index a4999d6964..f8efc65d04 100644 --- a/example/localTimeSystem/src/LADTickSource.js +++ b/example/localTimeSystem/src/LADTickSource.js @@ -31,7 +31,7 @@ define(['../../../platform/features/conductor/core/src/timeSystems/LocalClock'], this.metadata = { key: 'test-lad', mode: 'lad', - cssclass: 'icon-clock', + cssClass: 'icon-clock', label: 'Latest Available Data', name: 'Latest available data', description: 'Monitor real-time streaming data as it comes in. The Time Conductor and displays will automatically advance themselves based on a UTC clock.' diff --git a/example/msl/bundle.js b/example/msl/bundle.js index 12dbbb5157..e40200d13a 100644 --- a/example/msl/bundle.js +++ b/example/msl/bundle.js @@ -41,18 +41,18 @@ define([ { "name":"Mars Science Laboratory", "key": "msl.curiosity", - "cssclass": "icon-object" + "cssClass": "icon-object" }, { "name": "Instrument", "key": "msl.instrument", - "cssclass": "icon-object", + "cssClass": "icon-object", "model": {"composition": []} }, { "name": "Measurement", "key": "msl.measurement", - "cssclass": "icon-telemetry", + "cssClass": "icon-telemetry", "model": {"telemetry": {}}, "telemetry": { "source": "rems.source", diff --git a/example/plotOptions/bundle.js b/example/plotOptions/bundle.js index ae47d2a97c..15c420c3f7 100644 --- a/example/plotOptions/bundle.js +++ b/example/plotOptions/bundle.js @@ -81,7 +81,7 @@ define([ { "key": "plot", "name": "Example Telemetry Plot", - "cssclass": "icon-telemetry-panel", + "cssClass": "icon-telemetry-panel", "description": "For development use. A plot for displaying telemetry.", "priority": 10, "delegates": [ @@ -129,7 +129,7 @@ define([ { "name": "Period", "control": "textfield", - "cssclass": "l-input-sm l-numeric", + "cssClass": "l-input-sm l-numeric", "key": "period", "required": true, "property": [ diff --git a/example/profiling/src/DigestIndicator.js b/example/profiling/src/DigestIndicator.js index 826dd2f23f..5839cca904 100644 --- a/example/profiling/src/DigestIndicator.js +++ b/example/profiling/src/DigestIndicator.js @@ -63,7 +63,7 @@ define( * Get the CSS class that defines the icon * to display in this indicator. This will appear * as a dataflow icon. - * @returns {string} the cssclass of the dataflow icon + * @returns {string} the cssClass of the dataflow icon */ getCssClass: function () { return "icon-connectivity"; diff --git a/platform/commonUI/browse/bundle.js b/platform/commonUI/browse/bundle.js index 3d3d76ee8e..9a883ded23 100644 --- a/platform/commonUI/browse/bundle.js +++ b/platform/commonUI/browse/bundle.js @@ -226,7 +226,7 @@ define([ "$window" ], "group": "windowing", - "cssclass": "icon-new-window", + "cssClass": "icon-new-window", "priority": "preferred" }, { @@ -241,7 +241,7 @@ define([ { "key": "items", "name": "Items", - "cssclass": "icon-thumbs-strip", + "cssClass": "icon-thumbs-strip", "description": "Grid of available items", "template": itemsTemplate, "uses": [ diff --git a/platform/commonUI/browse/src/windowing/FullscreenAction.js b/platform/commonUI/browse/src/windowing/FullscreenAction.js index 975562af0c..5c74ace73e 100644 --- a/platform/commonUI/browse/src/windowing/FullscreenAction.js +++ b/platform/commonUI/browse/src/windowing/FullscreenAction.js @@ -46,12 +46,12 @@ define( }; FullscreenAction.prototype.getMetadata = function () { - // We override getMetadata, because the icon cssclass and + // We override getMetadata, because the icon cssClass and // description need to be determined at run-time // based on whether or not we are currently // full screen. var metadata = Object.create(FullscreenAction); - metadata.cssclass = screenfull.isFullscreen ? "icon-fullscreen-expand" : "icon-fullscreen-collapse"; + metadata.cssClass = screenfull.isFullscreen ? "icon-fullscreen-expand" : "icon-fullscreen-collapse"; metadata.description = screenfull.isFullscreen ? EXIT_FULLSCREEN : ENTER_FULLSCREEN; metadata.group = "windowing"; diff --git a/platform/commonUI/browse/test/windowing/FullscreenActionSpec.js b/platform/commonUI/browse/test/windowing/FullscreenActionSpec.js index 913535fa59..423b9a2eb0 100644 --- a/platform/commonUI/browse/test/windowing/FullscreenActionSpec.js +++ b/platform/commonUI/browse/test/windowing/FullscreenActionSpec.js @@ -51,7 +51,7 @@ define( }); it("provides displayable metadata", function () { - expect(action.getMetadata().cssclass).toBeDefined(); + expect(action.getMetadata().cssClass).toBeDefined(); }); }); diff --git a/platform/commonUI/edit/bundle.js b/platform/commonUI/edit/bundle.js index 75c0269373..d4e63e063b 100644 --- a/platform/commonUI/edit/bundle.js +++ b/platform/commonUI/edit/bundle.js @@ -163,7 +163,7 @@ define([ ], "description": "Edit", "category": "view-control", - "cssclass": "major icon-pencil" + "cssClass": "major icon-pencil" }, { "key": "properties", @@ -172,7 +172,7 @@ define([ "view-control" ], "implementation": PropertiesAction, - "cssclass": "major icon-pencil", + "cssClass": "major icon-pencil", "name": "Edit Properties...", "description": "Edit properties of this object.", "depends": [ @@ -183,7 +183,7 @@ define([ "key": "remove", "category": "contextual", "implementation": RemoveAction, - "cssclass": "icon-trash", + "cssClass": "icon-trash", "name": "Remove", "description": "Remove this object from its containing object.", "depends": [ @@ -195,7 +195,7 @@ define([ "category": "save", "implementation": SaveAndStopEditingAction, "name": "Save and Finish Editing", - "cssclass": "icon-save labeled", + "cssClass": "icon-save labeled", "description": "Save changes made to these objects.", "depends": [ "dialogService", @@ -207,7 +207,7 @@ define([ "category": "save", "implementation": SaveAction, "name": "Save and Continue Editing", - "cssclass": "icon-save labeled", + "cssClass": "icon-save labeled", "description": "Save changes made to these objects.", "depends": [ "dialogService", @@ -219,7 +219,7 @@ define([ "category": "save", "implementation": SaveAsAction, "name": "Save As...", - "cssclass": "icon-save labeled", + "cssClass": "icon-save labeled", "description": "Save changes made to these objects.", "depends": [ "$injector", @@ -237,7 +237,7 @@ define([ // Because we use the name as label for edit buttons and mct-control buttons need // the label to be set to undefined in order to not apply the labeled CSS rule. "name": undefined, - "cssclass": "icon-x no-label", + "cssClass": "icon-x no-label", "description": "Discard changes made to these objects.", "depends": [] } diff --git a/platform/commonUI/edit/res/templates/create/create-menu.html b/platform/commonUI/edit/res/templates/create/create-menu.html index 64c05e9d38..32ba58431f 100644 --- a/platform/commonUI/edit/res/templates/create/create-menu.html +++ b/platform/commonUI/edit/res/templates/create/create-menu.html @@ -25,14 +25,14 @@
  • + class="menu-item-a {{ createAction.getMetadata().cssClass }}"> {{createAction.getMetadata().name}}