460 lines
12 KiB
HTML
460 lines
12 KiB
HTML
<?xml version="1.0" encoding="UTF-8" standalone="no"?>
|
|
<!DOCTYPE html PUBLIC "-//W3C//DTD XHTML 1.0 Transitional//EN" "http://www.w3.org/TR/xhtml1/DTD/xhtml1-transitional.dtd">
|
|
<html xmlns="http://www.w3.org/1999/xhtml">
|
|
<style>
|
|
|
|
#messages {
|
|
background-color:yellow;
|
|
font-size:3;
|
|
font-weight:bold;
|
|
line-height:140%;
|
|
}
|
|
|
|
@keyframes flash {
|
|
0% { background-color: yellow; }
|
|
100% { background-color: transparent; }
|
|
}
|
|
|
|
.flash-bg {
|
|
animation: flash 0.5s ease-in-out;
|
|
}
|
|
|
|
#status {
|
|
background-color: #FFFFFF;
|
|
border-radius: 15px;
|
|
font-family: 'andale mono', monospace;
|
|
font-size: 16pt;
|
|
width: 610px;
|
|
margin: 5px;
|
|
text-align: center;
|
|
line-height: 35px;
|
|
}
|
|
|
|
.flex-container {
|
|
display: flex;
|
|
flex-direction: row;
|
|
justify-content: center;
|
|
/*background-color: DodgerBlue;*/
|
|
}
|
|
|
|
.flex-container > div {
|
|
background-color: #f1f1f1;
|
|
border-radius: 15px;
|
|
font-family: 'andale mono', monospace;
|
|
font-size: x-large;
|
|
width: 300px;
|
|
margin: 5px;
|
|
text-align: center;
|
|
line-height: 40px;
|
|
}
|
|
|
|
</style>
|
|
<head>
|
|
<title>EGFH Info</title>
|
|
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
|
<script src="https://cdnjs.cloudflare.com/ajax/libs/paho-mqtt/1.0.1/mqttws31.js" type="text/javascript"></script>
|
|
|
|
<script type = "text/javascript">
|
|
|
|
function onConnectionLost(){
|
|
invalidateDisplay();
|
|
console.log("connection lost");
|
|
document.getElementById("status").style.backgroundColor = '#AA0000';
|
|
document.getElementById("status").innerHTML = "connection Lost, reloading";
|
|
connected_flag=0;
|
|
setTimeout(location.reload(), reloadDelay);
|
|
}
|
|
|
|
function onFailure(message) {
|
|
invalidateDisplay();
|
|
console.log("Failed");
|
|
document.getElementById("status").style.backgroundColor = '#AA0000';
|
|
document.getElementById("status").innerHTML = "connection Failed- Retrying";
|
|
setTimeout(MQTTconnect, reconnectTimeout);
|
|
}
|
|
|
|
function onMessageArrived(r_message){
|
|
if (r_message.destinationName == "weather/loop"){
|
|
console.log("Got LOOP " + r_message.payloadString);
|
|
loopObj = JSON.parse(r_message.payloadString);
|
|
if (loopObj.interval_minute) {
|
|
console.log("AVG Speed " + loopObj.windSpeed_knot);
|
|
console.log("AVG Dir " + loopObj.windDir);
|
|
console.log(loopObj);
|
|
lastAvgWind = Date.now();
|
|
avgWindValid = 1;
|
|
avgWindSpeed = Number(loopObj.windSpeed_knot).toFixed(0);
|
|
roundedWind = (Math.round(loopObj.windDir / 10) * 10).toFixed(0);
|
|
avgWindDir = ('000' + roundedWind).substr(-3);
|
|
|
|
avgWindGustSpeed = Number(loopObj.windGust_knot).toFixed(0);
|
|
roundedWind = (Math.round(loopObj.windGustDir / 10) * 10).toFixed(0);
|
|
avgWindGustDir = ('000' + roundedWind).substr(-3);
|
|
updateAvgWind();
|
|
|
|
} else {
|
|
if (loopObj.windSpeed_knot) {
|
|
lastWind = Date.now();
|
|
windValid = 1;
|
|
instantWindSpeed = Number(loopObj.windSpeed_knot).toFixed(0);
|
|
roundedWind = (Math.round(loopObj.windDir / 10) * 10).toFixed(0);
|
|
zeroFilledDir = ('000' + roundedWind).substr(-3);
|
|
updateWind();
|
|
}
|
|
if (loopObj.outTemp_C) {
|
|
console.log("LOOP OAT " + loopObj.outTemp_C);
|
|
myDiv = document.getElementById("OAT");
|
|
newHtml = Number(loopObj.outTemp_C).toFixed(0) + " °C";
|
|
if (newHtml != myDiv.innerHTML) {
|
|
myDiv.innerHTML = newHtml;
|
|
flashBackground(myDiv);
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
}
|
|
|
|
function getInitialArchive() {
|
|
fetch(`archive.php`)
|
|
.then(response => response.json())
|
|
.then(archive => {
|
|
let now = Math.floor(Date.now() / 1000);
|
|
if (now - archive.dateTime > 300) {
|
|
console.log("Archive record too old");
|
|
} else {
|
|
document.getElementById("OAT").innerHTML = Number((archive.outTemp - 32) * 5 / 9).toFixed(0) + " °C";
|
|
avgWindSpeed = (archive.windSpeed * 0.868976).toFixed(0);
|
|
avgWindGustSpeed = (archive.windGust * 0.868976).toFixed(0);
|
|
roundedWind = (Math.round(archive.windDir / 10) * 10).toFixed(0);
|
|
avgWindDir = ('000' + roundedWind).substr(-3);
|
|
roundedWind = (Math.round(archive.windGustDir / 10) * 10).toFixed(0);
|
|
avgWindGustDir = ('000' + roundedWind).substr(-3);
|
|
avgWindValid = 1;
|
|
updateAvgWind();
|
|
}
|
|
})
|
|
}
|
|
|
|
function updateWind() {
|
|
myDiv = document.getElementById("windSpeed");
|
|
if (isNaN(zeroFilledDir)) {
|
|
newHtml = "NIL";
|
|
} else {
|
|
newHtml = zeroFilledDir + "/" + instantWindSpeed;
|
|
}
|
|
if (newHtml != myDiv.innerHTML) {
|
|
myDiv.innerHTML = newHtml;
|
|
flashBackground(myDiv);
|
|
}
|
|
}
|
|
|
|
function updateAvgWind() {
|
|
myDiv = document.getElementById("avgWindSpeed");
|
|
if (isNaN(avgWindSpeed) || avgWindSpeed < 2) {
|
|
newHtml = "CALM";
|
|
} else {
|
|
newHtml = avgWindDir + "/" + avgWindSpeed;
|
|
}
|
|
|
|
if (newHtml != myDiv.innerHTML) {
|
|
myDiv.innerHTML = newHtml;
|
|
flashBackground(myDiv);
|
|
updateWindDirection(avgWindDir);
|
|
}
|
|
|
|
myDiv = document.getElementById("avgWindGust");
|
|
if (isNaN(avgWindSpeed) || avgWindSpeed < 2) {
|
|
newHtml = "CALM";
|
|
} else {
|
|
newHtml = avgWindGustDir + "/" + avgWindGustSpeed;
|
|
}
|
|
if (newHtml != myDiv.innerHTML) {
|
|
myDiv.innerHTML = newHtml;
|
|
flashBackground(myDiv);
|
|
}
|
|
|
|
windAvgValid = 1;
|
|
|
|
}
|
|
|
|
function invalidateDisplay() {
|
|
invalidateWind();
|
|
invalidateAvgWind();
|
|
invalidateOAT();
|
|
}
|
|
|
|
function invalidateOAT() {
|
|
document.getElementById("OAT").innerHTML = "XXX";
|
|
windValid = 0;
|
|
}
|
|
|
|
function invalidateWind() {
|
|
document.getElementById("windSpeed").innerHTML = "XXX/X";
|
|
windValid = 0;
|
|
}
|
|
|
|
function invalidateAvgWind() {
|
|
document.getElementById("avgWindSpeed").innerHTML = "XXX/X";
|
|
document.getElementById("avgWindGust").innerHTML = "XXX/X";
|
|
windAvgValid = 0;
|
|
}
|
|
|
|
function onConnect() {
|
|
|
|
connected_flag=1;
|
|
document.getElementById("status").style.backgroundColor = '#00AA00';
|
|
document.getElementById("status").innerHTML = "connected";
|
|
console.log("Connected to MQTT broker");
|
|
sub_topics()
|
|
|
|
}
|
|
|
|
function disconnect() {
|
|
|
|
if (connected_flag==1)
|
|
mqtt.disconnect();
|
|
|
|
}
|
|
|
|
function MQTTconnect() {
|
|
|
|
clean_sessions=true
|
|
var x=Math.floor(Math.random() * 10000);
|
|
var cname="wx-"+x;
|
|
mqtt = new Paho.MQTT.Client(host,cname);
|
|
|
|
var options = {
|
|
timeout: 3,
|
|
cleanSession: clean_sessions,
|
|
onSuccess: onConnect,
|
|
onFailure: onFailure,
|
|
};
|
|
|
|
mqtt.onConnectionLost = onConnectionLost;
|
|
mqtt.onMessageArrived = onMessageArrived;
|
|
|
|
mqtt.connect(options);
|
|
return false;
|
|
|
|
}
|
|
|
|
function sub_topics() {
|
|
|
|
if (connected_flag==0) {
|
|
console.log("Not Connected so can't subscribe");
|
|
return false;
|
|
}
|
|
|
|
var sqos=0;
|
|
var soptions= {
|
|
qos:sqos,
|
|
};
|
|
mqtt.subscribe(topic,soptions);
|
|
return false;
|
|
}
|
|
|
|
async function getPressure() {
|
|
|
|
const response = await fetch('/wlproxy.php?api=current/195562');
|
|
const names = await response.json();
|
|
lastPressure = Date.now();
|
|
pressureValid = 1;
|
|
QFERaw = names.contents.sensors[2].data[0].bar_absolute;
|
|
QFE = Number((QFERaw / 0.029529983071445).toFixed(0)) + qCorrection;
|
|
QNH = QFE + 11;
|
|
|
|
document.getElementById("QFE").textContent = "QFE: " + QFE;
|
|
document.getElementById("QNH").textContent = "QNH: " + QNH;
|
|
|
|
var ts = new Date(names.contents.sensors[0].data[0].ts * 1000);
|
|
console.log(names.contents.sensors); // Dump the whole JSON to console
|
|
}
|
|
|
|
function drawCompass() {
|
|
ctx.clearRect(0, 0, canvas.width, canvas.height);
|
|
|
|
// Draw runways
|
|
drawRunway(220, 120, 12, -20); // 22/04 runway moved to the left
|
|
drawRunway(280, 90, 8, 0, 40); // 10/28 runway moved down
|
|
|
|
// Draw wind direction arrow
|
|
if (avgWindValid == 1) {
|
|
drawArrow(windDirection);
|
|
}
|
|
}
|
|
|
|
function drawRunway(angle, length, width, xShift, yShift = 0) {
|
|
const radian = (angle - 90) * (Math.PI / 180); // Convert to radians
|
|
|
|
// Calculate runway endpoints with xShift (left/right) and yShift (up/down)
|
|
const xStart = centerX - length * Math.cos(radian) + xShift;
|
|
const yStart = centerY - length * Math.sin(radian) + yShift;
|
|
const xEnd = centerX + length * Math.cos(radian) + xShift;
|
|
const yEnd = centerY + length * Math.sin(radian) + yShift;
|
|
|
|
// Draw runway as a thick line
|
|
ctx.strokeStyle = "#333";
|
|
ctx.lineWidth = width;
|
|
ctx.beginPath();
|
|
ctx.moveTo(xStart, yStart);
|
|
ctx.lineTo(xEnd, yEnd);
|
|
ctx.stroke();
|
|
|
|
// Correctly assign runway numbers at each end
|
|
drawRunwayNumber(angle, xStart, yStart, radian, -20);
|
|
drawRunwayNumber(angle + 180, xEnd, yEnd, radian, 20);
|
|
}
|
|
|
|
function drawRunwayNumber(angle, x, y, radian, extension) {
|
|
const runwayNumber = Math.round(angle / 10) % 36; // Convert heading to runway number
|
|
|
|
// Move the number along the extended centerline
|
|
const xOffset = x + extension * Math.cos(radian);
|
|
const yOffset = y + extension * Math.sin(radian);
|
|
|
|
ctx.font = "18px Arial";
|
|
ctx.fillStyle = "black";
|
|
ctx.textAlign = "center";
|
|
ctx.textBaseline = "middle";
|
|
ctx.fillText(runwayNumber.toString().padStart(2, "0"), xOffset, yOffset);
|
|
}
|
|
|
|
function drawArrow(angle) {
|
|
const arrowLength = 80; // Extended to pass through center
|
|
const radian = (angle + 90) * (Math.PI / 180); // Adjusted for correct direction
|
|
|
|
// Compute start and end points for the arrow
|
|
const xStart = centerX - arrowLength * Math.cos(radian);
|
|
const yStart = centerY - arrowLength * Math.sin(radian);
|
|
const xEnd = centerX + arrowLength * Math.cos(radian);
|
|
const yEnd = centerY + arrowLength * Math.sin(radian);
|
|
|
|
// Draw main arrow line
|
|
ctx.strokeStyle = "red";
|
|
ctx.lineWidth = 3;
|
|
ctx.beginPath();
|
|
ctx.moveTo(xStart, yStart);
|
|
ctx.lineTo(xEnd, yEnd);
|
|
ctx.stroke();
|
|
|
|
// Draw arrowhead at the end
|
|
ctx.fillStyle = "red";
|
|
ctx.beginPath();
|
|
ctx.moveTo(xEnd, yEnd);
|
|
ctx.lineTo(
|
|
xEnd - 12 * Math.cos(radian - Math.PI / 6),
|
|
yEnd - 12 * Math.sin(radian - Math.PI / 6)
|
|
);
|
|
ctx.lineTo(
|
|
xEnd - 12 * Math.cos(radian + Math.PI / 6),
|
|
yEnd - 12 * Math.sin(radian + Math.PI / 6)
|
|
);
|
|
ctx.closePath();
|
|
ctx.fill();
|
|
}
|
|
|
|
function updateWindDirection(degrees) {
|
|
windDirection = degrees % 360;
|
|
drawCompass();
|
|
}
|
|
|
|
function flashBackground(element) {
|
|
element.classList.add("flash-bg");
|
|
setTimeout(() => element.classList.remove("flash-bg"), 500);
|
|
}
|
|
|
|
function statusLoop() {
|
|
|
|
var dt = new Date();
|
|
secs = (dt.getTime() - lastWind)/1000;
|
|
|
|
if (secs > 10 && windValid == 1) {
|
|
invalidateWind();
|
|
console.log("Invalidating instant wind due to late message")
|
|
document.getElementById("status").innerHTML = "Missing Wind message";
|
|
document.getElementById("status").style.backgroundColor = '#AA0000';
|
|
}
|
|
|
|
secs = (dt.getTime() - lastAvgWind)/1000;
|
|
|
|
if (secs > 150 && windAvgValid == 1) {
|
|
invalidateAvgWind();
|
|
console.log("Invalidating average wind due to late message")
|
|
document.getElementById("status").innerHTML = "Missing avg Wind message";
|
|
document.getElementById("status").style.backgroundColor = '#AA0000';
|
|
}
|
|
|
|
if (avgWindValid == 1 && windValid == 1) {
|
|
document.getElementById("status").style.backgroundColor = '#00AA00';
|
|
document.getElementById("status").innerHTML = "status: ok";
|
|
}
|
|
|
|
}
|
|
|
|
</script>
|
|
|
|
</head>
|
|
<body>
|
|
|
|
<h2><center>Swansea EGFH - Live Conditions</center></h2>
|
|
|
|
<div class="flex-container">
|
|
<div id="QNH" class="data">XXXX</div>
|
|
<div id="QFE" class="data">XXXX</div>
|
|
</div>
|
|
|
|
<div class="flex-container">
|
|
<div>Sfc Wind<div id="avgWindSpeed">XXX/XX</div></div>
|
|
<div>Inst Wind<div id="windSpeed">XXX/XX</div></div>
|
|
</div>
|
|
|
|
<div class="flex-container"><canvas id="compass" width="300" height="300"></canvas></div>
|
|
|
|
<div class="flex-container">
|
|
<div>2min Gust<div id="avgWindGust">1</div></div>
|
|
<div>OAT<div id="OAT">1</div></div>
|
|
</div>
|
|
|
|
<center>Do not rely on this information for flight safety</center>
|
|
|
|
<div class="flex-container">
|
|
<div id="status"></div>
|
|
</div>
|
|
|
|
<script>
|
|
var connected_flag=0
|
|
var mqtt;
|
|
var reconnectTimeout = 2000;
|
|
var reloadDelay = 5000;
|
|
var host="wss://wx.swansea-airport.wales/mqtt";
|
|
var qCorrection = 0; // Offset for QFE / QNH
|
|
var row=0;
|
|
var mcount=0;
|
|
var topic = "weather/#";
|
|
var windValid = 0;
|
|
var avgWindValid = 0;
|
|
var pressureValid = 0;
|
|
let lastWind = Date.now();
|
|
let lastAvgWind = Date.now();
|
|
let lastPressure = Date.now();
|
|
|
|
const canvas = document.getElementById("compass");
|
|
const ctx = canvas.getContext("2d");
|
|
const centerX = canvas.width / 2;
|
|
const centerY = canvas.height / 2;
|
|
let windDirection = 0; // Initial wind direction in degrees
|
|
|
|
invalidateDisplay();
|
|
MQTTconnect();
|
|
getPressure();
|
|
getInitialArchive();
|
|
drawCompass();
|
|
|
|
setInterval(statusLoop, 5000);
|
|
setInterval(getPressure, 600000);
|
|
|
|
</script>
|
|
</body>
|
|
</html>
|