Source: components/userComponents/monitoredArea/Annotation/zones/ZoneCanvas.js

import React from 'react';
import * as ReactDOM from 'react-dom';
import { withNamespaces } from 'react-i18next';

/**
 * Show zones on canvas
 * @extends Component
 * @hideconstructor
 */
class ZoneCanvas extends React.Component {

  /**
   * @constructs
   * @param props
   */
  constructor(props) {
    super(props);
    this.state = {
      activeUrl: this.props.loadUrl,
      loading: true,
      spinnerInterval: null,
      loaded: false,
      actualZone: []
    };
    this.actualizeScreenshot = this.actualizeScreenshot.bind(this);
    this.onCanvasClick = this.onCanvasClick.bind(this);
    this.clearActualZone = this.clearActualZone.bind(this);
  }

  /**
   *
   */
  actualizeScreenshot() {
    if (this.state.loading === true) return;
    let context = this.state.canvas.getContext("2d");
    context.clearRect(0, 0, this.state.canvas.width, this.state.canvas.height);

    this.setState({
      loading: true,
      loaded: false,
      actualZone: [],
      activeUrl: this.props.refreshUrl + "&date=" + new Date()
    });
    this.showSpinner();
  }

  /**
   *
   */
  componentDidMount() {
    let canvas = ReactDOM.findDOMNode(this.refs.drawCanvas);
    let image = ReactDOM.findDOMNode(this.refs.image);
    let context = canvas.getContext("2d");
    this.setState({
      image: image,
      canvas: canvas
    });

    if (this.state.loading === false) context.drawImage(image, 0, 0, canvas.width, canvas.height);
  }

  /**
   *
   * @param prevProps
   * @param prevState
   * @param snapshot
   */
  componentDidUpdate(prevProps, prevState, snapshot) {
    let canvas = this.state.canvas;
    let image = this.state.image;
    let context = canvas.getContext("2d");
    if (this.state.loading === true || this.state.loaded === false) {
      return;
    }

    context.drawImage(image, 0, 0, canvas.width, canvas.height);
    context.font = "24px Arial";
    context.textAlign = "center";

    if (this.props.zones.length > 0) {
      this.props.zones.map(zone => {
        if (zone.cameraVertices === undefined || zone.cameraVertices.length === 0) return;
        context.strokeStyle = 'rgb(255, 134, 20)';
        context.fillStyle = 'rgba(255, 152, 56, 0.5)';
        context.lineWidth = 3;
        let start = this.displayToCanvas(this.imageToDisplay(zone.cameraVertices[0]));
        let sumX = start.x;
        let sumY = start.y;
        context.beginPath();
        context.moveTo(start.x, start.y);

        for (let i = 1; i < zone.cameraVertices.length; ++i) {
          let coord = this.displayToCanvas(this.imageToDisplay(zone.cameraVertices[i]));
          sumX += coord.x;
          sumY += coord.y;
          context.lineTo(coord.x, coord.y);
        }
        context.closePath();
        context.fill();
        context.stroke();

        context.fillStyle = 'rgba(255, 255, 255, 1)';
        if (this.props.showZoneNames === undefined || this.props.showZoneNames === true) {
          context.fillText(zone.zoneNumber.toString(),
            sumX / zone.cameraVertices.length,
            sumY / zone.cameraVertices.length);
        }
      });
    }

    let actualZoneVertices = this.state.actualZone;
    context.strokeStyle = 'rgb(0, 255, 0)';
    context.fillStyle = 'rgba(0, 255, 0, 0.5)';
    if (actualZoneVertices.length === 1) {
      let start = this.displayToCanvas(this.imageToDisplay(actualZoneVertices[0]));
      context.beginPath();
      context.arc(start.x, start.y, 2, 0, 2 * Math.PI);
      context.fill();
    } else if (actualZoneVertices.length > 1) {
      let start = this.displayToCanvas(this.imageToDisplay(actualZoneVertices[0]));
      context.beginPath();
      context.moveTo(start.x, start.y);

      for (let i = 1; i < actualZoneVertices.length; ++i) {
        let coords = this.displayToCanvas(this.imageToDisplay(actualZoneVertices[i]));
        context.lineTo(coords.x, coords.y);
      }
      context.closePath();
      context.fill();
      context.stroke();
    }
  }

  onCanvasClick(event) {
    if (this.props.interactive !== undefined && this.props.interactive === false) return;
    let actualZoneVertices = this.state.actualZone.slice();
    actualZoneVertices.push(
      this.displayToImage({ x: event.nativeEvent.offsetX, y: event.nativeEvent.offsetY })
    );
    this.setState({
      actualZone: actualZoneVertices
    });
    this.props.onActualZoneChange(actualZoneVertices);
  }

