text: /*****************************************************************************
   * Open MCT, Copyright (c) 2014-2022, United States Government
   * as represented by the Administrator of the National Aeronautics and Space
   * Administration. All rights reserved.
   *
   * Open MCT is licensed under the Apache License, Version 2.0 (the
   * "License"); you may not use this file except in compliance with the License.
   * You may obtain a copy of the License at
   * http://www.apache.org/licenses/LICENSE-2.0.
   *
   * Unless required by applicable law or agreed to in writing, software
   * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
   * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
   * License for the specific language governing permissions and limitations
   * under the License.
   *
   * Open MCT includes source code licensed under additional open source
   * licenses. See the Open Source Licenses file (LICENSES.md) included with
   * this source code distribution or the Licensing information page available
   * at runtime from the About dialog for additional information.
   *****************************************************************************/
  
  import EventEmitter from 'EventEmitter';
  import eventHelpers from '../lib/eventHelpers';
  import { MARKER_SHAPES } from './MarkerShapes';
  
  // WebGL shader sources (for drawing plain colors)
  const FRAGMENT_SHADER = `
          precision mediump float;
          uniform vec4 uColor;
          uniform int uMarkerShape;
          
          void main(void) {
              gl_FragColor = uColor;
  
              if (uMarkerShape > 1) {
                  vec2 clipSpacePointCoord = 2.0 * gl_PointCoord - 1.0;
  
                  if (uMarkerShape == 2) { // circle
                      float distance = length(clipSpacePointCoord);
  
                      if (distance > 1.0) {
                          discard;
                      }
                  } else if (uMarkerShape == 3) { // diamond
                      float distance = abs(clipSpacePointCoord.x) + abs(clipSpacePointCoord.y);
  
                      if (distance > 1.0) {
                          discard;
                      }
                  } else if (uMarkerShape == 4) { // triangle
                      float x = clipSpacePointCoord.x;
                      float y = clipSpacePointCoord.y;
                      float distance = 2.0 * x - 1.0;
                      float distance2 = -2.0 * x - 1.0;
  
                      if (distance > y || distance2 > y) {
                          discard;
                      }
                  }
  
              }
          }
      `;
  
  const VERTEX_SHADER = `
          attribute vec2 aVertexPosition;
          uniform vec2 uDimensions;
          uniform vec2 uOrigin;
          uniform float uPointSize;
          
          void main(void) {
              gl_Position = vec4(2.0 * ((aVertexPosition - uOrigin) / uDimensions) - vec2(1,1), 0, 1);
              gl_PointSize = uPointSize;
          }
      `;
  
  /**
   * Create a draw api utilizing WebGL.
   *
   * @constructor
   * @param {CanvasElement} canvas the canvas object to render upon
   * @throws {Error} an error is thrown if WebGL is unavailable.
   */
  function DrawWebGL(canvas, overlay) {
      this.canvas = canvas;
      this.gl = this.canvas.getContext("webgl", { preserveDrawingBuffer: true })
          || this.canvas.getContext("experimental-webgl", { preserveDrawingBuffer: true });
  
      this.overlay = overlay;
      this.c2d = overlay.getContext('2d');
      if (!this.c2d) {
          throw new Error("No canvas 2d!");
      }
  
      // Ensure a context was actually available before proceeding
      if (!this.gl) {
          throw new Error("WebGL unavailable.");
      }
  
      this.initContext();
  
      this.listenTo(this.canvas, "webglcontextlost", this.onContextLost, this);
  }
  
  Object.assign(DrawWebGL.prototype, EventEmitter.prototype);
  eventHelpers.extend(DrawWebGL.prototype);
  
  DrawWebGL.prototype.onContextLost = function (event) {
      this.emit('error');
      this.isContextLost = true;
      this.destroy();
  };
  
  DrawWebGL.prototype.initContext = function () {
      // Initialize shaders
      this.vertexShader = this.gl.createShader(this.gl.VERTEX_SHADER);
      this.gl.shaderSource(this.vertexShader, VERTEX_SHADER);
      this.gl.compileShader(this.vertexShader);
  
      this.fragmentShader = this.gl.createShader(this.gl.FRAGMENT_SHADER);
      this.gl.shaderSource(this.fragmentShader, FRAGMENT_SHADER);
      this.gl.compileShader(this.fragmentShader);
  
      // Assemble vertex/fragment shaders into programs
      this.program = this.gl.createProgram();
      this.gl.attachShader(this.program, this.vertexShader);
      this.gl.attachShader(this.program, this.fragmentShader);
      this.gl.linkProgram(this.program);
      this.gl.useProgram(this.program);
  
      // Get locations for attribs/uniforms from the
      // shader programs (to pass values into shaders at draw-time)
      this.aVertexPosition = this.gl.getAttribLocation(this.program, "aVertexPosition");
      this.uColor = this.gl.getUniformLocation(this.program, "uColor");
      this.uMarkerShape = this.gl.getUniformLocation(this.program, "uMarkerShape");
      this.uDimensions = this.gl.getUniformLocation(this.program, "uDimensions");
      this.uOrigin = this.gl.getUniformLocation(this.program, "uOrigin");
      this.uPointSize = this.gl.getUniformLocation(this.program, "uPointSize");
  
      this.gl.enableVertexAttribArray(this.aVertexPosition);
  
      // Create a buffer to holds points which will be drawn
      this.buffer = this.gl.createBuffer();
  
      // Enable blending, for smoothness
      this.gl.enable(this.gl.BLEND);
      this.gl.blendFunc(this.gl.SRC_ALPHA, this.gl.ONE_MINUS_SRC_ALPHA);
  
  };
  
  DrawWebGL.prototype.destroy = function () {
      this.stopListening();
  };
  
  // Convert from logical to physical x coordinates
  DrawWebGL.prototype.x = function (v) {
      return ((v - this.origin[0]) / this.dimensions[0]) * this.width;
  };
  
  // Convert from logical to physical y coordinates
  DrawWebGL.prototype.y = function (v) {
      return this.height
          - ((v - this.origin[1]) / this.dimensions[1]) * this.height;
  };
  
  DrawWebGL.prototype.doDraw = function (drawType, buf, color, points, shape) {
      if (this.isContextLost) {
          return;
      }
  
      const shapeCode = MARKER_SHAPES[shape] ? MARKER_SHAPES[shape].drawWebGL : 0;
  
      this.gl.bindBuffer(this.gl.ARRAY_BUFFER, this.buffer);
      this.gl.bufferData(this.gl.ARRAY_BUFFER, buf, this.gl.DYNAMIC_DRAW);
      this.gl.vertexAttribPointer(this.aVertexPosition, 2, this.gl.FLOAT, false, 0, 0);
      this.gl.uniform4fv(this.uColor, color);
      this.gl.uniform1i(this.uMarkerShape, shapeCode);
      if (points !== 0) {
          this.gl.drawArrays(drawType, 0, points);
      }
  };
  
  DrawWebGL.prototype.clear = function () {
      if (this.isContextLost) {
          return;
      }
  
      this.height = this.canvas.height = this.canvas.offsetHeight;
      this.width = this.canvas.width = this.canvas.offsetWidth;
      this.overlay.height = this.overlay.offsetHeight;
      this.overlay.width = this.overlay.offsetWidth;
      // 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.
      this.gl.viewport(
          0,
          0,
          this.gl.drawingBufferWidth,
          this.gl.drawingBufferHeight
      );
      this.gl.clear(this.gl.COLOR_BUFFER_BIT + this.gl.DEPTH_BUFFER_BIT);
  };
  
  /**
   * 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
   */
  DrawWebGL.prototype.setDimensions = function (dimensions, origin) {
      this.dimensions = dimensions;
      this.origin = origin;
      if (this.isContextLost) {
          return;
      }
  
      if (dimensions && dimensions.length > 0
          && origin && origin.length > 0) {
          this.gl.uniform2fv(this.uDimensions, dimensions);
          this.gl.uniform2fv(this.uOrigin, origin);
      }
  };
  
  /**
   * 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
   */
  DrawWebGL.prototype.drawLine = function (buf, color, points) {
      if (this.isContextLost) {
          return;
      }
  
      this.doDraw(this.gl.LINE_STRIP, buf, color, points);
  };
  
  /**
   * Draw the buffer as points.
   *
   */
  DrawWebGL.prototype.drawPoints = function (buf, color, points, pointSize, shape) {
      if (this.isContextLost) {
          return;
      }
  
      this.gl.uniform1f(this.uPointSize, pointSize);
      this.doDraw(this.gl.POINTS, buf, color, points, shape);
  };
  
  /**
   * 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
   */
  DrawWebGL.prototype.drawSquare = function (min, max, color) {
      if (this.isContextLost) {
          return;
      }
  
      this.doDraw(this.gl.TRIANGLE_FAN, new Float32Array(
          min.concat([min[0], max[1]]).concat(max).concat([max[0], min[1]])
      ), color, 4);
  };
  
  DrawWebGL.prototype.drawLimitPoint = function (x, y, size) {
      this.c2d.fillRect(x + size, y, size, size);
      this.c2d.fillRect(x, y + size, size, size);
      this.c2d.fillRect(x - size, y, size, size);
      this.c2d.fillRect(x, y - size, size, size);
  };
  
  DrawWebGL.prototype.drawLimitPoints = function (points, color, pointSize) {
      const limitSize = pointSize * 2;
      const offset = limitSize / 2;
  
      const 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 + ")";
  
      for (let i = 0; i < points.length; i++) {
          this.drawLimitPoint(
              this.x(points[i].x) - offset,
              this.y(points[i].y) - offset,
              limitSize
          );
      }
  };
  
  export default DrawWebGL;
  