Rates Graph Update

Prevent the balloon labels from overlapping,
Add dynamic stick position indicators to rates graph
Add minimum font size to text (for low DPI monitors).
Extend the length of the pointer on the balloons
Multi-Layer Canvas'
Add window resize triggers
Add current stick position values
Remove 360deg axes lines and code tidy
10.3.x-maintenance
Gary Keeble 2016-09-17 08:03:00 +01:00
parent 5512575051
commit 3a77c7fade
3 changed files with 178 additions and 64 deletions

View File

@ -204,6 +204,8 @@
.tab-pid_tuning .rate_curve {
margin: 0 4px 0px 0;
height: 100%;
min-height: 250px;
min-width: 250px;
border: 1px solid silver;
border-radius: 3px;
background-image: url(../images/paper.jpg);

View File

@ -349,8 +349,9 @@
<tr>
<td>
<div class="spacer" style="margin-top: 10px;">
<div class="rate_curve">
<canvas height="120px" style="width: 100%; height: 100%"></canvas>
<div class="rate_curve" style="position:relative;" >
<canvas height="120px" style="position:absolute; top: 0; left: 0; z-index: 0; height:100%; width:100%;"></canvas>
<canvas height="120px" style="position:absolute; top: 0; left: 0; z-index: 1; height:100%; width:100%;"></canvas>
</div>
</div>
</td>

View File

@ -398,7 +398,7 @@ TABS.pid_tuning.initialize = function (callback) {
}
}
function drawAxes(curveContext, width, height, scaleHeight) {
function drawAxes(curveContext, width, height) {
curveContext.strokeStyle = '#000000';
curveContext.lineWidth = 4;
@ -414,20 +414,27 @@ TABS.pid_tuning.initialize = function (callback) {
curveContext.lineTo(width / 2, height);
curveContext.stroke();
if (scaleHeight <= height / 2) {
curveContext.strokeStyle = '#c0c0c0';
curveContext.lineWidth = 4;
}
curveContext.beginPath();
curveContext.moveTo(0, height / 2 + scaleHeight);
curveContext.lineTo(width, height / 2 + scaleHeight);
curveContext.stroke();
function plotStickPosition(curveContext, rcData, rate, rcRate, rcExpo, superExpoActive, deadband, maxAngularVel, stickColor) {
const DEFAULT_SIZE = 60; // canvas units, relative size of the stick indicator (larger value is smaller indicator)
const rateScaling = (curveContext.canvas.height / 2) / maxAngularVel;
var currentValue = self.rateCurve.rcCommandRawToDegreesPerSecond(rcData, rate, rcRate, rcExpo, superExpoActive, deadband);
if(rcData!=undefined) {
curveContext.save();
curveContext.fillStyle = stickColor || '#000000';
curveContext.translate(curveContext.canvas.width/2, curveContext.canvas.height/2);
curveContext.beginPath();
curveContext.moveTo(0, height / 2 - scaleHeight);
curveContext.lineTo(width, height / 2 - scaleHeight);
curveContext.stroke();
curveContext.arc(rcData-1500, -rateScaling * currentValue, curveContext.canvas.height / DEFAULT_SIZE, 0, 2 * Math.PI);
curveContext.fill();
curveContext.restore();
}
return currentValue.toFixed(0); // The calculated value in deg/s is returned from the function call for further processing.
}
function drawAxisLabel(curveContext, axisLabel, x, y, align, color) {
@ -437,18 +444,19 @@ TABS.pid_tuning.initialize = function (callback) {
curveContext.fillText(axisLabel, x, y);
}
function drawBalloonLabel(curveContext, axisLabel, x, y, align, color, borderColor, textColor) {
function drawBalloonLabel(curveContext, axisLabel, x, y, align, colors, dirty) {
/**
* curveContext is the canvas to draw on
* axisLabel is the string to display in the center of the balloon
* x, y are the coordinates of the point of the balloon
* align is whether the balloon appears to the left (align 'right') or right (align left) of the x,y coordinates
* color, borderColor and textColor are the fill color, border color and text color of the balloon
* colors is an object defining color, border and text are the fill color, border color and text color of the balloon
*/
const DEFAULT_OFFSET = 50; // in canvas scale; this is the horizontal length of the pointer
const DEFAULT_OFFSET = 125; // in canvas scale; this is the horizontal length of the pointer
const DEFAULT_RADIUS = 10; // in canvas scale, this is the radius around the balloon
const DEFAULT_MARGIN = 5; // in canvas scale, this is the margin around the balloon when it overlaps
const fontSize = parseInt(curveContext.font);
@ -458,13 +466,34 @@ TABS.pid_tuning.initialize = function (callback) {
const pointerY = y; // always point to the required Y coordinate, even if we move the balloon itself to keep it on the canvas
// setup balloon background
curveContext.fillStyle = color || '#ffffff' ;
curveContext.strokeStyle = borderColor || '#000000' ;
curveContext.fillStyle = colors.color || '#ffffff' ;
curveContext.strokeStyle = colors.border || '#000000' ;
// correct x position to account for window scaling
x *= curveContext.canvas.clientWidth/curveContext.canvas.clientHeight;
// adjust the coordinates for determine where the balloon background should be drawn
x += ((align=='right')?-(width + DEFAULT_OFFSET):0) + ((align=='left')?DEFAULT_OFFSET:0);
y -= (height/2); if(y<0) y=0; else if(y>curveContext.height) y=curveContext.height; // prevent balloon from going out of canvas
// check that the balloon does not already overlap
for(var i=0; i<dirty.length; i++) {
if((x>=dirty[i].left && x<=dirty[i].right) || (x+width>=dirty[i].left && x+width<=dirty[i].right)) { // does it overlap horizontally
if((y>=dirty[i].top && y<=dirty[i].bottom) || (y+height>=dirty[i].top && y+height<=dirty[i].bottom )) { // this overlaps another balloon
// snap above or snap below
if(y<=(dirty[i].bottom - dirty[i].top) / 2 && (dirty[i].top - height) > 0) {
y = dirty[i].top - height;
} else { // snap down
y = dirty[i].bottom;
}
}
}
}
// Add the draw area to the dirty array
dirty.push({left:x, right:x+width, top:y-DEFAULT_MARGIN, bottom:y+height+DEFAULT_MARGIN});
var pointerLength = (height - 2 * DEFAULT_RADIUS ) / 6;
curveContext.beginPath();
@ -498,7 +527,7 @@ TABS.pid_tuning.initialize = function (callback) {
curveContext.stroke();
// and add the label
drawAxisLabel(curveContext, axisLabel, x + (width/2), y + (height + fontSize)/2 - 4, 'center', textColor);
drawAxisLabel(curveContext, axisLabel, x + (width/2), y + (height + fontSize)/2 - 4, 'center', colors.text);
}
@ -741,53 +770,126 @@ TABS.pid_tuning.initialize = function (callback) {
}
// Getting the DOM elements for curve display
var rcCurveElement = $('.rate_curve canvas').get(0);
var curveContext = rcCurveElement.getContext("2d");
var rcCurveElement = $('.rate_curve canvas').get(0),
rcStickElement = $('.rate_curve canvas').get(1),
curveContext = rcCurveElement.getContext("2d"),
stickContext = rcStickElement.getContext("2d"),
maxAngularVelRollElement = $('.pid_tuning .maxAngularVelRoll'),
maxAngularVelPitchElement = $('.pid_tuning .maxAngularVelPitch'),
maxAngularVelYawElement = $('.pid_tuning .maxAngularVelYaw'),
updateNeeded = true,
maxAngularVel;
rcCurveElement.width = 1000;
rcCurveElement.height = 1000;
rcStickElement.width = 1000;
rcStickElement.height = 1000;
var maxAngularVelRollElement = $('.pid_tuning .maxAngularVelRoll');
var maxAngularVelPitchElement = $('.pid_tuning .maxAngularVelPitch');
var maxAngularVelYawElement = $('.pid_tuning .maxAngularVelYaw');
self.updateRatesLabels = function() {
if (!useLegacyCurve && maxAngularVel) {
stickContext.save();
var updateNeeded = true;
const BALLOON_COLORS = {
roll : {color: 'rgba(255,128,128,0.4)', border: 'rgba(255,128,128,0.6)', text: '#000000'},
pitch : {color: 'rgba(128,255,128,0.4)', border: 'rgba(128,255,128,0.6)', text: '#000000'},
yaw : {color: 'rgba(128,128,255,0.4)', border: 'rgba(128,128,255,0.6)', text: '#000000'}
};
function updateRates(event) {
var maxAngularVelRoll = maxAngularVelRollElement.text() + ' deg/s',
maxAngularVelPitch = maxAngularVelPitchElement.text() + ' deg/s',
maxAngularVelYaw = maxAngularVelYawElement.text() + ' deg/s',
currentValues = [],
balloonsDirty = [],
curveHeight = rcStickElement.height,
curveWidth = rcStickElement.width,
windowScale = (400 / stickContext.canvas.clientHeight),
rateScale = (curveHeight / 2) / maxAngularVel,
lineScale = stickContext.canvas.width / stickContext.canvas.clientWidth;
stickContext.clearRect(0, 0, curveWidth, curveHeight);
// calculate the fontSize based upon window scaling
if(windowScale <= 1) {
stickContext.font = "24pt Verdana, Arial, sans-serif";
} else {
stickContext.font = (24 * windowScale) + "pt Verdana, Arial, sans-serif";
}
currentValues.push(plotStickPosition(stickContext, RC.channels[0], self.currentRates.roll_rate, self.currentRates.rc_rate, self.currentRates.rc_expo, self.currentRates.superexpo, self.currentRates.deadband, maxAngularVel, '#FF8080') + ' deg/s');
currentValues.push(plotStickPosition(stickContext, RC.channels[1], self.currentRates.roll_rate, self.currentRates.rc_rate, self.currentRates.rc_expo, self.currentRates.superexpo, self.currentRates.deadband, maxAngularVel, '#80FF80') + ' deg/s');
currentValues.push(plotStickPosition(stickContext, RC.channels[2], self.currentRates.yaw_rate, self.currentRates.rc_rate_yaw, self.currentRates.rc_yaw_expo, self.currentRates.superexpo, self.currentRates.yawDeadband, maxAngularVel, '#8080FF') + ' deg/s');
stickContext.lineWidth = 1 * lineScale;
// use a custom scale so that the text does not appear stretched
stickContext.scale(stickContext.canvas.clientHeight/stickContext.canvas.clientWidth,1);
// add the maximum range label
drawAxisLabel(stickContext, maxAngularVel.toFixed(0) + ' deg/s', curveContext.canvas.clientWidth/curveContext.canvas.clientHeight * ((curveWidth / 2) - 10), parseInt(stickContext.font)*1.2, 'right');
// and then the balloon labels.
balloonsDirty = []; // reset the dirty balloon draw area (for overlap detection)
// create an array of balloons to draw
var balloons = [
{value: parseInt(maxAngularVelRoll), balloon: function() {drawBalloonLabel(stickContext, maxAngularVelRoll, curveWidth, rateScale * (maxAngularVel - parseInt(maxAngularVelRoll)), 'right', BALLOON_COLORS.roll, balloonsDirty);}},
{value: parseInt(maxAngularVelPitch), balloon: function() {drawBalloonLabel(stickContext, maxAngularVelPitch, curveWidth, rateScale * (maxAngularVel - parseInt(maxAngularVelPitch)), 'right', BALLOON_COLORS.pitch, balloonsDirty);}},
{value: parseInt(maxAngularVelYaw), balloon: function() {drawBalloonLabel(stickContext, maxAngularVelYaw, curveWidth, rateScale * (maxAngularVel - parseInt(maxAngularVelYaw)), 'right', BALLOON_COLORS.yaw, balloonsDirty);}}
];
// and sort them in descending order so the largest value is at the top always
balloons.sort(function(a,b) {return (b.value - a.value)});
// add the current rc values
balloons.push(
{value: parseInt(currentValues[0]), balloon: function() {drawBalloonLabel(stickContext, currentValues[0], 10, 150, 'none', BALLOON_COLORS.roll, balloonsDirty);}},
{value: parseInt(currentValues[1]), balloon: function() {drawBalloonLabel(stickContext, currentValues[1], 10, 250, 'none', BALLOON_COLORS.pitch, balloonsDirty);}},
{value: parseInt(currentValues[2]), balloon: function() {drawBalloonLabel(stickContext, currentValues[2], 10, 350, 'none', BALLOON_COLORS.yaw, balloonsDirty);}}
);
// then display them on the chart
for(var i=0; i<balloons.length; i++) balloons[i].balloon();
stickContext.restore();
}
};
function updateRates (event) {
setTimeout(function () { // let global validation trigger and adjust the values first
var targetElement = $(event.target),
targetValue = checkInput(targetElement);
if(event) { // if an event is passed, then use it
var targetElement = $(event.target),
targetValue = checkInput(targetElement);
if (self.currentRates.hasOwnProperty(targetElement.attr('name')) && targetValue !== undefined) {
self.currentRates[targetElement.attr('name')] = targetValue;
if (self.currentRates.hasOwnProperty(targetElement.attr('name')) && targetValue !== undefined) {
self.currentRates[targetElement.attr('name')] = targetValue;
updateNeeded = true;
}
if (targetElement.attr('name') === 'rc_rate' && semver.lt(CONFIG.flightControllerVersion, "2.8.1")) {
self.currentRates.rc_rate_yaw = targetValue;
}
if (targetElement.attr('name') === 'roll_pitch_rate' && semver.lt(CONFIG.apiVersion, "1.7.0")) {
self.currentRates.roll_rate = targetValue;
self.currentRates.pitch_rate = targetValue;
updateNeeded = true;
}
if (targetElement.attr('name') === 'SUPEREXPO_RATES') {
self.currentRates.superexpo = targetElement.is(':checked');
updateNeeded = true;
}
} else { // no event was passed, just force a graph update
updateNeeded = true;
}
if (targetElement.attr('name') === 'rc_rate' && semver.lt(CONFIG.flightControllerVersion, "2.8.1")) {
self.currentRates.rc_rate_yaw = targetValue;
}
if (targetElement.attr('name') === 'roll_pitch_rate' && semver.lt(CONFIG.apiVersion, "1.7.0")) {
self.currentRates.roll_rate = targetValue;
self.currentRates.pitch_rate = targetValue;
updateNeeded = true;
}
if (targetElement.attr('name') === 'SUPEREXPO_RATES') {
self.currentRates.superexpo = targetElement.is(':checked');
updateNeeded = true;
}
if (updateNeeded) {
var curveHeight = rcCurveElement.height;
var curveWidth = rcCurveElement.width;
var lineScale = stickContext.canvas.width / stickContext.canvas.clientWidth;
curveContext.clearRect(0, 0, curveWidth, curveHeight);
curveContext.font = "24pt Verdana, Arial, sans-serif";
var maxAngularVel;
if (!useLegacyCurve) {
maxAngularVel = Math.max(
printMaxAngularVel(self.currentRates.roll_rate, self.currentRates.rc_rate, self.currentRates.rc_expo, self.currentRates.superexpo, self.currentRates.deadband, maxAngularVelRollElement),
@ -797,28 +899,18 @@ TABS.pid_tuning.initialize = function (callback) {
// make maxAngularVel multiple of 200deg/s so that the auto-scale doesn't keep changing for small changes of the maximum curve
maxAngularVel = Math.ceil(maxAngularVel/200) * 200;
drawAxes(curveContext, curveWidth, curveHeight, (curveHeight / 2) / maxAngularVel * 360);
drawAxisLabel(curveContext, maxAngularVel.toFixed(0) + ' deg/s', (curveWidth / 2) - 10, parseInt(curveContext.font)*1.2, 'right');
drawAxes(curveContext, curveWidth, curveHeight);
} else {
maxAngularVel = 0;
}
curveContext.lineWidth = 4;
curveContext.lineWidth = 2 * lineScale;
drawCurve(self.currentRates.roll_rate, self.currentRates.rc_rate, self.currentRates.rc_expo, self.currentRates.superexpo, self.currentRates.deadband, maxAngularVel, '#ff0000', 0, curveContext);
drawCurve(self.currentRates.pitch_rate, self.currentRates.rc_rate, self.currentRates.rc_expo, self.currentRates.superexpo, self.currentRates.deadband, maxAngularVel, '#00ff00', -4, curveContext);
drawCurve(self.currentRates.yaw_rate, self.currentRates.rc_rate_yaw, self.currentRates.rc_yaw_expo, self.currentRates.superexpo, self.currentRates.yawDeadband, maxAngularVel, '#0000ff', 4, curveContext);
if (!useLegacyCurve && maxAngularVel) {
var maxAngularVelRoll = maxAngularVelRollElement.text() + ' deg/s',
maxAngularVelPitch = maxAngularVelPitchElement.text() + ' deg/s',
maxAngularVelYaw = maxAngularVelYawElement.text() + ' deg/s',
rateScaling = (curveHeight / 2) / maxAngularVel;
drawBalloonLabel(curveContext, maxAngularVelRoll, curveWidth, rateScaling * (maxAngularVel - parseInt(maxAngularVelRoll)), 'right', '#FF8080', '#FF8080', '#000000');
drawBalloonLabel(curveContext, maxAngularVelPitch, curveWidth, rateScaling * (maxAngularVel - parseInt(maxAngularVelPitch)), 'right', '#80FF80', '#80FF80', '#000000');
drawBalloonLabel(curveContext, maxAngularVelYaw, curveWidth, rateScaling * (maxAngularVel - parseInt(maxAngularVelYaw)), 'right', '#8080FF', '#8080FF', '#000000');
}
self.updateRatesLabels();
updateNeeded = false;
}
@ -970,6 +1062,7 @@ TABS.pid_tuning.initRatesPreview = function () {
this.model = new Model($('.rates_preview'), $('.rates_preview canvas'));
$(window).on('resize', $.proxy(this.model.resize, this.model));
$(window).on('resize', $.proxy(this.updateRatesLabels, this));
};
TABS.pid_tuning.renderModel = function () {
@ -977,6 +1070,9 @@ TABS.pid_tuning.renderModel = function () {
if (!this.clock) { this.clock = new THREE.Clock(); }
if (!this.oldRC) {this.oldRC = [RC.channels[0], RC.channels[1], RC.channels[2]];}
if (this.updateRequired==null) this.updateRequired = new Object(false);
if (RC.channels[0] && RC.channels[1] && RC.channels[2]) {
var delta = this.clock.getDelta();
@ -985,6 +1081,18 @@ TABS.pid_tuning.renderModel = function () {
yaw = delta * this.rateCurve.rcCommandRawToDegreesPerSecond(RC.channels[2], this.currentRates.yaw_rate, this.currentRates.rc_rate_yaw, this.currentRates.rc_yaw_expo, this.currentRates.superexpo, this.currentRates.yawDeadband);
this.model.rotateBy(-degToRad(pitch), -degToRad(yaw), -degToRad(roll));
this.updateRequired = false;
for(var i=0; i<this.oldRC.length; i++) {
if(this.oldRC[i] != RC.channels[i]) {
this.oldRC[i] = RC.channels[i];
this.updateRequired = true;
}
}
if(this.updateRequired) { //TODO : find a way to trigger on screen resize and change update rate
this.updateRequired = false;
this.updateRatesLabels();
} // trigger a rate graph update if the RC value has changed
}
};
@ -995,6 +1103,9 @@ TABS.pid_tuning.cleanup = function (callback) {
$(window).off('resize', $.proxy(self.model.resize, self.model));
}
$(window).off('resize', $.proxy(this.updateRatesLabels, this));
self.keepRendering = false;
if (callback) callback();