  /**
   *
   */
  onImageLoaded() {
    window.clearInterval(this.state.spinnerInterval);

    let canvas = ReactDOM.findDOMNode(this.refs.drawCanvas);
    let image = ReactDOM.findDOMNode(this.refs.image);
    this.setState({
      loading: false,
      loaded: true,
      image: image,
      canvas: canvas
    });
  }

  /**
   *
   */
  onImageError() {
    this.setState({
      loading: false,
      loaded: false
    })
  }

  /**
   *
   */
  clearActualZone() {
    this.setState({
      actualZone: []
    });
    this.props.onActualZoneChange([]);
  }

  /**
   *
   * @returns {*}
   */
  render() {
    const { t } = this.props;
    return (
      <div>
        <div className="box-header no-border">
          <div className="pull-right spacing">
            <button
              type="submit"
              className="btn btn-primary"
              onClick={this.actualizeScreenshot}
              disabled={this.state.loading === true}
            >{t('forms.newScreenshot')}</button>
          </div>
        </div>
        <div className="box-body">
          <canvas
            ref="drawCanvas"
            id="drawCanvas"
            onClick={event => this.onCanvasClick(event)}
            height={600}
            width={700}
            style={{
              position: 'relative',
            }}
          />
          <img
            id="image"
            ref="image"
            src={this.state.activeUrl}
            alt="monitored area view"
            onLoad={() => this.onImageLoaded()}
            onError={() => this.onImageError()}
            className="hide"
            style={{
              position: 'relative',
            }}
          />
        </div>
        <label className="control-label fa fa-info-circle info-icon pull-right"
               data-toggle="tooltip"
               title={t('forms.tooltip.canvas')}/>
        {
          (this.props.interactive === undefined || this.props.interactive === true) ?
            <div className="box-footer no-border">
            <button
              type="submit"
              className="btn btn-primary pull-left"
              onClick={this.clearActualZone}
            >{t('forms.erase')}</button>
            </div>
            : null
        }
      </div>
    )
  }

  /**
   * converts from displayed canvas coordinates to image coordinates
   * @param coords
   * @returns {{x: number, y: number}}
   */
  displayToImage(coords) {
    const widthRatio = this.state.image.width / this.state.canvas.clientWidth;
    const heightRatio = this.state.image.height / this.state.canvas.clientHeight;

    return { x: Math.round(coords.x * widthRatio), y: Math.round(coords.y * heightRatio) };
  }

  /**
   * converts from image coordinates to displayed canvas coordinates
   * @param coords
   * @returns {{x: number, y: number}}
   */
  imageToDisplay(coords) {
    const widthRatio = this.state.image.width / this.state.canvas.clientWidth;
    const heightRatio = this.state.image.height / this.state.canvas.clientHeight;

    return { x: Math.round(coords.x / widthRatio), y: Math.round(coords.y / heightRatio) };
  }

  /**
   * converts from displayed canvas coordinates to logical canvas coordinates
   * @param coords
   * @returns {{x: number, y: number}}
   */
  displayToCanvas(coords) {
    const widthRatio = this.state.canvas.width / this.state.canvas.clientWidth;
    const heightRatio = this.state.canvas.height / this.state.canvas.clientHeight;

    return { x: coords.x * widthRatio, y: coords.y * heightRatio };
  }

  /** code source: https://codepen.io/reneras/pen/HFrmC
   * edited size of spinner */
  showSpinner() {
    const canvas = ReactDOM.findDOMNode(this.refs.drawCanvas);
    const context = canvas.getContext('2d');
    const start = new Date();
    const lines = 16,
      cW = context.canvas.width,
      cH = context.canvas.height;

    let draw = function () {
      let rotation = parseInt(((new Date() - start) / 1000) * lines) / lines;
      context.save();
      context.clearRect(0, 0, cW, cH);
      context.translate(cW / 2, cH / 2);
      // context.translate(cW / 2, cH / 2);
      context.rotate(Math.PI * 2 * rotation);
      for (let i = 0; i < lines; i++) {
        context.beginPath();
        context.rotate(Math.PI * 2 / lines);
        // context.moveTo(cW / 10, 0);
        // context.lineTo(cW / 4, 0);
        // context.lineWidth = cW / 30;
        context.moveTo(cW / 30, 0);
        context.lineTo(cW / 12, 0);
        context.lineWidth = cW / 90;
        context.strokeStyle = "rgba(0, 0, 0," + i / lines + ")";
        context.stroke();
      }
      context.restore();
    };
    this.state.spinnerInterval = window.setInterval(draw, 1000 / 30);
  }

}

export default withNamespaces()(ZoneCanvas);