Source: components/userComponents/monitoredArea/statisticsTabs/RealviewOfTransits.js

import React from 'react';
import { Checkbox } from 'react-ui-icheck';
import { withNamespaces } from 'react-i18next';
import Konva from 'konva';
import { Stage, Layer, Image } from 'react-konva';
import { API } from '../../../../LocalConfiguration';
import { DataSet, Network } from 'vis/dist/vis-network.min'
import * as simpleheat from 'simpleheat';
import { Range } from 'rc-slider';
import Tooltip from 'rc-tooltip';
import Handle from 'rc-slider/es/Handle';
import {
  fetchAggregatedMovements,
  fetchAggregatedMovementsCount,
  fetchAggregatedTrajectories,
  fetchMovements,
  fetchMovementsCount
} from '../../../restmodules/MovementsRestModule';
import { createColorsConfig, getColor } from '../../../../Utils';
import PassDetail from '../detail/PassDetail';

/**
 * Statistics Camera view of transits Tab
 * @extends Component
 * @hideconstructor
 */
class RealviewOfTransits extends React.Component {
  /**
   * @constructs
   * @param props
   */
  constructor(props) {
    super(props);
    this.state = {
      // image - real view of camera
      image: null,
      // drawing mode controls
      showTransitions: true,
      showTrajectories: false,
      showHeatMap: false,
      aggregateMovements: false,
      // heat map values
      sliderRange: [1, 2],
      sliderMin: 0,
      sliderMax: 10,
      sliderStep: 1,
      showDataOverloadWarning: false,
      missingMinMaxVelocity: false,
      numberOfTrajectoryPoints: 0,
      trajectoryPoints: [],
      aggregatedPoints: [],
      aggregatedTrajectories: [],

      selectedPass: {
        name: '',
        count: 0,
        tableOfTransits: []
      }
    };

    this.zonesLayer = React.createRef();
    this.canvasWidth = 700;
    this.canvasHeight = 600;

    this.zonesLayer = React.createRef();
    this.trajectoriesLayer = React.createRef();
    this.heatMapLayer = React.createRef();
    this.visLayerOne = React.createRef();

    this.transitNetwork = undefined;
    this.options = {};
  }
/**
 */
  componentDidMount() {
    this.loadImage();
    this.options = {
      width: this.canvasWidth + 'px',
      height: this.canvasHeight + 'px',
      manipulation: {
        enabled: false
      },
      physics: {
        enabled: false
      },
      interaction: {
        dragNodes: false,// do not allow dragging nodes
        zoomView: false, // do not allow zooming
        dragView: false, // do not allow dragging
        selectable: true,
        hover: true
      },
      nodes: {
        fixed: true,
        color: 'rgba(255,255,255,0)',
        // shape: 'dot',
        // size: 10
      },
      edges: {
        arrowStrikethrough: false,
        width: 5,
        hoverWidth: function (width) {
          return width * 2;
        },
        selectionWidth: function (width) {
          return width * 2;
        },
        arrows: {
          to: {
            enabled: true,
            scaleFactor: 1.5
          }
        },
        smooth: {
          type: 'curvedCW',
          forceDirection: 'none',
          roundness: 0.3
        },
        color: {
          // color: '#1fd329',
          inherit: false,
          opacity: 0.7,
          // highlight: '#167c19'
        }
      }
    };
    let data = {
      nodes: new DataSet([]),
      edges: new DataSet([])
    };
    this.transitNetwork = new Network(this.visLayerOne.current, data, this.options);
    this.transitNetwork.moveTo({
      position: { x: 0, y: 0 },
      // offset: {x: -350, y: -300},
      scale: 1,
    });
  }

