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