<template>
<!-- Patrol Map widget -->
<div class="h-100 w-100 position-relative">
<!-- Video Box -->
<video-box
:show="true"
:video-id="patrolMapId"
class="w-100 h-100 position-absolute"
style="top:0;left:0;" />
<!-- Canvas -->
<canvas
ref="canvas"
class="w-100 h-100 position-absolute"
style="top:0;left:0;z-index:10;"
@mousedown="onMouseDown"
@mousemove="onMouseMove"
@mouseup="onMouseUp"
@mouseout="onMouseOut" />
</div>
</template>
<script>
/**
* Vue SFC used as a widget to set waypoint on a map. This component
* uses the VideoBox component to show the robot map (that is sent from
* the robot in a video feed format) and put a canvas on top of it to
* detect user clicks. When a click is detected the x and y position
* is used to set a waypoint in the array given in props (push). The
* canvas then read the array and draw waypoint on the map where the
* user clicked previously.
* This component have the following dependency :
* VideoBox.vue Component and Bootstrap-Vue for styling.
*
*
* @module widget/PatrolMap
* @vue-prop {Object[]} waypointList - Lists the current waypoints
* @vue-prop {String} patrolMapId - Identifies map source with exact name reference
* @vue-data {Object} videoElement - Contains reference to video-id
* @vue-data {Object} canvas - Contains reference to responsive overlay of map
* @vue-data {Object} context - Sets canvas context
* @vue-data {Number} CanvasRefreshRate - Sets the constant refresh rate of the displayed canvas
* @vue-data {Object} loopIntervalId - Contains refresh rate and display parameter of the map
* @vue-data {Boolean} enable - Enables or disables the display of the map and canvas
*/
/* Disabled comment documentation
* Might use those eventually by forking jsdoc-vue-js so it can manage the author
* and version tag correctly
* @author Valerie Gauthier <valerie.gauthier@usherbrooke.ca>
* @author Edouard Legare <edouard.legare@usherbrooke.ca>
* @version 1.0.0
*/
import VideoBox from './VideoBox';
export default {
name: 'patrol-map',
components: {
VideoBox,
},
props: {
waypointList: {
type: Array,
default: () => [],
required: true,
},
patrolMapId: {
type: String,
required: true,
},
},
data() {
return {
videoElement: null,
canvas: null,
context: null,
CanvasRefreshRate: 60.0, // Hz
isMouseDown: false,
loopIntervalId: null,
enable: true,
};
},
/**
* Lifecycle Hook - mounted.
* On component mounted, Get html elements and initialize.
* @method
* @listens mount(el)
*/
mounted() {
this.videoElement = document.getElementById(this.patrolMapId);
this.canvas = this.$refs.canvas;
this.context = this.canvas.getContext('2d');
this.init();
},
/**
* Lifecycle Hook - destroyed.
* On component destroyed, clear refresh rate of canvas.
* @method
* @listens destroyed(el)
*/
destroyed() {
clearInterval(this.loopIntervalId);
},
methods: {
/**
* Initialisation of canvas refrash rate and call to canvas resizing functions.
* @method
*/
init() {
this.loopIntervalId = setInterval(() => {
if (this.enable) {
this.adjustCanvasToVideo();
this.drawCanvas();
}
}, 1000 / this.CanvasRefreshRate);
},
/**
* Clears canvas and redraws the waypoints of the current patrol.
* @method
*/
drawCanvas() {
this.context.clearRect(0, 0, this.canvas.width, this.canvas.height);
this.drawWaypointList();
},
/**
* Calls for the drawing the waypoints with corresponding arrows (indicating the yaws) of
* each waypoint of the current waypoint list.
* @method
*/
drawWaypointList() {
for (const [index, wp] of this.waypointList.entries()) {
this.drawWaypoint(wp, index);
this.drawYawArrow(wp);
}
},
/**
* Draws a waypoint on the canvas.
* @method
* @param {Object} wp - Waypoint
* @param {Number} wp.x - Waypoint's x coordinate in pixel
* @param {Number} wp.y - Waypoint's y coordinate in pixel
* @param {Number} index - Index number of sent waypoint
*/
drawWaypoint(wp, index) {
const wpColor = '#00FF00';
const coord = this.getCanvasCoordinatesFromVideo(wp.x, wp.y);
const wpRadius = 7;
this.context.beginPath();
this.context.arc(coord.x, coord.y, wpRadius, 0, 2 * Math.PI);
this.context.fillStyle = wpColor;
this.context.fill();
this.context.font = '20px serif';
this.context.fillStyle = '#000000';
this.context.fillText(index + 1, coord.x + 8, coord.y + 8, 25);
},
/**
* Draws arrow for the yaw of the waypoint on the canvas.
* @method
* @param {Object} wp - Waypoint
* @param {Number} wp.x - Waypoint's x coordinate in pixel
* @param {Number} wp.y - Waypoint's y coordinate in pixel
* @param {Number} wp.yaw - Waypoint's yaw angle in radians
*/
drawYawArrow(wp) {
const arrowColor = '#00FF00';
const coord = this.getCanvasCoordinatesFromVideo(wp.x, wp.y);
const arrowLength = Math.min(this.canvas.width, this.canvas.height) / 15;
const headLength = arrowLength / 4;
const radYaw = -wp.yaw / 180 * Math.PI;
const arrowEnd = {
x: coord.x + arrowLength * Math.cos(radYaw),
y: coord.y + arrowLength * Math.sin(radYaw),
};
const arrowTip1 = {
x: arrowEnd.x - headLength * Math.cos(radYaw - Math.PI / 4),
y: arrowEnd.y - headLength * Math.sin(radYaw - Math.PI / 4),
};
const arrowTip2 = {
x: arrowEnd.x - headLength * Math.cos(radYaw + Math.PI / 4),
y: arrowEnd.y - headLength * Math.sin(radYaw + Math.PI / 4),
};
this.context.lineCap = 'round';
this.context.lineWidth = Math.max(1, arrowLength / 10);
this.context.strokeStyle = arrowColor;
this.context.beginPath();
this.context.moveTo(coord.x, coord.y);
this.context.lineTo(arrowEnd.x, arrowEnd.y);
this.context.stroke();
this.context.beginPath();
this.context.moveTo(arrowEnd.x, arrowEnd.y);
this.context.lineTo(arrowTip1.x, arrowTip1.y);
this.context.stroke();
this.context.beginPath();
this.context.moveTo(arrowEnd.x, arrowEnd.y);
this.context.lineTo(arrowTip2.x, arrowTip2.y);
this.context.stroke();
},
/**
* Get position/coordinate of mouse event on video.
* @method
* @param {HTMLElement} - Event given by the click.
* @returns {Object} X and Y coordinate in pixels of event (mouse position).
*/
getVideoCoordinatesOfEvent(event) {
const offsetAndScale = this.getVideoOffsetAndScale();
const rect = this.videoElement.getBoundingClientRect();
const x = (event.clientX - rect.left - offsetAndScale.offsetX) / offsetAndScale.scale;
const y = (event.clientY - rect.top - offsetAndScale.offsetY) / offsetAndScale.scale;
return {
x,
y,
};
},
/**
* Sets canvas size (height and width) to match the size of the video.
* @method
*/
adjustCanvasToVideo() {
this.canvas.width = this.videoElement.offsetWidth;
this.canvas.height = this.videoElement.offsetHeight;
},
/**
* Compute the offset and rescaling parameters of resized video from original content.
* @method
* @returns {Object} Offset in X, offset in Y and scaling ratios.
*/
getVideoOffsetAndScale() {
const videoRatio = this.videoElement.videoWidth / this.videoElement.videoHeight;
let offsetX = 0;
let offsetY = 0;
let scale = 1;
if ((this.videoElement.offsetHeight * videoRatio) > this.videoElement.offsetWidth) {
scale = this.videoElement.osffsetWidth / this.videoElement.videoWidth;
offsetY = (this.videoElement.offsetHeight - (this.videoElement.videoHeight * scale)) / 2;
} else {
scale = this.videoElement.offsetHeight / this.videoElement.videoHeight;
offsetX = (this.videoElement.offsetWidth - (this.videoElement.videoWidth * scale)) / 2;
}
return {
offsetX,
offsetY,
scale,
};
},
/**
* Corrects the waypoint coordinate (x,y) from the offsets and scale parameters of video.
* @method
* @param {Number} x - Horizontal coordinate on canvas.
* @param {Number} y - Vertical coordinate on canvas.
* @returns {Object} - X and Y coordinate in pixels of event (mouse position).
*/
getCanvasCoordinatesFromVideo(x, y) {
const offsetAndScale = this.getVideoOffsetAndScale();
return {
x: (x * offsetAndScale.scale) + offsetAndScale.offsetX,
y: (y * offsetAndScale.scale) + offsetAndScale.offsetY,
};
},
/**
* CallBack of mouse down event. Verify validity and initialise waypoint creation.
* @method
* @param {HTMLElement} event - Event element given by the click.
*/
onMouseDown(event) {
if (event.button === 0) {
const coord = this.getVideoCoordinatesOfEvent(event);
if (this.isClickValid(coord)) {
const wp = coord;
wp.yaw = 0;
this.addWaypointCoord(wp);
}
}
this.isMouseDown = true;
},
/**
* Callback of mouse move event. Updates the yaw with mouse position.
* @method
* @param {HTMLElement} event - Event given by mouse move.
*/
onMouseMove(event) {
if (this.isMouseDown) {
console.log('MouseMoved');
const wp = this.waypointList[this.waypointList.length - 1];
const mousePosition = this.getVideoCoordinatesFromEvent(event);
wp.yaw = -Math.atan2(mousePosition.y - wp.y, mousePosition.x - wp.x) * 180 / Math.PI;
this.updateWaypoint(wp);
}
},
/**
* Callback of mouse up event, finalize the waypoint creation process.
* @method
* @param {HTMLElement} event - Event element given by the click.
*/
onMouseUp(event) {
if (this.isMouseDown) {
// Write waypoint to list of waypoints
console.log('MouseUP');
const date = new Date();
const wp = this.waypointList[this.waypointList.length - 1];
const coord = this.getVideoCoordinatesFromEvent(event);
wp.yaw = -Math.atan2(coord.y - wp.y, coord.x - wp.x) * 180 / Math.PI;
wp.dateTime = date.getTime();
this.updateWaypoint(wp);
this.isMouseDown = false;
}
},
/**
* Callback of mouse out event, terminate the waypoint creation.
* @method
* @param {HTMLElement} event - Event element given by the click.
*/
onMouseOut(event) {
if (this.isMouseDown) {
console.log('MouseOut');
this.waypointList.pop();
this.isMouseDown = false;
}
},
/**
* Adds a waypoint to the list of current waypoints.
* @method
* @param {Object} wp - Waypoint.
*/
addWaypointCoord(wp) {
this.waypointList.push(wp);
},
/**
* Updates last waypoint of the waypoint list.
* @method
* @param {Object} wp - Waypoint.
*/
updateWaypoint(wp) {
this.waypointList.pop();
this.waypointList.push(wp);
},
/**
* Check to see if element is in bound.
* @method
* @param {Object} coord - Coordinates of an element.
* @param {Number} coord.x - X coordinate of element.
* @param {Number} coord.y - Y coordinate of element.
* @returns {boolean} true if coordinates of the element are in bounds of the video.
*/
isClickValid(coord) {
return coord.x >= 0
&& coord.x < this.videoElement.videoWidth
&& coord.y >= 0
&& coord.y < this.videoElement.videoHeight;
},
},
};
</script>
<style>
</style>