  /**
   */
  componentDidUpdate(prevProps, prevState, snapshot) {
    if (prevProps.areaId !== this.props.areaId) {
      this.loadImage();
    }

    if (prevProps.filter !== this.props.filter) {
      this.setState({
        trajectoryPoints: [],
        aggregatedPoints: [],
        aggregatedTrajectories: [],
        numberOfTrajectoryPoints: 0,
        showDataOverloadWarning: false,
        missingMinMaxVelocity: false
      })
    }

    this.state.aggregateMovements = prevProps.aggregateMovements;

    if (prevProps !== this.props) {
      if (this.props.movementsMinMax && this.props.movementsMinMax.min != null && this.props.movementsMinMax.max != null) {
        this.state.sliderRange = [this.props.movementsMinMax.min, this.props.movementsMinMax.max];
        this.state.sliderMin = this.roundToTwoDecimalPlaces(this.props.movementsMinMax.min);
        this.state.sliderMax = this.roundToTwoDecimalPlaces(this.props.movementsMinMax.max);
        this.state.sliderStep = this.roundToTwoDecimalPlaces((this.state.sliderMax - this.state.sliderMin) / 50);
        this.state.missingMinMaxVelocity = false;
        this.handleSliderAfterChange();
      } else {
        this.state.sliderRange = [0, 10];
        this.state.sliderMin = 0;
        this.state.sliderMax = 10;
        this.state.sliderStep = 1;
        this.state.missingMinMaxVelocity = true;
      }
    }

    let nodesArray = [];
    let edgesArray = [];

    if (this.props.aggregatedData && this.props.aggregatedData.zones && this.state.image) {
      this.zonesLayer.current.removeChildren();

      let agregatedCounts = this.props.aggregatedData.zones.map(zone => {
        return this.props.aggregatedData.passes
        .filter(pass => pass.zoneNumbersSequence
        .includes(zone.zoneNumber))
        .reduce((total, pass) => total + pass.count, 0)
      });
      const colorsConfig = createColorsConfig(agregatedCounts);

      this.props.aggregatedData.zones.map(zone => {
        let pts = [];
        let sumX = 0;
        let sumY = 0;
        zone.cameraVertices.map(vertices => {
          let coords = this.displayToImage(vertices);
          pts.push(coords.x);
          pts.push(coords.y);
          sumX += coords.x;
          sumY += coords.y;
        });
        nodesArray.push({
          id: zone.zoneNumber,
          label: '',
          x: sumX / zone.cameraVertices.length,
          y: sumY / zone.cameraVertices.length
        });
        let agregatedCount = this.props.aggregatedData.passes
        .filter(pass => pass.zoneNumbersSequence
          .includes(zone.zoneNumber))
          .reduce((total, pass) => total + pass.count, 0);
        let color = getColor(agregatedCount, colorsConfig);
        // console.log(color);
        let polygon = new Konva.Line({
          points: pts,
          closed: true,
          fill: color,
          // fill: 'rgba(255, 152, 56, 0.5)',
          stroke: color,
          // stroke: '#ff8614',
          strokeWidth: 3
        });
        this.zonesLayer.current.add(polygon);
      });
      this.zonesLayer.current.draw();

      if (this.state.showTransitions === true && this.props.aggregatedData.passes && this.props.aggregatedData.passes.length > 0 && this.state.image) {
        let index = 0;
        this.props.aggregatedData.passes.map(transit => {
          edgesArray.push({
            from: transit.zoneNumbersSequence[0],
            to: transit.zoneNumbersSequence[1],
            label: String(transit.count),
            id: index++
          });
        });
        edgesArray.sort((a, b) => {
          return Number(a.label) - Number(b.label)
        });

        const colorsConfig = createColorsConfig(this.props.aggregatedData.passes.map(pass => pass.count));
        for (let i = 0; i < edgesArray.length; i++) {
          edgesArray[i].color = {
            color: getColor(Number(edgesArray[i].label), colorsConfig),
            hover: getColor(Number(edgesArray[i].label), colorsConfig),
            highlight: getColor(Number(edgesArray[i].label), colorsConfig)
          };
          edgesArray[i].width = 10;
        }

        let data = {
          nodes: new DataSet(nodesArray),
          edges: new DataSet(edgesArray)
        };
        this.transitNetwork.destroy();
        this.transitNetwork = new Network(this.visLayerOne.current, data, this.options);
        this.transitNetwork.on('selectEdge', (event) => {
          if (event.edges.length === 1) {
            let selectedPass = {
              name: this.props.aggregatedData.passes[event.edges[0]].name,
              count: this.props.aggregatedData.passes[event.edges[0]].count,
              tableOfTransits: this.props.aggregatedData.passes[event.edges[0]].transits
            };
            this.setState({
              selectedPass: selectedPass
            });
          }
        });
        this.transitNetwork.moveTo({
          position: { x: this.canvasWidth / 2, y: this.canvasHeight / 2 },
          scale: 1,
        });
      } else {
        this.transitNetwork.destroy();
      }
    }

    if (this.state.showTrajectories && !this.state.aggregateMovements && this.state.trajectoryPoints) {
      this.trajectoriesLayer.current.clear();
      let context = this.trajectoriesLayer.current.getContext("2d");
      context.clearRect(0, 0, context.canvas.width, context.canvas.height);
      context.strokeStyle = 'rgba(26,83,255,0.1)';
      context.lineWidth = 5;
      this.state.trajectoryPoints.map(trajectory => {
        if (trajectory.length > 1) {
          let start = this.displayToImage(trajectory[0]);
          context.beginPath();
          context.moveTo(start.x, start.y);
          for (let i = 1; i < trajectory.length; i++) {
            let coords = this.displayToImage(trajectory[i].position);
            context.lineTo(coords.x, coords.y);
          }
          context.closePath();
          context.stroke();
        }
      })
    } else if (this.state.showTrajectories && this.state.aggregateMovements && this.state.aggregatedPoints) {
      this.trajectoriesLayer.current.clear();
      let context = this.trajectoriesLayer.current.getContext("2d");
      context.clearRect(0, 0, context.canvas.width, context.canvas.height);
      context.lineWidth = 5;
      this.state.aggregatedTrajectories.map(trajectory => {
        if (trajectory.points.length > 1) {
          let start = this.displayToImage(trajectory.points[0]);
          context.strokeStyle = 'rgba(26,83,255,' + (1 - 0.95 ** trajectory.weight) + ')';
          context.beginPath();
          context.moveTo(start.x, start.y);
          for (let i = 1; i < trajectory.points.length; i++) {
            let coords = this.displayToImage(trajectory.points[i]);
            context.lineTo(coords.x, coords.y);
          }
          context.closePath();
          context.stroke();
        }
      })
    } else {
      this.trajectoriesLayer.current.clear();
    }

    const heatRadius = 10000 / this.state.image.width;
    if (this.state.showHeatMap && !this.state.aggregateMovements && this.state.trajectoryPoints) {
      this.heatMapLayer.current.clear();
      let points = [];
      this.state.trajectoryPoints.map(trajectory =>
        trajectory.map(point => {
          let coords = this.displayToImage(point.position);
          points.push([coords.x, coords.y, 0.1])
        })
      );
      let heat = simpleheat(this.heatMapLayer.current.getCanvas());
      heat.radius(4, 2);
      heat.data(points);
      heat.draw();
    } else if (this.state.showHeatMap && this.state.aggregateMovements && this.state.aggregatedPoints) {
      this.heatMapLayer.current.clear();
      let points = [];
      this.state.aggregatedPoints.map(point => {
            let coords = this.displayToImage(point.position);
            points.push([coords.x, coords.y, 1 - 0.9 ** point.weight])
      });
      let heat = simpleheat(this.heatMapLayer.current.getCanvas());
      heat.radius(heatRadius, heatRadius / 1.5);
      heat.data(points);
      heat.draw();
    } else {
      //  clear heat map layer
      // let context = this.heatMapLayer.current.getContext("2d");
      // context.clearRect(0, 0, context.canvas.width, context.canvas.height);
      this.heatMapLayer.current.clear();
    }
  }

