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); + }); + + }); + } +);