1
0
mirror of https://github.com/f4exb/sdrangel.git synced 2026-03-10 18:19:36 -04:00

1464 lines
54 KiB
JavaScript

// Airbus like PFD (Primary Flight Display)
var icao;
var callsign;
var aircraftType;
var onSurface;
var wasOnSurface60SecsAgo;
var speed;
var modelSpeed; // Speed of 3D model in Cesium
var trueAirspeed;
var groundspeed;
var targetSpeed;
var altitude;
var targetAltitude;
var radioAltitude;
var verticalSpeed; // ft/m
var heading; // degrees mag
var targetHeading;
var track; // degrees true
var pressure; // mb
var mach;
var roll;
var autopilot;
var verticalMode;
var lateralMode;
var tcasMode;
var windSpeed;
var windDirection;
var staticAirTemperature;
var mdoelSpeed;
var toga = false;
var rollOut = false;
var runwayAltitude;
const grayColor = "#6A75AE";
const greenColor = "#20E966";
const cyanColor = "#2FF7FE";
const yellowColor = "#F4F82F";
const orangeColor = "#FEDA30";
const skyColor = "#26C9FF";
const groundColor = "#D35E34";
function setPFDData(forward, newICAO, newCallsign, newAircraftType, newOnSurface, newWasOnSurface60SecsAgo, newRunwayAltitudeEstimate,
newModelSpeed, newIndicatedAirspeed, newTrueAirspeed, newGroundspeed, newMach, newAltitude, newRadioAltitude, newQnh, newVerticalSpeed,
newHeading, newTrack, newRoll, newSelectedAltitude, newSelectedHeading,
newAutopilot, newVerticalMode, newLateralMode, newTCASMode,
newWindSpeed, newWindDirection, newStaticAirTemperature) {
//console.log('PFD', newIndicatedAirspeed, newMach, newAltitude, newQnh, newVerticalSpeed, newHeading, newTrack, newRoll, newSelectedAltitude, newSelectedHeading);
const newAircraft = icao !== newICAO;
if (newAircraft) {
toga = false;
rollOut = false;
runwayAltitude = undefined;
}
icao = newICAO;
callsign = newCallsign;
aircraftType = newAircraftType;
onSurface = newOnSurface;
wasOnSurface60SecsAgo = newWasOnSurface60SecsAgo;
modelSpeed = newModelSpeed;
if ((newIndicatedAirspeed === undefined) || (newOnSurface > 0)) {
// IAS not transmitted frequently. After landing will no longer be valid
if (newGroundspeed !== undefined) {
speed = newGroundspeed;
} else {
speed = newModelSpeed;
}
} else {
speed = newIndicatedAirspeed;
}
trueAirspeed = newTrueAirspeed;
groundspeed = newGroundspeed;
mach = newMach;
altitude = newAltitude;
radioAltitude = newRadioAltitude;
pressure = newQnh;
verticalSpeed = newVerticalSpeed;
if (newHeading === undefined) {
heading = newTrack; // Track more frequent than heading
} else {
heading = newHeading;
}
track = newTrack;
roll = newRoll;
targetAltitude = newSelectedAltitude;
targetHeading = newSelectedHeading;
if (newAutopilot === -1) {
autopilot = undefined;
} else {
autopilot = newAutopilot;
}
verticalMode = newVerticalMode;
lateralMode = newLateralMode;
tcasMode = newTCASMode;
windSpeed = newWindSpeed;
windDirection = newWindDirection;
staticAirTemperature = newStaticAirTemperature;
// Correct altitude for QNH setting
if (pressure !== undefined) {
altitude += (pressure - 1013.25) * 30;
}
if (!rollOut && forward && (wasOnSurface60SecsAgo === 0) && (onSurface > 0) && (modelSpeed >= 70)) {
rollOut = true;
} else if (rollOut && ((modelSpeed < 40) || (onSurface === 0))) {
rollOut = false;
}
const accelerationHeight = 1500;
if (!toga && !rollOut && forward && (onSurface > 0) && (modelSpeed >= 35)) {
toga = true; // Start take-off roll on surface
runwayAltitude = altitude;
} else if (!toga && !rollOut && (modelSpeed >= 35) && (runwayAltitude === undefined) && (wasOnSurface60SecsAgo > 0) && (newRunwayAltitudeEstimate !== undefined)) {
toga = true; // For when selecting an aircraft having just taken off
runwayAltitude = newRunwayAltitudeEstimate;
} else if (!toga && !rollOut && (onSurface === 0) && (wasOnSurface60SecsAgo > 0) && (altitude !== undefined) && (runwayAltitude !== undefined) && (altitude < runwayAltitude + accelerationHeight)) {
toga = true; // For when played in reverse
} else if (toga && (modelSpeed <= 20)) {
toga = false;
} else if (toga && (altitude !== undefined) && (runwayAltitude !== undefined) && (altitude >= runwayAltitude + accelerationHeight)) {
toga = false;
}
if (toga && (runwayAltitude === undefined) && (newRunwayAltitudeEstimate !== undefined)) {
runwayAltitude = newRunwayAltitudeEstimate;
}
}
function isStd() {
return (pressure >= 1012) && (pressure < 1014);
}
function drawAirspeedIndicator(ctx, fm20, fm30) {
// Airspeed indicator
const speedIndWidth = 100;
const speedIndHeight = 500;
const speedIndLeft = 30;
const speedIndRight = speedIndLeft + speedIndWidth;
const speedIndTop = 250;
const speedIndBottom = speedIndTop + speedIndHeight;
const speedIndMid = speedIndTop + speedIndHeight / 2;
// Background
ctx.fillStyle = grayColor;
ctx.fillRect(speedIndLeft, speedIndTop, speedIndWidth, speedIndHeight);
// Clip speed tape
ctx.save();
ctx.rect(speedIndLeft, speedIndTop, speedIndWidth, speedIndHeight);
ctx.clip();
// Speed tape markings every 10knts
const speedTapMinSpeed = 0; // A320 only starts drawning from 30knts, but we draw from 0
const speedTapeLeft = speedIndRight - 20;
const speedTapeRight = speedIndRight;
const speedTapeSpacing = 60;
const speedTapeStep = 10;
const speedTextStep = 20;
const speedPerPixel = speedTapeSpacing / speedTapeStep;
const speedOffset = (speed % speedTextStep) * speedPerPixel;
ctx.beginPath();
var speedTape = Math.round(speed - speed % speedTextStep + 6 * speedTapeStep);
for (var i = -6; i < 5; i++) {
if (speedTape >= speedTapMinSpeed) {
ctx.moveTo(speedTapeLeft, speedOffset + i * speedTapeSpacing + speedIndMid);
ctx.lineTo(speedTapeRight, speedOffset + i * speedTapeSpacing + speedIndMid);
}
speedTape -= speedTapeStep;
}
ctx.lineWidth = 2;
ctx.strokeStyle = "white";
ctx.stroke();
// Speeds every 20knts
var speedText = Math.round(speed - speed % speedTextStep + 3 * speedTextStep);
for (var i = -3; i < 3; i++) {
ctx.font = "30px Arial";
ctx.fillStyle = "white";
if (speedText >= speedTapMinSpeed) {
const speedTextString = speedText.toString().padStart(3, '0');
ctx.fillText(speedTextString, speedIndLeft + 20, speedOffset + i * speedTapeSpacing * 2 + speedIndMid + fm30.actualBoundingBoxAscent / 2);
}
speedText -= speedTextStep;
}
// Disable clipping
ctx.restore();
ctx.beginPath();
ctx.moveTo(speedIndLeft, speedIndTop);
ctx.lineTo(speedIndRight + 30, speedIndTop);
var speedIndRightLineHeight = speed / (speedIndHeight / 2 / speedPerPixel); // White line on right only drawn down to speedTapMinSpeed
if (speedIndRightLineHeight > 1) {
speedIndRightLineHeight = 1;
ctx.moveTo(speedIndLeft, speedIndTop + speedIndHeight);
ctx.lineTo(speedIndRight + 30, speedIndTop + speedIndHeight);
}
ctx.moveTo(speedIndRight, speedIndTop);
ctx.lineTo(speedIndRight, speedIndTop + speedIndRightLineHeight * (speedIndHeight / 2) + (speedIndHeight / 2));
ctx.lineWidth = 2;
ctx.strokeStyle = "white";
ctx.stroke();
// Speed indicator
ctx.beginPath();
ctx.moveTo(speedIndLeft - 10, speedIndMid);
ctx.lineTo(speedIndLeft, speedIndMid);
ctx.moveTo(speedTapeLeft - 5, speedIndMid);
ctx.lineTo(speedTapeRight + 20, speedIndMid);
ctx.lineWidth = 2;
ctx.strokeStyle = yellowColor;
ctx.stroke();
ctx.beginPath();
ctx.moveTo(speedTapeRight + 10, speedIndMid);
ctx.lineTo(speedTapeRight + 35, speedIndMid - 10);
ctx.lineTo(speedTapeRight + 35, speedIndMid + 10);
ctx.lineTo(speedTapeRight + 10, speedIndMid);
ctx.fillStyle = yellowColor;
ctx.fill();
// Target speed
if (targetSpeed !== undefined) {
var targetSpeedY = (speed - targetSpeed) * speedPerPixel;
targetSpeedY = targetSpeedY + speedIndMid;
if (targetSpeedY < speedIndTop) {
// Target speed as text on top
ctx.font = "20px Arial";
ctx.fillStyle = cyanColor;
const targetSpeedMetrics = ctx.measureText(targetSpeed);
ctx.fillText(targetSpeed, speedIndRight - targetSpeedMetrics.width / 2, speedIndTop - 4);
} else if (targetSpeedY > speedIndBottom) {
// Target speed as text underneath
ctx.font = "20px Arial";
ctx.fillStyle = cyanColor;
const targetSpeedMetrics = ctx.measureText(targetSpeed);
ctx.fillText(targetSpeed, speedIndRight - targetSpeedMetrics.width / 2, speedIndBottom + fm20.actualBoundingBoxAscent + 4);
} else {
// Cyan triangle on right side
const targetSpeedWidth = 40;
const targetSpeedHeight = 30;
const targetSpeedLeft = speedIndRight;
const targetSpeedRight = targetSpeedLeft + targetSpeedWidth;
ctx.beginPath();
ctx.moveTo(targetSpeedLeft, targetSpeedY);
ctx.lineTo(targetSpeedRight, targetSpeedY - targetSpeedHeight / 2);
ctx.lineTo(targetSpeedRight, targetSpeedY + targetSpeedHeight / 2);
ctx.lineTo(targetSpeedLeft, targetSpeedY);
ctx.lineWidth = 2;
ctx.strokeStyle = cyanColor;
ctx.stroke();
}
}
}
function drawAltitudeIndicator(ctx, fm20, fm30) {
// Altitude indicator
const altitudeIndWidth = 70;
const altitudeIndHeight = 500;
const altitudeIndLeft = 750;
const altitudeIndRight = altitudeIndLeft + altitudeIndWidth;
const altitudeIndTop = 250;
const altitudeIndBottom = altitudeIndTop + altitudeIndHeight;
const altitudeIndMid = altitudeIndTop + altitudeIndHeight / 2;
// Background
ctx.fillStyle = grayColor;
ctx.fillRect(altitudeIndLeft, altitudeIndTop, altitudeIndWidth, altitudeIndHeight);
ctx.font = "30px Arial";
const greaterThanMetrics = ctx.measureText(">");
// Mid point marker (for glideslope, but always visible)
ctx.fillStyle = yellowColor;
ctx.fillRect(altitudeIndLeft - 70, altitudeIndMid - 3, 50, 6);
// Clip altitude tape
ctx.save();
ctx.rect(altitudeIndLeft - greaterThanMetrics.width, altitudeIndTop, altitudeIndWidth + greaterThanMetrics.width, altitudeIndHeight);
ctx.clip();
// Altitude tape markings every 100 ft
const altitudeTapeLeft = altitudeIndRight - 10;
const altitudeTapeRight = altitudeIndRight;
const altitudeTapeSpacing = 40;
const altitudeTapeStep = 100;
const altitudeTextStep = 500;
const altitudePerPixel = altitudeTapeSpacing / altitudeTapeStep; // reciprocal
const altitudeOffset = (altitude % altitudeTextStep) * altitudePerPixel;
ctx.beginPath();
var altitudeTape = Math.round(altitude - altitude % altitudeTextStep + 10 * altitudeTapeStep);
for (var i = -10; i < 7; i++) {
ctx.moveTo(altitudeTapeLeft, altitudeOffset + i * altitudeTapeSpacing + altitudeIndMid);
ctx.lineTo(altitudeTapeRight, altitudeOffset + i * altitudeTapeSpacing + altitudeIndMid);
altitudeTape -= altitudeTapeStep;
}
ctx.lineWidth = 2;
ctx.strokeStyle = "white";
ctx.stroke();
// Flight level texts every 500ft
ctx.font = "30px Arial";
ctx.fillStyle = "white";
var altitudeText = Math.round(altitude - altitude % altitudeTextStep + 2 * altitudeTextStep);
for (var i = -2; i < 2; i++) {
const flightLevelText = Math.abs(altitudeText / 100); // Don't display - if negative
const flightLevelTextString = ">" + flightLevelText.toString().padStart(3, '0');
ctx.fillText(flightLevelTextString, altitudeIndLeft - greaterThanMetrics.width, altitudeOffset + i * altitudeTapeSpacing * 5 + altitudeIndMid + fm30.actualBoundingBoxAscent / 2);
altitudeText -= altitudeTextStep;
}
// Disable clipping
ctx.restore();
// White lines at top, bottom and on right
ctx.beginPath();
ctx.moveTo(altitudeIndLeft, altitudeIndTop);
ctx.lineTo(altitudeIndRight + 30, altitudeIndTop);
ctx.moveTo(altitudeIndLeft, altitudeIndTop + altitudeIndHeight);
ctx.lineTo(altitudeIndRight + 30, altitudeIndTop + altitudeIndHeight);
ctx.moveTo(altitudeIndRight, altitudeIndTop);
ctx.lineTo(altitudeIndRight, altitudeIndTop + altitudeIndHeight);
ctx.lineWidth = 2;
ctx.strokeStyle = "white";
ctx.stroke();
// Target altitude
if (targetAltitude !== undefined) {
var targetAltitudeY = (altitude - targetAltitude) * altitudePerPixel;
targetAltitudeY = targetAltitudeY + altitudeIndMid;
if (isStd()) {
targetAltitudeText = Math.round(targetAltitude / 100);
} else {
targetAltitudeText = Math.round(targetAltitude);
}
if (targetAltitudeY < altitudeIndTop) {
// Target altitude as text on top
ctx.font = "30px Arial";
ctx.fillStyle = cyanColor;
const targetAltitudeMetrics = ctx.measureText(targetAltitudeText);
if (isStd()) {
ctx.fillText("FL", altitudeIndLeft, altitudeIndTop - 4);
}
ctx.fillText(targetAltitudeText, altitudeIndRight - targetAltitudeMetrics.width / 2, altitudeIndTop - 4);
} else if (targetAltitudeY > altitudeIndBottom) {
// Target altitude as text underneath
ctx.font = "30px Arial";
ctx.fillStyle = cyanColor;
const targetAltitudeMetrics = ctx.measureText(targetAltitudeText);
if (isStd()) {
ctx.fillText("FL", altitudeIndLeft, altitudeIndBottom + fm30.actualBoundingBoxAscent + 4);
}
ctx.fillText(targetAltitudeText, altitudeIndRight - targetAltitudeMetrics.width / 2, altitudeIndBottom + fm30.actualBoundingBoxAscent + 4);
} else {
// Clip
ctx.save();
ctx.rect(altitudeIndLeft - greaterThanMetrics.width, altitudeIndTop, altitudeIndWidth + greaterThanMetrics.width, altitudeIndHeight);
ctx.clip();
targetAltitudeText = targetAltitudeText.toString().padStart(3, '0');
ctx.font = "30px Arial";
ctx.fillStyle = cyanColor;
const targetAltitudeMetrics = ctx.measureText(targetAltitudeText);
// Cyan box
const targetAltitudeWidth = 30;
const targetAltitudeHeight = 3 * (fm30.actualBoundingBoxAscent + fm30.actualBoundingBoxDescent);
const targetAltitudeBoxHeight = 1.1 * (fm30.actualBoundingBoxAscent + fm30.actualBoundingBoxDescent);
const targetAltitudeLeft = altitudeIndLeft - 5;
const targetAltitudeRight = targetAltitudeLeft + targetAltitudeWidth;
ctx.beginPath();
ctx.moveTo(targetAltitudeLeft, targetAltitudeY - targetAltitudeHeight / 2);
ctx.lineTo(targetAltitudeLeft, targetAltitudeY - 5);
ctx.lineTo(targetAltitudeLeft + 5, targetAltitudeY);
ctx.lineTo(targetAltitudeLeft, targetAltitudeY + 5);
ctx.lineTo(targetAltitudeLeft, targetAltitudeY + targetAltitudeHeight / 2);
ctx.lineTo(targetAltitudeRight, targetAltitudeY + targetAltitudeHeight / 2);
ctx.lineTo(targetAltitudeRight, targetAltitudeY - targetAltitudeHeight / 2);
ctx.lineTo(targetAltitudeLeft, targetAltitudeY - targetAltitudeHeight / 2);
ctx.lineWidth = 2;
ctx.lineWidth = 2;
ctx.strokeStyle = cyanColor;
ctx.stroke();
ctx.fillStyle = 'black';
ctx.fillRect(altitudeIndLeft, targetAltitudeY - targetAltitudeBoxHeight / 2, altitudeIndWidth - 2, targetAltitudeBoxHeight);
ctx.fillStyle = cyanColor;
ctx.fillText(targetAltitudeText, altitudeIndLeft, targetAltitudeY + fm30.actualBoundingBoxAscent / 2);
// Disable clipping
ctx.restore();
}
}
// Altitude
ctx.font = "34px Arial";
const levelText = (altitude === undefined) || isNaN(altitude) ? "" : Math.floor(altitude / 100).toString();
const levelTextMetrics = ctx.measureText(levelText);
const levelTextHeightMetrics = ctx.measureText("0");
const border = 2;
const levelHeight = levelTextHeightMetrics.actualBoundingBoxAscent + levelTextHeightMetrics.actualBoundingBoxDescent + 8 * border;
const subscaleHeight = 60;
const subscaleWidth = 30;
// Black background box
ctx.fillStyle = 'black';
ctx.fillRect(altitudeIndLeft, altitudeIndMid - levelHeight / 2, altitudeIndWidth + 1, levelHeight);
// Current flight level
if (levelText !== "") {
ctx.fillStyle = greenColor;
ctx.fillText(levelText, altitudeIndRight - levelTextMetrics.width, altitudeIndMid + levelTextMetrics.actualBoundingBoxAscent / 2);
}
// Yellow box
ctx.beginPath();
ctx.moveTo(altitudeIndLeft, altitudeIndMid - levelHeight / 2);
ctx.lineTo(altitudeIndRight, altitudeIndMid - levelHeight / 2);
ctx.lineTo(altitudeIndRight, altitudeIndMid - subscaleHeight / 2);
ctx.lineTo(altitudeIndRight + subscaleWidth, altitudeIndMid - subscaleHeight / 2);
ctx.lineTo(altitudeIndRight + subscaleWidth, altitudeIndMid + subscaleHeight / 2);
ctx.lineTo(altitudeIndRight, altitudeIndMid + subscaleHeight / 2);
ctx.lineTo(altitudeIndRight, altitudeIndMid + levelHeight / 2);
ctx.lineTo(altitudeIndLeft, altitudeIndMid + levelHeight / 2);
ctx.lineWidth = 2;
if (Math.abs(verticalSpeed) >= 6000) {
ctx.strokeStyle = orangeColor;
} else {
ctx.strokeStyle = yellowColor;
}
ctx.stroke();
// Clip around subscale
ctx.save();
ctx.rect(altitudeIndRight, altitudeIndMid - subscaleHeight / 2, subscaleWidth, subscaleHeight);
ctx.clip();
const subscaleStep = 20;
var subscaleText = (altitude % 100) + subscaleStep * 2;
var subscaleSpacing = fm20.actualBoundingBoxAscent + fm20.actualBoundingBoxDescent + 2;
const subscalePerPixel = subscaleSpacing / subscaleStep; // reciprocal
const subscaleOffset = (altitude % subscaleStep) * subscalePerPixel;
ctx.font = "20px Arial";
for (var i = -2; i < 2; i++) {
const subscaleTextString = (Math.floor((subscaleText % 100) / subscaleStep) * subscaleStep).toString().padStart(2, '0');
ctx.fillText(subscaleTextString, altitudeIndRight + 1, subscaleOffset + i * subscaleSpacing + altitudeIndMid + fm20.actualBoundingBoxAscent / 2);
subscaleText -= subscaleStep;
if (subscaleText < 0) {
subscaleText += 100;
}
}
// Disable clipping
ctx.restore();
}
function drawHeadingIndicator(ctx, fm20, fm30) {
const headingIndWidth = 450;
const headingIndHeight = 60;
const headingIndLeft = 225;
const headingIndRight = headingIndLeft + headingIndWidth;
const headingIndTop = 890;
const headingIndBottom = headingIndTop + headingIndHeight;
const headingIndMid = headingIndLeft + headingIndWidth / 2;
ctx.fillStyle = grayColor;
ctx.fillRect(headingIndLeft, headingIndTop, headingIndWidth, headingIndHeight);
// White lines left, right and top
ctx.beginPath();
ctx.moveTo(headingIndLeft, headingIndBottom);
ctx.lineTo(headingIndLeft, headingIndTop);
ctx.lineTo(headingIndRight, headingIndTop);
ctx.lineTo(headingIndRight, headingIndBottom);
ctx.lineWidth = 2;
ctx.strokeStyle = "white";
ctx.stroke();
// Heading indicator line
ctx.beginPath();
ctx.moveTo(headingIndMid, headingIndTop - 30);
ctx.lineTo(headingIndMid, headingIndTop);
ctx.lineWidth = 4;
ctx.strokeStyle = yellowColor;
ctx.stroke();
// Clip tape
ctx.save();
ctx.rect(headingIndLeft, headingIndTop, headingIndWidth, headingIndHeight);
ctx.clip();
// Tape markings every 5 degrees
const headingTapeLeft = headingIndRight - 20;
const headingTapeRight = headingIndRight;
const headingTapeSpacing = 50;
const headingTapeStep = 5;
const headingTextStep = 10;
const headingPerPixel = headingTapeSpacing / headingTapeStep;
const headingOffset = - (heading % headingTextStep) * headingPerPixel;
ctx.beginPath();
var headingTape = Math.round(heading - heading % headingTextStep - 6 * headingTapeStep);
for (var i = -6; i < 7; i++) {
ctx.moveTo(headingOffset + i * headingTapeSpacing + headingIndMid, headingIndTop);
if (headingTape % 10 != 0) {
ctx.lineTo(headingOffset + i * headingTapeSpacing + headingIndMid, headingIndTop + 10);
} else {
ctx.lineTo(headingOffset + i * headingTapeSpacing + headingIndMid, headingIndTop + 20);
}
headingTape += headingTapeStep;
}
ctx.lineWidth = 2;
ctx.strokeStyle = "white";
ctx.stroke();
ctx.font = "30px Arial";
ctx.fillStyle = "white";
const headingMetricsBig = ctx.measureText("36");
ctx.font = "24px Arial";
ctx.fillStyle = "white";
const headingMetricsSmall = ctx.measureText("36");
// Headings every 10 degrees
var headingText = Math.round(heading - heading % headingTextStep - 3 * headingTextStep);
for (var i = -3; i < 4; i++) {
const big = headingText % 30 == 0;
if (big) {
ctx.font = "30px Arial";
} else {
ctx.font = "24px Arial";
}
ctx.fillStyle = "white";
var headingTextMod = Math.round(headingText / 10) % 36;
if (headingTextMod < 0) {
headingTextMod += 36;
}
const headingTextString = headingTextMod.toString();
const headingMetrics = ctx.measureText(headingTextString);
ctx.fillText(headingTextString, headingOffset + i * headingTapeSpacing * 2 + headingIndMid - headingMetrics.width / 2, headingIndTop + 30 + headingMetrics.actualBoundingBoxAscent);
headingText += headingTextStep;
}
// Track diamond
if (track !== undefined) {
const trackMid = headingIndMid + (track - heading) * headingPerPixel;
const trackSize = 20;
ctx.beginPath();
ctx.moveTo(trackMid, headingIndTop);
ctx.lineTo(trackMid - trackSize / 2, headingIndTop + trackSize / 2);
ctx.lineTo(trackMid, headingIndTop + trackSize);
ctx.lineTo(trackMid + trackSize / 2, headingIndTop + trackSize / 2);
ctx.lineTo(trackMid, headingIndTop);
ctx.lineWidth = 5;
ctx.strokeStyle = greenColor;
ctx.stroke();
}
// Disable clipping
ctx.restore();
// Target heading
if (targetHeading !== undefined) {
var targetDiff = targetHeading - heading;
if (targetDiff > 180) {
targetDiff -= 360;
}
var targetMid = headingIndMid + targetDiff * headingPerPixel;
if (targetMid < headingIndLeft) {
// Display target heading as text on left
ctx.font = "20px Arial";
ctx.fillStyle = cyanColor;
const targetHeadingText = Math.round(targetHeading).toString().padStart(3, '0');
const targetHeadingMetrics = ctx.measureText(targetHeadingText);
ctx.fillText(targetHeadingText, headingIndLeft, headingIndTop - 4);
} else if (targetMid > headingIndRight) {
// Display target heading as text on right
ctx.font = "20px Arial";
ctx.fillStyle = cyanColor;
const targetHeadingText = Math.round(targetHeading).toString().padStart(3, '0');
const targetHeadingMetrics = ctx.measureText(targetHeadingText);
ctx.fillText(targetHeadingText, headingIndRight - targetHeadingMetrics.width, headingIndTop - 4);
} else {
// Target indicator triangle
const targetWidth = 30;
const targetHeight = 40;
ctx.beginPath();
ctx.moveTo(targetMid, headingIndTop);
ctx.lineTo(targetMid - targetWidth / 2, headingIndTop - targetHeight);
ctx.lineTo(targetMid + targetWidth / 2, headingIndTop - targetHeight);
ctx.lineTo(targetMid, headingIndTop);
ctx.lineWidth = 2;
ctx.strokeStyle = cyanColor;
ctx.stroke();
}
}
}
function drawBankAngleBoxMarker(ctx, radius, height) {
ctx.beginPath();
ctx.moveTo(-5, -radius);
ctx.lineTo(-5, -radius - height);
ctx.lineTo(5, -radius - height);
ctx.lineTo(5, -radius);
ctx.lineWidth = 2;
ctx.strokeStyle = 'white';
ctx.stroke();
}
function drawBankAngleLineMarker(ctx, radius, height) {
ctx.beginPath();
ctx.moveTo(0, -radius);
ctx.lineTo(0, -radius - height);
ctx.lineWidth = 2;
ctx.strokeStyle = 'white';
ctx.stroke();
}
function drawAttitudeIndicator(ctx, fm20, fm30) {
if (roll === undefined) {
return;
}
// Height the same as airspeed indicator - cropped width same as heading indicator
const speedIndRight = 30 + 100;
const altitudeIndLeft = 750;
const attitudeIndMidX = speedIndRight + ((altitudeIndLeft - speedIndRight) / 2);
//const attitudeIndLeft = 30 + 100; // speedIndRight
//const attitudeIndRight = 750; // altitudeIndLeft
const attitudeIndWidth = 500; // speedIndHeight = 500; // attitudeIndRight - attitudeIndLeft;
const attitudeIndTop = 250;
const attitudeIndLeft = attitudeIndMidX - attitudeIndWidth / 2;
const attitudeIndHeight = attitudeIndWidth; // It's a circle
const attitudeIndMidY = attitudeIndTop + attitudeIndHeight / 2;
const attitudeIndCrop = (attitudeIndWidth - 450) / 2; // headingIndWidth = 450
// Clip
ctx.save();
//ctx.arc(attitudeIndMidX, attitudeIndMidY, attitudeIndWidth / 2, 0, 2 * Math.PI);
ctx.beginPath();
ctx.arc(attitudeIndMidX, attitudeIndMidY, attitudeIndWidth / 2, Math.PI / 10 + Math.PI, Math.PI - Math.PI / 10 + Math.PI);
ctx.arc(attitudeIndMidX, attitudeIndMidY, attitudeIndWidth / 2, Math.PI / 10, Math.PI - Math.PI / 10);
// //ctx.rect(attitudeIndLeft + attitudeIndCrop, attitudeIndTop, attitudeIndWidth - 2 * attitudeIndCrop, attitudeIndWidth);
// ctx.rect(attitudeIndLeft + attitudeIndCrop, attitudeIndTop, attitudeIndWidth - 2 * attitudeIndCrop, attitudeIndWidth);
ctx.clip();
// Background
//ctx.beginPath();
//ctx.fillStyle = cyanColor;
//ctx.arc(attitudeIndMidX, attitudeIndMidY, attitudeIndWidth / 2, Math.PI / 10, Math.PI - Math.PI / 10);
//ctx.arc(attitudeIndMidX, attitudeIndMidY, attitudeIndWidth / 2, Math.PI / 10 + Math.PI, Math.PI - Math.PI / 10 + Math.PI);
//console.log(attitudeIndMidX, attitudeIndMidY, attitudeIndWidth / 2, 0, 2 * Math.PI);
//ctx.fill();
ctx.fillStyle = skyColor;
ctx.fillRect(attitudeIndLeft, attitudeIndTop, attitudeIndWidth, attitudeIndWidth);
const rollRad = -roll * Math.PI / 180;
const radius = attitudeIndWidth / 2;
ctx.fillStyle = groundColor;
ctx.beginPath();
ctx.arc(attitudeIndMidX, attitudeIndMidY, radius, 0 + rollRad, Math.PI + rollRad);
ctx.fill();
//const offsetX = Math.cos(rollRad) * radius;
//const offsetY = Math.sin(rollRad) * radius;
//ctx.beginPath();
//ctx.moveTo(attitudeIndMidX - offsetX, attitudeIndMidY - offsetY);
//ctx.lineTo(attitudeIndMidX + offsetX, attitudeIndMidY + offsetY);
//ctx.lineWidth = 2;
//ctx.strokeStyle = 'white';
//ctx.stroke();
// Disable clipping
//ctx.restore();
const twoAndHalfDegOffset = 15;
const fiveDegOffset = twoAndHalfDegOffset * 2;
const tenDegOffset = twoAndHalfDegOffset * 4;
const fifteenDegOffset = twoAndHalfDegOffset * 2.5;
const spacing = 50;
// ctx.save();
ctx.translate(attitudeIndMidX, attitudeIndMidY);
ctx.rotate(rollRad);
ctx.beginPath();
ctx.moveTo(-radius, 0);
ctx.lineTo(radius, 0);
for (var i = -2; i < 4; i++) {
ctx.moveTo(-twoAndHalfDegOffset, -i * spacing - spacing / 2);
ctx.lineTo(twoAndHalfDegOffset, -i * spacing - spacing / 2);
}
for (var i = -1; i < 5; i++) {
ctx.moveTo(-fiveDegOffset, -i * spacing);
ctx.lineTo(fiveDegOffset, -i * spacing);
}
for (var i = -1; i < 3; i++) {
ctx.moveTo(-tenDegOffset, -i * 2 * spacing);
ctx.lineTo(tenDegOffset, -i * 2 * spacing);
}
ctx.moveTo(-fifteenDegOffset, 2.75 * spacing);
ctx.lineTo(fifteenDegOffset, 2.75 * spacing);
ctx.moveTo(-tenDegOffset, 3.5 * spacing);
ctx.lineTo(tenDegOffset, 3.5 * spacing);
ctx.lineWidth = 2;
ctx.strokeStyle = 'white';
ctx.stroke();
const greenThingWidth = 20;
ctx.beginPath();
ctx.strokeStyle = greenColor;
ctx.moveTo(-fifteenDegOffset - greenThingWidth, 2.75 * spacing - 3);
ctx.lineTo(-fifteenDegOffset, 2.75 * spacing - 3);
ctx.moveTo(-fifteenDegOffset - greenThingWidth, 2.75 * spacing + 3);
ctx.lineTo(-fifteenDegOffset, 2.75 * spacing + 3);
ctx.moveTo(fifteenDegOffset + greenThingWidth, 2.75 * spacing - 3);
ctx.lineTo(fifteenDegOffset, 2.75 * spacing - 3);
ctx.moveTo(fifteenDegOffset + greenThingWidth, 2.75 * spacing + 3);
ctx.lineTo(fifteenDegOffset, 2.75 * spacing + 3);
ctx.stroke();
ctx.font = "20px Arial";
ctx.fillStyle = 'white';
const labelMetrics = ctx.measureText("20");
const labelCenterY = labelMetrics.actualBoundingBoxAscent / 2;
const labelOffset = tenDegOffset + 10;
ctx.fillText("10", labelOffset, spacing * 2 + labelCenterY);
ctx.fillText("10", -labelOffset - labelMetrics.width, spacing * 2 + labelCenterY);
ctx.fillText("10", labelOffset, -spacing * 2 + labelCenterY);
ctx.fillText("10", -labelOffset - labelMetrics.width, -spacing * 2 + labelCenterY);
ctx.fillText("20", labelOffset, -spacing * 4 + labelCenterY);
ctx.fillText("20", -labelOffset - labelMetrics.width, -spacing * 4 + labelCenterY);
ctx.fillText("20", labelOffset, spacing * 3.5 + labelCenterY);
ctx.fillText("20", -labelOffset - labelMetrics.width, spacing * 3.5 + labelCenterY);
const rollBig = 18;
const rollSmall = 10;
const rollTriangleOffet = 4;
ctx.beginPath();
ctx.moveTo(0, -radius + rollTriangleOffet);
ctx.lineTo(rollBig, -radius + rollBig + rollTriangleOffet);
ctx.lineTo(-rollBig, -radius + rollBig + rollTriangleOffet);
ctx.lineTo(0, -radius + rollTriangleOffet);
ctx.lineWidth = 2;
ctx.strokeStyle = yellowColor;
ctx.stroke();
const rollTrapX1 = 20;
const rollTrapX2 = 30;
const rollTrapY1 = -radius + rollBig + rollTriangleOffet + 5;
const rollTrapY2 = rollTrapY1 + 10;
ctx.beginPath();
ctx.moveTo(-rollTrapX1, rollTrapY1);
ctx.lineTo(rollTrapX1, rollTrapY1);
ctx.lineTo(rollTrapX2, rollTrapY2);
ctx.lineTo(-rollTrapX2, rollTrapY2);
ctx.lineTo(-rollTrapX1, rollTrapY1);
ctx.lineWidth = 2;
ctx.strokeStyle = yellowColor;
ctx.stroke();
ctx.restore();
// Roll scale
ctx.save();
ctx.translate(attitudeIndMidX, attitudeIndMidY);
ctx.beginPath();
ctx.rect(-radius, -radius - 2 *rollBig , radius * 2, radius);
ctx.clip();
ctx.beginPath();
ctx.moveTo(0, -radius);
ctx.lineTo(rollBig, -radius - rollBig);
ctx.lineTo(-rollBig, -radius - rollBig);
ctx.lineTo(0, -radius);
ctx.lineWidth = 2;
ctx.strokeStyle = yellowColor;
ctx.stroke();
ctx.beginPath();
ctx.arc(0, 0, radius, (240 - 2) * Math.PI / 180, (300 + 2) * Math.PI / 180);
ctx.lineWidth = 2;
ctx.strokeStyle = 'white';
ctx.stroke();
ctx.rotate(45 * Math.PI / 180);
drawBankAngleLineMarker(ctx, radius, rollBig);
ctx.rotate(-15 * Math.PI / 180);
drawBankAngleBoxMarker(ctx, radius, rollBig);
ctx.rotate(-10 * Math.PI / 180);
drawBankAngleBoxMarker(ctx, radius, 10);
ctx.rotate(-10 * Math.PI / 180);
drawBankAngleBoxMarker(ctx, radius, 10);
ctx.rotate(-10 * Math.PI / 180);
ctx.rotate(-10 * Math.PI / 180);
drawBankAngleBoxMarker(ctx, radius, 10);
ctx.rotate(-10 * Math.PI / 180);
drawBankAngleBoxMarker(ctx, radius, 10);
ctx.rotate(-10 * Math.PI / 180);
drawBankAngleBoxMarker(ctx, radius, rollBig);
ctx.rotate(-15 * Math.PI / 180);
drawBankAngleLineMarker(ctx, radius, rollBig);
// ctx.rotate(-rollRad);
// ctx.translate(-attitudeIndMidX, -attitudeIndMidY);
ctx.restore();
// Wings
const wingThickness = 14;
const wingHeight = 35;
const wingWidth = 90;
const wingOffset = 130;
ctx.beginPath();
ctx.moveTo(attitudeIndMidX - wingOffset - wingWidth, attitudeIndMidY - wingThickness / 2);
ctx.lineTo(attitudeIndMidX - wingOffset, attitudeIndMidY - wingThickness / 2);
ctx.lineTo(attitudeIndMidX - wingOffset, attitudeIndMidY + wingHeight);
ctx.lineTo(attitudeIndMidX - wingOffset - wingThickness, attitudeIndMidY + wingHeight);
ctx.lineTo(attitudeIndMidX - wingOffset - wingThickness, attitudeIndMidY + wingThickness / 2);
ctx.lineTo(attitudeIndMidX - wingOffset - wingWidth, attitudeIndMidY + wingThickness / 2);
ctx.lineTo(attitudeIndMidX - wingOffset - wingWidth, attitudeIndMidY - wingThickness / 2);
ctx.lineWidth = 4;
ctx.fillStyle = 'black';
ctx.strokeStyle = yellowColor;
ctx.stroke();
ctx.fill();
ctx.beginPath();
ctx.moveTo(attitudeIndMidX + wingOffset + wingWidth, attitudeIndMidY - wingThickness / 2);
ctx.lineTo(attitudeIndMidX + wingOffset, attitudeIndMidY - wingThickness / 2);
ctx.lineTo(attitudeIndMidX + wingOffset, attitudeIndMidY + wingHeight);
ctx.lineTo(attitudeIndMidX + wingOffset + wingThickness, attitudeIndMidY + wingHeight);
ctx.lineTo(attitudeIndMidX + wingOffset + wingThickness, attitudeIndMidY + wingThickness / 2);
ctx.lineTo(attitudeIndMidX + wingOffset + wingWidth, attitudeIndMidY + wingThickness / 2);
ctx.lineTo(attitudeIndMidX + wingOffset + wingWidth, attitudeIndMidY - wingThickness / 2);
ctx.lineWidth = 4;
ctx.fillStyle = 'black';
ctx.strokeStyle = yellowColor;
ctx.stroke();
ctx.fill();
ctx.strokeRect(attitudeIndMidX - wingThickness / 2, attitudeIndMidY - wingThickness / 2, wingThickness, wingThickness);
// Radio altitude
if ((radioAltitude !== undefined) && (radioAltitude <= 2500)) {
ctx.font = "30px Arial";
if (radioAltitude < 100) {
ctx.fillStyle = orangeColor;
} else {
ctx.fillStyle = greenColor;
}
const raText = Math.round(radioAltitude).toString();
const raTextMetrics = ctx.measureText(raText);
ctx.fillText(raText, attitudeIndMidX - raTextMetrics.width / 2, attitudeIndTop + attitudeIndHeight - 4);
}
}
function drawPressure(ctx, fm20, fm30) {
if (pressure !== undefined) {
const pressureLeft = 750; // Righthand side of altitude indicator
const pressureMidX = 750 + 80; // Lefthand side of altitude indicator
const pressureMidY = 850;
const pressureInt = Math.round(pressure);
if (isStd()) {
const border = 3;
ctx.font = "30px Arial";
ctx.fillStyle = cyanColor;
const pressureText = "STD";
const pressureMetrics = ctx.measureText(pressureText);
const height = pressureMetrics.actualBoundingBoxAscent + pressureMetrics.actualBoundingBoxDescent + 2 * border;
ctx.fillText(pressureText, pressureMidX - pressureMetrics.width / 2, pressureMidY);
ctx.lineWidth = 2;
ctx.strokeStyle = yellowColor;
ctx.strokeRect(pressureMidX - pressureMetrics.width / 2 - border, pressureMidY - pressureMetrics.actualBoundingBoxAscent - 2 * border, pressureMetrics.width + border * 2, height + border * 2);
} else {
ctx.font = "30px Arial";
ctx.fillStyle = "white";
ctx.fillText("QNH", pressureLeft, pressureMidY);
ctx.fillStyle = cyanColor;
const pressureText = pressureInt.toString();
ctx.fillText(pressureText, pressureMidX, pressureMidY);
}
}
}
function drawMachIndicator(ctx) {
if (mach !== undefined) {
if (mach >= 0.45) { // Actually comes on at .5 and goes off at 0.45
ctx.font = "38px Arial";
ctx.fillStyle = greenColor;
var machString;
if (mach < 1) {
machString = mach.toFixed(3).substring(1); // No leading 0
} else {
machString = mach.toFixed(2);
}
ctx.fillText(machString, 40, 840);
}
}
}
function drawVerticalSpeedIndicator(ctx, fm20, fm30) {
const verticalSpeedIndWidth = 50;
const verticalSpeedIndHeight = 620;
const verticalSpeedIndLeft = 900;
const verticalSpeedIndTop = 190;
const verticalSpeedIndBottom = verticalSpeedIndTop + verticalSpeedIndHeight;
const verticalSpeedIndMidY = verticalSpeedIndTop + + verticalSpeedIndHeight / 2;
const verticalSpeedIndMidX = verticalSpeedIndLeft + verticalSpeedIndWidth / 2;
const verticalSpeedIndRight = verticalSpeedIndLeft + verticalSpeedIndWidth;
const verticalSpeedBevel = 80;
const verticalSpeedMarkerSpacing = 35;
const verticalSpeedMarkerWidth = verticalSpeedIndWidth / 4;
const verticalSpeedMarkerLeft = verticalSpeedIndLeft + verticalSpeedMarkerWidth;
const verticalSpeedIndText = verticalSpeedIndLeft + 1;
const verticalSpeedLineRight = verticalSpeedIndRight + 30;
const verticalSpeedLineLeft = verticalSpeedIndMidX - 5;
// Background
ctx.beginPath();
ctx.moveTo(verticalSpeedIndLeft, verticalSpeedIndTop);
ctx.lineTo(verticalSpeedIndMidX, verticalSpeedIndTop);
ctx.lineTo(verticalSpeedIndRight, verticalSpeedIndTop + verticalSpeedBevel);
ctx.lineTo(verticalSpeedIndRight, verticalSpeedIndBottom - verticalSpeedBevel);
ctx.lineTo(verticalSpeedIndMidX, verticalSpeedIndBottom);
ctx.lineTo(verticalSpeedIndLeft, verticalSpeedIndBottom);
ctx.lineTo(verticalSpeedIndLeft, verticalSpeedIndTop);
ctx.fillStyle = grayColor;
ctx.fill();
// Labels
ctx.font = "20px Arial";
ctx.fillStyle = 'white';
ctx.fillText("6", verticalSpeedIndText, verticalSpeedIndMidY - 8 * verticalSpeedMarkerSpacing + fm20.actualBoundingBoxAscent / 2);
ctx.fillText("2", verticalSpeedIndText, verticalSpeedIndMidY - 6 * verticalSpeedMarkerSpacing + fm20.actualBoundingBoxAscent / 2);
ctx.fillText("1", verticalSpeedIndText, verticalSpeedIndMidY - 4 * verticalSpeedMarkerSpacing + fm20.actualBoundingBoxAscent / 2);
ctx.fillText("1", verticalSpeedIndText, verticalSpeedIndMidY + 4 * verticalSpeedMarkerSpacing + fm20.actualBoundingBoxAscent / 2);
ctx.fillText("2", verticalSpeedIndText, verticalSpeedIndMidY + 6 * verticalSpeedMarkerSpacing + fm20.actualBoundingBoxAscent / 2);
ctx.fillText("6", verticalSpeedIndText, verticalSpeedIndMidY + 8 * verticalSpeedMarkerSpacing + fm20.actualBoundingBoxAscent / 2);
// Markers
ctx.fillStyle = 'white';
for (var i = -8; i <= 8; i++)
{
const j = Math.abs(i);
if (!((j == 1) || (j == 3))) {
var height;
if ((j == 4) || (j == 6) || (j == 8)) {
height = 6;
} else {
height = 2;
}
ctx.fillRect(verticalSpeedMarkerLeft, verticalSpeedIndMidY - i * verticalSpeedMarkerSpacing - height / 2, verticalSpeedMarkerWidth, height);
}
}
// 0 marker
ctx.fillStyle = yellowColor;
ctx.fillRect(verticalSpeedIndLeft, verticalSpeedIndMidY - 3, verticalSpeedIndWidth / 2, 6);
// Indicator line
const verticalSpeedAbsolute = Math.abs(verticalSpeed);
var verticalOffset;
var color = greenColor;
if (verticalSpeedAbsolute <= 1000) {
verticalOffset = verticalSpeedAbsolute / 1000 * 4 * verticalSpeedMarkerSpacing;
} else if (verticalSpeedAbsolute < 2000) {
verticalOffset = (verticalSpeedAbsolute - 1000) / 1000 * 2 * verticalSpeedMarkerSpacing + 4 * verticalSpeedMarkerSpacing;
} else if (verticalSpeedAbsolute < 6000) {
verticalOffset = (verticalSpeedAbsolute - 2000) / 4000 * 2 * verticalSpeedMarkerSpacing + 6 * verticalSpeedMarkerSpacing;
} else {
verticalOffset = 8 * verticalSpeedMarkerSpacing;
color = orangeColor;
}
const verticalSpeedLineY = verticalSpeedIndMidY - verticalOffset * Math.sign(verticalSpeed);
ctx.beginPath();
ctx.moveTo(verticalSpeedLineRight, verticalSpeedIndMidY);
ctx.lineTo(verticalSpeedLineLeft, verticalSpeedLineY);
ctx.lineWidth = 4;
ctx.strokeStyle = color;
ctx.stroke();
// Text
if (verticalSpeedAbsolute > 200) {
const verticalSpeedTextX = verticalSpeedLineLeft + 3;
const verticalSpeedTextHeight = fm20.actualBoundingBoxAscent + fm20.actualBoundingBoxDescent + 2;
const verticalSpeedText = Math.round(verticalSpeedAbsolute / 100);
const verticalSpeedTextString = verticalSpeedText.toString().padStart(2, '0');
// Background
ctx.fillStyle = 'black';
if (verticalSpeed > 0) {
ctx.fillRect(verticalSpeedTextX, verticalSpeedLineY - verticalSpeedTextHeight, verticalSpeedIndWidth / 2, verticalSpeedTextHeight);
} else {
ctx.fillRect(verticalSpeedTextX, verticalSpeedLineY, verticalSpeedIndWidth / 2, verticalSpeedTextHeight);
}
ctx.fillStyle = color;
if (verticalSpeed > 0) {
ctx.fillText(verticalSpeedTextString, verticalSpeedTextX, verticalSpeedLineY - fm20.actualBoundingBoxDescent - 1);
} else {
ctx.fillText(verticalSpeedTextString, verticalSpeedTextX, verticalSpeedLineY + fm20.actualBoundingBoxAscent + 1);
}
}
}
// For testing only
var speedInc = 0.1;
var headingInc = 0.1;
var altitudeInc = 1;
var verticalSpeedInc = 10;
var rollInc = 0.1;
function animatePFD() {
if (speed === undefined) {
speed = 0;
targetSpeed = 200;
} else if (speed <= 0) {
speedInc = 0.1;
} else if (speed >= 350) {
speedInc = -0.1;
}
if (heading === undefined) {
heading = 0;
targetHeading = 180;
} else if (heading <= 0) {
headingInc = 0.1;
} else if (heading >= 360) {
headingInc = -0.1;
}
if (altitude === undefined) {
altitude = 0;
targetAltitude = 1000;
} else if (altitude <= 0) {
altitudeInc = 1;
} else if (altitude >= 45000) {
altitudeInc = -1;
}
if (verticalSpeed === undefined) {
verticalSpeed = -7000;
} else if (verticalSpeed <= -7000) {
verticalSpeedInc = 10;
} else if (verticalSpeed >= 7000) {
verticalSpeedInc = -10;
}
if (roll === undefined) {
roll = 45;
} else if (roll <= -45) {
rollInc = 0.1;
} else if (roll >= 45) {
rollInc = -0.1;
}
speed = speed + speedInc;
groundspeed = 150;
trueAirspeed = 130;
heading = heading + headingInc;
altitude = altitude + altitudeInc;
altitude = 39;
radioAltitude = 1720;
onSurface = 0;
verticalSpeed = verticalSpeed + verticalSpeedInc;
roll = roll + rollInc;
callsign = "BAW123G";
aircraftType = "A320";
windDirection = 85;
windSpeed = 15;
staticAirTemperature = 7;
verticalMode = 3;
lateralMode = 2;
autopilot = 1;
tcasMode = 1;
pressure = 1013;
//pressure = 1018;
}
function drawPFD() {
const canvas = document.getElementById("pfdCanvas");
const ctx = canvas.getContext("2d");
ctx.font = "20px Arial";
const fm20 = ctx.measureText("180");
ctx.font = "30px Arial";
const fm30 = ctx.measureText("180");
// Background
ctx.fillStyle = "black";
ctx.fillRect(0, 0, 1000, 1000);
// Airspeed indicator
drawAirspeedIndicator(ctx, fm20, fm30);
// Attitude indicator
drawAttitudeIndicator(ctx, fm20, fm30);
// Altimeter
drawAltitudeIndicator(ctx, fm20, fm30);
// Vertical speed indicator
drawVerticalSpeedIndicator(ctx, fm20, fm30);
// Heading indicator
drawHeadingIndicator(ctx, fm20, fm30);
// Pressure setting
drawPressure(ctx, fm20, fm30);
// Mach indicator
drawMachIndicator(ctx);
// Flight mode announciators
ctx.beginPath();
ctx.moveTo(220, 5);
ctx.lineTo(220, 140);
ctx.lineWidth = 1;
ctx.strokeStyle = "white";
ctx.stroke();
if (targetSpeed !== undefined) {
ctx.font = "30px Arial";
ctx.fillStyle = greenColor;
ctx.fillText("SPEED", 55, 50); // THR CLB is climbing?
} else if (toga) {
ctx.font = "30px Arial";
ctx.fillStyle = "white";
ctx.fillText("TOGA", 70, 50);
}
// FIXME:
//ctx.font = "30px Arial";
//ctx.fillStyle = "white";
//ctx.fillText("toga " + toga + " roll " + rollOut + " Surf " + onSurface + " wasSurf " + wasOnSurface60SecsAgo + " MS " + modelSpeed + " r/w Alt " + runwayAltitude, 50, 150);
var landing = false;
if (verticalMode !== undefined) {
ctx.font = "30px Arial";
ctx.fillStyle = greenColor;
if (verticalMode == 1) {
if (verticalSpeed !== undefined) {
if (verticalSpeed > 0) {
ctx.fillText("CLB", 290, 50);
} else if (verticalSpeed < 0) {
ctx.fillText("DES", 290, 50);
} else {
ctx.fillText("ALT", 290, 50);
}
} else if (targetAltitude !== undefined && altitude !== undefined) {
if (targetAltitude > altitude) {
ctx.fillText("CLB", 290, 50);
} else if (targetAltitude < altitude) {
ctx.fillText("DES", 290, 50);
} else {
ctx.fillText("ALT", 290, 50);
}
}
} else if (verticalMode == 2) {
ctx.fillText("ALT", 290, 50);
} else if (verticalMode == 3) {
if (rollOut) {
ctx.fillText("ROLL OUT", 370, 50);
landing = true;
} else if ((onSurface === 0) && (radioAltitude < 40)) {
ctx.fillText("FLARE", 390, 50);
landing = true;
} else if ((onSurface === 0) && (radioAltitude < 400)) {
ctx.fillText("LAND", 400, 50);
landing = true;
} else {
ctx.fillText("G/S", 290, 50);
}
}
}
if (!landing) {
ctx.beginPath();
ctx.moveTo(420, 5);
ctx.lineTo(420, 140);
ctx.lineWidth = 1;
ctx.strokeStyle = "white";
ctx.stroke();
}
ctx.beginPath();
ctx.moveTo(660, 5);
ctx.lineTo(660, 140);
ctx.lineWidth = 1;
ctx.strokeStyle = "white";
ctx.stroke();
if (lateralMode !== undefined) {
ctx.font = "30px Arial";
ctx.fillStyle = greenColor;
if (lateralMode == 1) {
ctx.fillText("NAV", 510, 50);
} else if (lateralMode == 2) {
if (!landing) {
ctx.fillText("LOC", 510, 50);
}
}
}
ctx.beginPath();
ctx.moveTo(850, 5);
ctx.lineTo(850, 140);
ctx.lineWidth = 1;
ctx.strokeStyle = "white";
ctx.stroke();
ctx.font = "30px Arial";
ctx.fillStyle = "white";
if (autopilot !== undefined) {
if (autopilot) {
if (verticalMode == 3) {
// We can't tell whether both APs are enabled - but they typically would be
ctx.fillText("AP1+2", 870, 40);
ctx.fillText("CAT3", 720, 40);
ctx.fillText("DUAL", 720, 85);
} else {
ctx.fillText("AP1", 890, 40);
}
}
}
ctx.fillText("1 FD 2", 870, 85);
if (((targetSpeed !== undefined) || autopilot) && !rollOut) {
ctx.fillText("A/THR", 870, 130);
}
// Aircraft callsign and type
ctx.fillStyle = "white";
ctx.font = "30px Arial";
if (callsign !== undefined) {
ctx.fillText(callsign, 20, 940);
}
ctx.font = "21px Arial";
if (aircraftType !== undefined) {
ctx.fillText(aircraftType, 20, 980);
}
const dataLeft = 750; // Aligned with QNH
const dataTop = 890;
// TCAS - As displayed on ND/ECAM
if (tcasMode === 1) {
ctx.fillStyle = orangeColor;
ctx.font = "21px Arial";
ctx.fillText("TCAS STBY", dataLeft, dataTop);
} else if (tcasMode === 2) {
ctx.fillStyle = "white";
ctx.font = "21px Arial";
ctx.fillText("TA ONLY", dataLeft, dataTop);
}
// Groundspeed and TAS as displayed on ND
if ((groundspeed !== undefined) || (trueAirspeed !== undefined)) {
ctx.font = "21px Arial";
if (groundspeed !== undefined) {
ctx.fillStyle = "white";
ctx.fillText("GS", dataLeft, dataTop + 30);
ctx.fillStyle = greenColor;
ctx.fillText(Math.round(groundspeed).toString(), dataLeft + 40, dataTop + 30);
}
if (trueAirspeed !== undefined) {
ctx.fillStyle = "white";
ctx.fillText("TAS", dataLeft + 90, dataTop + 30);
ctx.fillStyle = greenColor;
ctx.fillText(Math.round(trueAirspeed).toString(), dataLeft + 140, dataTop + 30);
}
}
// Wind speed and direction as displayed on ND
if ((windSpeed !== undefined) && (windDirection !== undefined)) {
const windText = pad(Math.round(windDirection), 3) + "/" + Math.round(windSpeed);
ctx.fillStyle = greenColor;
ctx.font = "21px Arial";
ctx.fillText(windText, dataLeft, dataTop + 60);
if (heading !== undefined) {
// An arrow showing wind direction relative to heading
var angle = (windDirection - heading) * Math.PI / 180;
ctx.save();
ctx.translate(dataLeft + 80, dataTop + 53);
ctx.rotate(angle);
ctx.strokeStyle = greenColor;
ctx.beginPath();
ctx.moveTo(0, -10);
ctx.lineTo(0, 10);
ctx.lineTo(-5, 7);
ctx.moveTo(0, 10);
ctx.lineTo(5, 7);
ctx.stroke();
ctx.restore();
}
}
// Static air temperature as displayed on lower ECAM
if (staticAirTemperature !== undefined) {
var satText;
if (staticAirTemperature > 0) {
satText = "+" + Math.round(staticAirTemperature);
} else {
satText = Math.round(staticAirTemperature).toString();
}
ctx.font = "21px Arial";
ctx.fillStyle = "white";
ctx.fillText("SAT", dataLeft, dataTop + 90);
ctx.fillStyle = greenColor;
ctx.fillText(satText, dataLeft + 60, dataTop + 90);
ctx.fillStyle = cyanColor;
ctx.fillText("\u00B0C", dataLeft + 100, dataTop + 90);
}
}
const canvas = document.getElementById('pfdCanvas');
canvas.onmousedown = pfdMouseDown;
canvas.onmouseup = pfdMouseUp;
canvas.onmousemove = pfdMouseMove;
canvas.onmouseleave = pfdMouseLeave;
canvas.onmouseenter = pfdMouseEnter;
var docMouseMove = null;
var docMouseUp = null;
var movePFD = false;
var scalePFD = false;
var posX = 0;
var posY = 0;
var canvasWidth = 0;
var canvasHeight = 0;
function pad(num, size) {
num = num.toString();
while (num.length < size) num = "0" + num;
return num;
}
function pfdMouseDown(e) {
e.preventDefault();
e.stopPropagation();
const r = canvas.getBoundingClientRect();
posX = r.left;
posY = r.top;
canvasWidth = r.width;
canvasHeight = r.height;
if ((e.offsetX > 0.95 * canvasWidth) && (e.offsetY > 0.95 * canvasHeight)) {
scalePFD = true;
} else {
movePFD = true;
}
}
function pfdMouseUp(e) {
e.preventDefault();
e.stopPropagation();
movePFD = false;
scalePFD = false;
pfdRestoreDocMouse();
}
function pfdMouseMove(e) {
e.preventDefault();
e.stopPropagation();
if (movePFD) {
posX = posX + e.movementX;
posY = posY + e.movementY;
canvas.style.position = "absolute";
canvas.style.left = posX + "px";
canvas.style.top = posY + "px";
} else if (scalePFD) {
canvasWidth = canvasWidth + e.movementX;
canvasHeight = canvasHeight + e.movementY;
const constrainedWidth = Math.max(canvasWidth, 250);
const constrainedHeight = Math.max(canvasHeight, 250);
canvas.style.width = constrainedWidth + "px";
canvas.style.height = constrainedHeight + "px";
}
}
function pfdMouseLeave(e) {
//movePFD = false;
//scalePFD = false;
if (scalePFD || movePFD) {
docMouseUp = document.onmouseup;
docMouseMove = document.onmousemove;
document.onmouseup = pfdMouseUp;
document.onmousemove = pfdMouseMove;
}
}
function pfdRestoreDocMouse() {
if (docMouseUp) {
document.onmouseup = docMouseUp;
document.onmousemove = docMouseMove;
docMouseDown = null;
docMouseMove = null;
}
}
function pfdMouseEnter(e) {
pfdRestoreDocMouse();
}