  /**
   */
  componentWillUnmount() {
    this.image.removeEventListener('load', this.handleLoad);
  }

  /**
   */
  handleShowTrajectoriesChange = (event) => {
    this.setState({
      showTrajectories: event.target.checked
    });
  };
  /**
   */
  handleShowTransitionsChange = (event) => {
    this.setState({
      showTransitions: event.target.checked
    });
  };
  /**
   */
  handleShowHeatMapChange = (event) => {
    this.setState({
      showHeatMap: event.target.checked
    });
  };
  /**
   */
  loadImage() {
    this.image = new window.Image();
    if (this.props.areaId) {
      // save to "this" to remove "load" handler on unmount
      this.image.src = API + "/screenshot/" + this.props.areaId;
      this.image.addEventListener('load', this.handleLoad);
    }
  }
  /**
   */
  handleLoad = () => {
    // after setState react-konva will update canvas and redraw the layer
    // because "image" property is changed
    this.setState({
      image: this.image
    });
    // if you keep same image object during source updates
    // you will have to update layer manually:
    // this.imageNode.getLayer().batchDraw();
  };
  /**
   */
  handleSliderOnChange = (value) => {
    this.setState({
      sliderRange: value
    });
  };
  /**
   */
  handleSliderAfterChange = () => {
    const countFun = this.state.aggregateMovements
        ? fetchAggregatedMovementsCount
        : fetchMovementsCount;

    countFun(this.props.filter, this.state.sliderRange[0], this.state.sliderRange[1])
    .then(count => {
      this.setState({
        numberOfTrajectoryPoints: count,
        showDataOverloadWarning: this.state.aggregateMovements
      });
    });
  };
  /**
   */
  loadTrajectoryPoints = () => {
    if (this.state.aggregateMovements) {
      this.state.trajectoryPoints = [];

      fetchAggregatedMovements(this.props.filter, this.state.sliderRange[0], this.state.sliderRange[1])
      .then(data => this.setState({
        aggregatedPoints: data
      }));

      fetchAggregatedTrajectories(this.props.filter, this.state.sliderRange[0], this.state.sliderRange[1])
      .then(data => this.setState({
        aggregatedTrajectories: data
      }));
    }
    else {
      this.state.aggregatedPoints = [];
      this.state.aggregatedTrajectories = [];

      fetchMovements(this.props.filter, this.state.sliderRange[0],
          this.state.sliderRange[1])
      .then(data => this.setState({
        trajectoryPoints: data
      }))
    }
  };
  /**
   */
  render() {
    const { t } = this.props;

    const handle = (props) => {
      const { value, dragging, index, ...restProps } = props;
      return (
        <Tooltip
          prefixCls="rc-slider-tooltip"
          overlay={value}
          visible={dragging}
          placement="top"
          key={index}
        >
          <Handle value={value} {...restProps} />
        </Tooltip>
      );
    };

    return (
      <div>
        <div className="box">

          <div className="box-header">
            <div className="col-md-6">
              <h3 className="box-title text-right">{t('monitoredArea.cameraViewOfTransits')}</h3>
            </div>
          </div>

          <div className="box-body col-xs-12">
            <div className="input-group col-xs-12">

              <div className="col-xs-12 col-md-6 col-lg-4" data-toggle="tooltip"
                   title={t('realView.showTrajectoriesTooltip')}>
                <Checkbox
                  checkboxClass="icheckbox_square-blue"
                  name={'checkedShowTrajectories'}
                  checked={false}
                  label={t('realView.showTrajectories')}
                  onChange={this.handleShowTrajectoriesChange}
                />
              </div>

              <div className="col-xs-12 col-md-6 col-lg-4" data-toggle="tooltip"
                   title={t('realView.showTransitionsTooltip')}>
                <Checkbox
                  checkboxClass="icheckbox_square-blue"
                  name={'checkedShowTransits'}
                  checked={true}
                  label={t('realView.showTransitions')}
                  onChange={this.handleShowTransitionsChange}
                />
              </div>

              <div className="col-xs-12 col-md-6 col-lg-4" data-toggle="tooltip"
                   title={t('realView.showHeatMapTooltip')}>
                <Checkbox
                  checkboxClass="icheckbox_square-blue"
                  name={'checkedShowHeatMap'}
                  checked={false}
                  label={t('realView.showHeatMap')}
                  onChange={this.handleShowHeatMapChange}
                />
              </div>
            </div>

            <div hidden={!this.state.showTrajectories && !this.state.showHeatMap}>
              <div className="row">
                <div className="col-xs-10 col-xs-offset-1">
                  <h5>{t('realView.velocityFilter')}</h5>
                </div>
              </div>
              <div className="row container-center">
                <div className="col-xs-1">
                  <span className="pull-right">{t('realView.lowVelocity') + ' (' + this.state.sliderMin + ')'}
                    <label className="control-label fa fa-info-circle info-icon"
                           data-toggle="tooltip"
                           title={t('realView.minSliderTooltip')}/>
                  </span>
                </div>
                <div className="col-xs-8">
                  <Range
                    min={this.state.sliderMin}
                    max={this.state.sliderMax}
                    step={this.state.sliderStep}
                    onChange={this.handleSliderOnChange}
                    onAfterChange={this.handleSliderAfterChange}
                    handle={handle}
                    defaultValue={[0, 10]}
                    value={this.state.sliderRange}
                    pushable={true}
                    disabled={this.state.missingMinMaxVelocity}
                  />
                </div>
                <div className="col-xs-1">
                      <span>{t('realView.highVelocity') + ' (' + this.state.sliderMax + ')'}
                        <label className="control-label fa fa-info-circle info-icon"
                               data-toggle="tooltip"
                               title={t('realView.maxSliderTooltip')}/>
                      </span>
                </div>
              </div>
              <div className="row center-content-vertically row-height-margin">
                <div className="warning col-xs-offset-1 col-xs-11"
                     hidden={!this.state.missingMinMaxVelocity}>
                <span>
                  <i className="fa fa-exclamation-triangle"/>
                  {t('realView.missingMinMaxWarning')}
                </span>
                </div>
              </div>
              <div className="row center-content-vertically row-height-margin">
                <div className="warning2 col-xs-offset-1 col-xs-11"
                     hidden={!this.state.showDataOverloadWarning}>
                    <span>
                      <i className="fa fa-exclamation-triangle"/>
                      {t('realView.showDataOverloadWarning')}
                    </span>
                </div>
              </div>
              <div className="row" hidden={this.state.missingMinMaxVelocity}>
                <div className="center-content-vertically">
                  <div className="col-xs-offset-1 col-xs-6">
                    <span>{t('realView.numberOfTrajectoryPoints') + this.state.numberOfTrajectoryPoints}</span>
                  </div>
                  <div className="col-xs-4"
                       hidden={this.props.areaId === undefined}>
                    <button
                      className={"btn btn-primary pull-right"}
                      onClick={this.loadTrajectoryPoints}
                    >
                      {t('realView.visualize')}
                    </button>
                  </div>
                </div>
              </div>
            </div>
            <div className="col-xs-12">
              <div>
                <div className="box-body">
                  <div className="col-xs-12">
                    <div style={{ position: 'relative', width: 700, height: 620, margin: 'auto' }}>
                      <Stage
                        style={{
                          position: 'absolute',
                          top: 0,
                          left: 0,
                          width: 700,
                          height: 600,
                          zIndex: 0
                        }} lineWidth
                        width={this.canvasWidth}
                        height={this.canvasHeight}
                      >
                        <Layer>
                          <Image
                            image={this.state.image}
                            width={700}
                            height={600}
                          />
                        </Layer>
                        <Layer ref={this.zonesLayer}/>
                        <Layer ref={this.trajectoriesLayer}/>
                        <Layer ref={this.heatMapLayer}/>
                      </Stage>
                      <div style={{ position: 'absolute', top: 0, left: 0, zIndex: 1 }}
                           ref={this.visLayerOne}>
                      </div>
                      {/*<div style={{ position: 'absolute', top: 0, left: 0 }} ref={this.visLayerTwo}/>*/}
                    </div>
                  </div>

                  <div className="col-xs-12" hidden={!this.state.showTransitions}>
                    <PassDetail data={this.state.selectedPass}/>
                  </div>

                </div>
                <div className="box-footer">
                </div>
              </div>
            </div>

          </div>
          <div className="box-footer">
          </div>
        </div>
      </div>
    );
  }
  /**
   */
  roundToTwoDecimalPlaces = (number) => {
    return Math.round(number * 100) / 100;
  };
  /**
   */
  roundToOneDecimalPlace = (number) => {
    return Math.round(number * 10) / 10;
  };

  /**
   * converts from displayed canvas coordinates to image coordinates
   */
  displayToImage(coords) {
    const widthRatio = this.state.image.width / this.canvasWidth;
    const heightRatio = this.state.image.height / this.canvasHeight;

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

export default withNamespaces()(RealviewOfTransits);