0e19ee7903
Co-authored-by: aider (gemini/gemini-2.5-pro) <aider@aider.chat>
628 lines
27 KiB
HTML
628 lines
27 KiB
HTML
<!DOCTYPE html>
|
|
<html lang="en">
|
|
<head>
|
|
<meta charset="UTF-8">
|
|
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
|
<title>METAR Practice</title>
|
|
<style>
|
|
body {
|
|
font-family: monospace;
|
|
padding: 2em;
|
|
}
|
|
#metar-display {
|
|
font-size: 1.2em;
|
|
margin-bottom: 1em;
|
|
min-height: 3em;
|
|
}
|
|
|
|
@media (max-width: 600px) {
|
|
#metar-display {
|
|
min-height: 6em;
|
|
}
|
|
}
|
|
button {
|
|
font-size: 1em;
|
|
padding: 0.5em 1em;
|
|
}
|
|
.highlight {
|
|
background-color: yellow;
|
|
}
|
|
#decoding-display {
|
|
margin-top: 1em;
|
|
padding: 1em;
|
|
border: 1px solid #ccc;
|
|
white-space: pre-wrap; /* Respect newlines for remarks */
|
|
}
|
|
</style>
|
|
</head>
|
|
<body>
|
|
<div id="app">
|
|
<h1>METAR Practice</h1>
|
|
|
|
<div id="metar-display"></div>
|
|
<button id="new-metar-btn">New METAR</button>
|
|
<button id="test-all-btn">Test All METARs</button>
|
|
<label style="margin-left: 1em;"><input type="checkbox" id="decode-all-checkbox"> Decode all</label>
|
|
<div id="decoding-display"></div>
|
|
<button id="next-btn" style="margin-top: 1em;">Next</button>
|
|
</div>
|
|
<script>
|
|
const metarDisplay = document.getElementById('metar-display');
|
|
const decodingDisplay = document.getElementById('decoding-display');
|
|
const newMetarBtn = document.getElementById('new-metar-btn');
|
|
const testAllBtn = document.getElementById('test-all-btn');
|
|
const nextBtn = document.getElementById('next-btn');
|
|
const decodeAllCheckbox = document.getElementById('decode-all-checkbox');
|
|
const metars = [];
|
|
|
|
let currentMetarSections = [];
|
|
let currentSectionIndex = 0;
|
|
|
|
// --- Decoding Functions ---
|
|
|
|
function decodeAirport(code, metarString) {
|
|
if (code === 'CYYC') return "CYYC: Calgary International Airport";
|
|
if (code === 'CYEG') return "CYEG: Edmonton International Airport";
|
|
if (code === 'CYXS') return "CYXS: Prince George Airport";
|
|
if (code === 'CYVR') return "CVYR: Vancouver International Airport";
|
|
if (code === 'CYBW') return "CYBW: Calgary/Springbank Airport";
|
|
console.log(`Unknown Airport code: ${code} in METAR: ${metarString}`);
|
|
return `${code}: Unknown Airport`;
|
|
}
|
|
|
|
function decodeTime(code) {
|
|
const day = code.substring(0, 2);
|
|
const hours = code.substring(2, 4);
|
|
const minutes = code.substring(4, 6);
|
|
return `${code}: On the ${day}th day of the month at ${hours}:${minutes} UTC`;
|
|
}
|
|
|
|
function decodeWind(code) {
|
|
if (code === '00000KT') return `${code}: Calm winds`;
|
|
const dir = code.substring(0, 3);
|
|
const speed = parseInt(code.substring(3, 5));
|
|
const gustMatch = code.match(/G(\d{2})/);
|
|
let text = `${code}: Wind from ${dir}° at ${speed} knots`;
|
|
if (gustMatch) {
|
|
text += `, gusting to ${parseInt(gustMatch[1])} knots`;
|
|
}
|
|
return text;
|
|
}
|
|
|
|
function decodeVisibility(code) {
|
|
const vis = code.replace('SM', '');
|
|
return `${code}: Visibility ${vis} statute miles`;
|
|
}
|
|
|
|
function decodeWeather(code, metarString) {
|
|
const originalCode = code;
|
|
const intensityMap = {
|
|
'-': 'Light ',
|
|
'+': 'Heavy '
|
|
};
|
|
const descriptorMap = {
|
|
'MI': 'Shallow ', 'PR': 'Partial ', 'BC': 'Patches of ', 'DR': 'Low drifting ',
|
|
'BL': 'Blowing ', 'SH': 'Showers of ', 'TS': 'Thunderstorm with ', 'FZ': 'Freezing '
|
|
};
|
|
const weatherMap = {
|
|
'DZ': 'drizzle', 'RA': 'rain', 'SN': 'snow', 'SG': 'snow grains',
|
|
'IC': 'ice crystals', 'PL': 'ice pellets', 'GR': 'hail', 'GS': 'small hail and/or snow pellets',
|
|
'UP': 'unknown precipitation', 'BR': 'mist', 'FG': 'fog', 'FU': 'smoke',
|
|
'VA': 'volcanic ash', 'DU': 'widespread dust', 'SA': 'sand', 'HZ': 'haze',
|
|
'PY': 'spray', 'PO': 'well-developed dust/sand whirls', 'SQ': 'squalls',
|
|
'FC': 'funnel cloud(s)', 'SS': 'sandstorm', 'DS': 'duststorm', 'TR': 'trace'
|
|
};
|
|
const precipitationTypes = ['DZ', 'RA', 'SN', 'SG', 'IC', 'PL', 'GR', 'GS', 'UP'];
|
|
|
|
let decoding = '';
|
|
let codeToParse = code;
|
|
|
|
if (codeToParse.startsWith('-') || codeToParse.startsWith('+')) {
|
|
decoding += intensityMap[codeToParse[0]];
|
|
codeToParse = codeToParse.substring(1);
|
|
}
|
|
|
|
let vicinity = '';
|
|
if (codeToParse.startsWith('VC')) {
|
|
vicinity = ' in the vicinity';
|
|
codeToParse = codeToParse.substring(2);
|
|
}
|
|
|
|
const chunks = codeToParse.match(/.{1,2}/g) || [];
|
|
let decodedChunks = [];
|
|
let hasPrecipitation = false;
|
|
|
|
chunks.forEach(chunk => {
|
|
if (descriptorMap[chunk]) {
|
|
decodedChunks.push(descriptorMap[chunk]);
|
|
} else if (weatherMap[chunk]) {
|
|
decodedChunks.push(weatherMap[chunk]);
|
|
} else {
|
|
console.log(`Unknown weather chunk: ${chunk} in code: ${code} in METAR: ${metarString}`);
|
|
decodedChunks.push(`unknown (${chunk})`);
|
|
}
|
|
if (precipitationTypes.includes(chunk)) {
|
|
hasPrecipitation = true;
|
|
}
|
|
});
|
|
|
|
if ((codeToParse.includes('TS') || codeToParse.includes('SH')) && !hasPrecipitation) {
|
|
decodedChunks.push('rain');
|
|
}
|
|
|
|
decoding += decodedChunks.join('');
|
|
|
|
return `${originalCode}: ${decoding.trim()}${vicinity}`;
|
|
}
|
|
|
|
function decodeClouds(code, metarString) {
|
|
const coverMap = {
|
|
'SKC': 'Sky clear',
|
|
'CLR': 'Sky clear',
|
|
'FEW': 'Few clouds',
|
|
'SCT': 'Scattered clouds',
|
|
'BKN': 'Broken clouds',
|
|
'OVC': 'Overcast clouds'
|
|
};
|
|
const coverType = code.substring(0, 3);
|
|
const cover = coverMap[coverType];
|
|
if (!cover) {
|
|
console.log(`Unknown cloud information: ${code} in METAR: ${metarString}`);
|
|
return `${code}: Unknown cloud information`;
|
|
}
|
|
|
|
if (coverType === 'SKC' || coverType === 'CLR') {
|
|
return `${code}: ${cover}`;
|
|
}
|
|
|
|
const alt = parseInt(code.substring(3, 6)) * 100;
|
|
return `${code}: ${cover} at ${alt.toLocaleString()} feet above ground`;
|
|
}
|
|
|
|
function decodeTempDew(code) {
|
|
const parts = code.split('/');
|
|
const temp = parts[0].startsWith('M') ? -parseInt(parts[0].substring(1)) : parseInt(parts[0]);
|
|
const dew = parts[1].startsWith('M') ? -parseInt(parts[1].substring(1)) : parseInt(parts[1]);
|
|
return `${code}: Temperature ${temp}°C, Dewpoint ${dew}°C`;
|
|
}
|
|
|
|
function decodeAltimeter(code) {
|
|
const alt = code.substring(1);
|
|
return `${code}: Altimeter ${alt.slice(0, 2)}.${alt.slice(2)} inches of mercury`;
|
|
}
|
|
|
|
function decodeSlp(code) {
|
|
const pressure = parseInt(code.substring(3));
|
|
const hpa = (pressure < 500 ? 1000 : 900) + (pressure / 10);
|
|
return `SLP${pressure}: Sea-level pressure ${hpa.toFixed(1)} hPa`;
|
|
}
|
|
|
|
function decodeCloudTypesRemark(code, metarString) {
|
|
const cloudTypes = {
|
|
'CI': 'Cirrus',
|
|
'CC': 'Cirrocumulus',
|
|
'CS': 'Cirrostratus',
|
|
'AC': 'Altocumulus',
|
|
'AS': 'Altostratus',
|
|
'NS': 'Nimbostratus',
|
|
'SC': 'Stratocumulus',
|
|
'ST': 'Stratus',
|
|
'CU': 'Cumulus',
|
|
'CB': 'Cumulonimbus'
|
|
};
|
|
|
|
let decodedParts = [];
|
|
const matches = code.matchAll(/([A-Z]{2})(\d)/g);
|
|
|
|
for (const match of matches) {
|
|
const cloudCode = match[1];
|
|
const oktas = match[2];
|
|
let cloudName = cloudTypes[cloudCode];
|
|
if (!cloudName) {
|
|
cloudName = `Unknown cloud type (${cloudCode})`;
|
|
console.log(`Unknown cloud type in remark: ${cloudCode} in METAR: ${metarString}`);
|
|
}
|
|
decodedParts.push(`${oktas}/8 ${cloudName}`);
|
|
}
|
|
|
|
return `${code}: ${decodedParts.join(', ')}`;
|
|
}
|
|
|
|
function decodeObscurationRemark(code) {
|
|
const weatherMap = {
|
|
'FG': 'Fog',
|
|
'BR': 'Mist',
|
|
'FU': 'Smoke',
|
|
'HZ': 'Haze',
|
|
'DU': 'Widespread dust',
|
|
'SA': 'Sand',
|
|
};
|
|
const weatherCode = code.substring(0, 2);
|
|
const oktas = code.substring(2);
|
|
const weatherName = weatherMap[weatherCode];
|
|
return `${code}: ${weatherName} obscuring ${oktas}/8 of the sky`;
|
|
}
|
|
|
|
function decodeRemarks(parts, metarString) {
|
|
let decoded = ["RMK: Remarks"];
|
|
const weatherRegex = /^([+-]|VC)?(MI|PR|BC|DR|BL|SH|TS|FZ|DZ|RA|SN|SG|IC|PL|GR|GS|UP|BR|FG|FU|VA|DU|SA|HZ|PY|PO|SQ|FC|SS|DS|TR)+$/;
|
|
const obscurationRegex = /^(FG|BR|FU|HZ|DU|SA)\d$/;
|
|
const cloudTypes = {
|
|
'CI': 'Cirrus',
|
|
'CC': 'Cirrocumulus',
|
|
'CS': 'Cirrostratus',
|
|
'AC': 'Altocumulus',
|
|
'AS': 'Altostratus',
|
|
'NS': 'Nimbostratus',
|
|
'SC': 'Stratocumulus',
|
|
'ST': 'Stratus',
|
|
'CU': 'Cumulus',
|
|
'CB': 'Cumulonimbus',
|
|
'TCU': 'Towering cumulus'
|
|
};
|
|
const specialRemarks = {
|
|
'FROIN': 'Frontal passage in vicinity',
|
|
'CONTRAILS': 'Contrails observed',
|
|
'VIRGA': 'Virga (precipitation not reaching the ground)',
|
|
'HALO': 'Halo phenomenon observed'
|
|
};
|
|
const directions = {
|
|
'N': 'North', 'NE': 'Northeast', 'E': 'East', 'SE': 'Southeast',
|
|
'S': 'South', 'SW': 'Southwest', 'W': 'West', 'NW': 'Northwest'
|
|
};
|
|
|
|
const decoders = [
|
|
// SLP
|
|
{
|
|
match: p => p[0].startsWith('SLP'),
|
|
decode: p => ({ consumed: 1, text: ` - ${decodeSlp(p[0])}` })
|
|
},
|
|
// Obscuration
|
|
{
|
|
match: p => obscurationRegex.test(p[0]),
|
|
decode: p => ({ consumed: 1, text: ` - ${decodeObscurationRemark(p[0])}` })
|
|
},
|
|
// Cloud types summary
|
|
{
|
|
match: p => p[0].match(/^([A-Z]{2}\d)+$/),
|
|
decode: (p, ms) => ({ consumed: 1, text: ` - ${decodeCloudTypesRemark(p[0], ms)}` })
|
|
},
|
|
// VIA
|
|
{
|
|
match: p => p[0] === 'VIA' && p.length > 1,
|
|
decode: p => ({ consumed: 2, text: ` - VIA ${p[1]}: relayed via ${p[1]}` })
|
|
},
|
|
// VIS
|
|
{
|
|
match: p => p[0] === 'VIS' && p.length > 2,
|
|
decode: p => ({ consumed: 3, text: ` - VIS ${p[1]} ${p[2]}: Sector visibility to the ${p[1]} of ${p[2]} statute miles` })
|
|
},
|
|
// VIRGA
|
|
{
|
|
match: p => p[0] === 'VIRGA',
|
|
decode: p => {
|
|
if (p.length > 1 && p[1] === 'ALQDS') {
|
|
return { consumed: 2, text: ` - VIRGA ALQDS: Virga in all quadrants` };
|
|
}
|
|
if (p.length > 1 && directions[p[1]]) {
|
|
return { consumed: 2, text: ` - VIRGA ${p[1]}: Virga to the ${directions[p[1]].toLowerCase()}` };
|
|
}
|
|
return { consumed: 1, text: ` - ${p[0]}: ${specialRemarks[p[0]]}` };
|
|
}
|
|
},
|
|
// CVCTV CLD EMBD
|
|
{
|
|
match: p => p[0] === 'CVCTV' && p.length > 2 && p[1] === 'CLD' && p[2] === 'EMBD',
|
|
decode: p => ({ consumed: 3, text: ` - CVCTV CLD EMBD: Convective cloud embedded in stratiform layer` })
|
|
},
|
|
// DENSITY ALT
|
|
{
|
|
match: p => p[0] === 'DENSITY' && p.length > 2 && p[1] === 'ALT' && p[2].endsWith('FT'),
|
|
decode: p => {
|
|
const altFeet = p[2];
|
|
const altitude = parseInt(altFeet.replace('FT', '')).toLocaleString();
|
|
return { consumed: 3, text: ` - DENSITY ALT ${altFeet}: Density altitude ${altitude} feet` };
|
|
}
|
|
},
|
|
// Cloud remarks
|
|
{
|
|
match: p => cloudTypes[p[0]] || p[0] === 'ACC',
|
|
decode: p => {
|
|
const cloudCode = p[0];
|
|
const cloudName = cloudTypes[cloudCode] || (cloudCode === 'ACC' ? 'Altocumulus Castellanus' : cloudCode);
|
|
let consumed = 1;
|
|
let text = ` - ${cloudCode}: ${cloudName}`;
|
|
|
|
if (p.length > 1) {
|
|
const mod = p[1];
|
|
if (mod === 'LENT') {
|
|
text = ` - ${cloudCode} LENT: ${cloudName} lenticularis`;
|
|
consumed = 2;
|
|
if (p.length > 2 && directions[p[2]]) {
|
|
text += ` to the ${directions[p[2]].toLowerCase()}`;
|
|
consumed = 3;
|
|
}
|
|
} else if (mod === 'ASOCTD') {
|
|
text = ` - ${cloudCode} ASOCTD: ${cloudName} associated`;
|
|
consumed = 2;
|
|
if (p.length > 3 && p[2] === '/' && p[3] === 'HALO') {
|
|
text = ` - ${cloudCode} ASOCTD / HALO: ${cloudName} associated with Halo phenomenon`;
|
|
consumed = 4;
|
|
} else if (p.length > 2 && directions[p[2]]) {
|
|
text += ` to the ${directions[p[2]].toLowerCase()}`;
|
|
consumed = 3;
|
|
}
|
|
} else if (mod === 'ALQDS') {
|
|
text = ` - ${cloudCode} ALQDS: ${cloudName} in all quadrants`;
|
|
consumed = 2;
|
|
} else if (mod === 'TR') {
|
|
text = ` - ${cloudCode} TR: ${cloudName} clouds are translucent (thin)`;
|
|
consumed = 2;
|
|
} else if (mod === 'DSNT' && p.length > 2 && directions[p[2]]) {
|
|
text = ` - ${cloudCode} DSNT ${p[2]}: ${cloudName} distant ${directions[p[2]].toLowerCase()}`;
|
|
consumed = 3;
|
|
} else if (directions[mod]) {
|
|
text = ` - ${cloudCode} ${mod}: ${cloudName} to the ${directions[mod].toLowerCase()}`;
|
|
consumed = 2;
|
|
if (p.length > 3 && p[2] === 'MOV' && directions[p[3]]) {
|
|
text += `, moving ${directions[p[3]].toLowerCase()}`;
|
|
consumed = 4;
|
|
}
|
|
}
|
|
}
|
|
return { consumed, text };
|
|
}
|
|
},
|
|
// Simple remarks
|
|
{
|
|
match: p => specialRemarks[p[0]],
|
|
decode: p => ({ consumed: 1, text: ` - ${p[0]}: ${specialRemarks[p[0]]}` })
|
|
},
|
|
// Weather
|
|
{
|
|
match: p => weatherRegex.test(p[0]),
|
|
decode: (p, ms) => ({ consumed: 1, text: ` - ${decodeWeather(p[0], ms)}` })
|
|
},
|
|
];
|
|
|
|
let i = 0;
|
|
while (i < parts.length) {
|
|
const remainingParts = parts.slice(i);
|
|
let result = null;
|
|
|
|
for (const decoder of decoders) {
|
|
if (decoder.match(remainingParts)) {
|
|
result = decoder.decode(remainingParts, metarString);
|
|
break;
|
|
}
|
|
}
|
|
|
|
if (result) {
|
|
if (result.text) decoded.push(result.text);
|
|
i += result.consumed;
|
|
} else if (parts[i] === '/') {
|
|
i++; // separator, ignore
|
|
} else {
|
|
console.log(`Other remark: ${parts[i]} in METAR: ${metarString}`);
|
|
decoded.push(` - ${parts[i]}: Other remark`);
|
|
i++;
|
|
}
|
|
}
|
|
return decoded.join('\n');
|
|
}
|
|
|
|
function generateMetarSections(metarString) {
|
|
const sections = [];
|
|
const parts = metarString.split(/\s+/).filter(Boolean);
|
|
let index = 0;
|
|
|
|
// Section 1: Airport, Time, Auto
|
|
let airportTimeAutoRaw = [];
|
|
let airportTimeAutoDecoded = [];
|
|
|
|
if (parts[index] && parts[index].length === 4 && parts[index].match(/^[A-Z][A-Z0-9]{3}$/)) {
|
|
airportTimeAutoRaw.push(parts[index]);
|
|
airportTimeAutoDecoded.push(decodeAirport(parts[index], metarString));
|
|
index++;
|
|
}
|
|
if (parts[index] && parts[index].endsWith('Z')) {
|
|
airportTimeAutoRaw.push(parts[index]);
|
|
airportTimeAutoDecoded.push(decodeTime(parts[index]));
|
|
index++;
|
|
}
|
|
if (parts[index] && parts[index] === 'AUTO') {
|
|
airportTimeAutoRaw.push(parts[index]);
|
|
airportTimeAutoDecoded.push("AUTO: Fully automated observation");
|
|
index++;
|
|
}
|
|
if (airportTimeAutoRaw.length > 0) {
|
|
sections.push({
|
|
raw: airportTimeAutoRaw.join(' '),
|
|
decoded: airportTimeAutoDecoded.join('\n')
|
|
});
|
|
}
|
|
|
|
// Find remarks
|
|
const rmkIndex = parts.indexOf('RMK');
|
|
const mainParts = rmkIndex !== -1 ? parts.slice(index, rmkIndex) : parts.slice(index);
|
|
|
|
// Intermediate sections
|
|
mainParts.forEach(part => {
|
|
if (part.endsWith('KT')) {
|
|
sections.push({ raw: part, decoded: decodeWind(part) });
|
|
} else if (part.endsWith('SM') || part.match(/^\d+$/) || part.match(/^\d\/\dSM$/)) {
|
|
sections.push({ raw: part, decoded: decodeVisibility(part) });
|
|
} else if (part.match(/^([+-]|VC)?(MI|PR|BC|DR|BL|SH|TS|FZ|DZ|RA|SN|SG|IC|PL|GR|GS|UP|BR|FG|FU|VA|DU|SA|HZ|PY|PO|SQ|FC|SS|DS|TR)+$/)) {
|
|
sections.push({ raw: part, decoded: decodeWeather(part, metarString) });
|
|
} else if (part.match(/^(SKC|CLR|FEW|SCT|BKN|OVC)/)) {
|
|
sections.push({ raw: part, decoded: decodeClouds(part, metarString) });
|
|
} else if (part.match(/^(M?\d{2})\/(M?\d{2})$/)) {
|
|
sections.push({ raw: part, decoded: decodeTempDew(part) });
|
|
} else if (part.startsWith('A') && part.length === 5 && !isNaN(part.substring(1))) {
|
|
sections.push({ raw: part, decoded: decodeAltimeter(part) });
|
|
}
|
|
});
|
|
|
|
// Remarks section
|
|
if (rmkIndex !== -1) {
|
|
const remarkParts = parts.slice(rmkIndex + 1);
|
|
sections.push({
|
|
raw: `RMK ${remarkParts.join(' ')}`,
|
|
decoded: decodeRemarks(remarkParts, metarString)
|
|
});
|
|
}
|
|
|
|
return sections;
|
|
}
|
|
|
|
function displayNewMetar() {
|
|
if (metars.length === 0) {
|
|
metarDisplay.textContent = 'No METARs loaded.';
|
|
return;
|
|
}
|
|
const randomIndex = Math.floor(Math.random() * metars.length);
|
|
const metarString = metars[randomIndex];
|
|
|
|
currentMetarSections = generateMetarSections(metarString);
|
|
currentSectionIndex = 0;
|
|
updateDisplay();
|
|
}
|
|
|
|
fetch('metars.txt')
|
|
.then(response => response.text())
|
|
.then(data => {
|
|
const lines = data.split('\n');
|
|
lines.forEach(line => {
|
|
if (line.includes('METAR')) {
|
|
const metarString = line.substring(line.indexOf('METAR') + 6).replace('=', '').trim();
|
|
if (metarString) {
|
|
metars.push(metarString);
|
|
}
|
|
}
|
|
});
|
|
console.log(`Loaded ${metars.length} METARs.`);
|
|
displayNewMetar(); // Display initial METAR
|
|
})
|
|
.catch(error => {
|
|
console.error('Error fetching or parsing metars.txt:', error)
|
|
metarDisplay.textContent = 'Could not load METARs.';
|
|
});
|
|
|
|
newMetarBtn.addEventListener('click', displayNewMetar);
|
|
decodeAllCheckbox.addEventListener('change', updateDisplay);
|
|
|
|
function updateDisplay() {
|
|
if (currentMetarSections.length === 0) return;
|
|
|
|
if (decodeAllCheckbox.checked) {
|
|
metarDisplay.innerHTML = currentMetarSections.map(s => s.raw).join(' ');
|
|
decodingDisplay.textContent = currentMetarSections.map(s => s.decoded).join('\n\n');
|
|
nextBtn.style.display = 'none';
|
|
} else {
|
|
// Build highlighted string
|
|
const parts = [];
|
|
currentMetarSections.forEach((section, index) => {
|
|
if (index === currentSectionIndex) {
|
|
parts.push(`<span class="highlight">${section.raw}</span>`);
|
|
} else {
|
|
parts.push(section.raw);
|
|
}
|
|
});
|
|
metarDisplay.innerHTML = parts.join(' ');
|
|
|
|
// Show decoding
|
|
decodingDisplay.textContent = currentMetarSections[currentSectionIndex].decoded;
|
|
|
|
// Show/hide next button
|
|
nextBtn.style.display = (currentSectionIndex < currentMetarSections.length - 1) ? 'inline-block' : 'none';
|
|
}
|
|
}
|
|
|
|
nextBtn.addEventListener('click', () => {
|
|
if (currentSectionIndex < currentMetarSections.length - 1) {
|
|
currentSectionIndex++;
|
|
updateDisplay();
|
|
}
|
|
});
|
|
|
|
function testAllMetars() {
|
|
console.log(`--- Starting test of ${metars.length} METARs ---`);
|
|
let failed = 0;
|
|
let withUnknowns = 0;
|
|
const originalConsoleLog = console.log;
|
|
const unknowns = {
|
|
airports: new Set(),
|
|
weatherChunks: new Set(),
|
|
cloudInfos: new Set(),
|
|
cloudTypesInRemark: new Set(),
|
|
otherRemarks: new Set(),
|
|
};
|
|
|
|
metars.forEach(metarString => {
|
|
const tempLogs = [];
|
|
console.log = (...args) => {
|
|
tempLogs.push(args.join(' '));
|
|
};
|
|
|
|
try {
|
|
const sections = generateMetarSections(metarString);
|
|
if (sections.length === 0 && metarString.length > 0) {
|
|
throw new Error("Parser produced no sections");
|
|
}
|
|
|
|
const hasUnknowns = tempLogs.some(msg => msg.includes('Unknown') || msg.includes('Other remark'));
|
|
if (hasUnknowns) {
|
|
withUnknowns++;
|
|
originalConsoleLog(`METAR with decoding issues: ${metarString}`);
|
|
tempLogs.forEach(msg => {
|
|
originalConsoleLog(` -> ${msg}`);
|
|
if (msg.startsWith('Unknown Airport code:')) {
|
|
const code = msg.split(' ')[3];
|
|
unknowns.airports.add(code);
|
|
} else if (msg.startsWith('Unknown weather chunk:')) {
|
|
const chunk = msg.split(' ')[3];
|
|
unknowns.weatherChunks.add(chunk);
|
|
} else if (msg.startsWith('Unknown cloud information:')) {
|
|
const code = msg.split(' ')[3];
|
|
unknowns.cloudInfos.add(code);
|
|
} else if (msg.startsWith('Unknown cloud type in remark:')) {
|
|
const code = msg.split(' ')[5];
|
|
unknowns.cloudTypesInRemark.add(code);
|
|
} else if (msg.startsWith('Other remark:')) {
|
|
const remark = msg.split(' ')[2];
|
|
unknowns.otherRemarks.add(remark);
|
|
}
|
|
});
|
|
}
|
|
|
|
} catch (e) {
|
|
console.error("Failed to parse:", metarString, e);
|
|
failed++;
|
|
}
|
|
});
|
|
|
|
console.log = originalConsoleLog;
|
|
const total = metars.length;
|
|
const clean = total - failed - withUnknowns;
|
|
|
|
console.log(`--- Test finished ---`);
|
|
console.log(`Total METARs: ${total}`);
|
|
console.log(`Clean decodes: ${clean} (${(clean/total*100).toFixed(1)}%)`);
|
|
console.log(`Decodes with issues: ${withUnknowns} (${(withUnknowns/total*100).toFixed(1)}%)`);
|
|
console.log(`Failed to parse: ${failed} (${(failed/total*100).toFixed(1)}%)`);
|
|
|
|
console.log(`\n--- Unknowns encountered ---`);
|
|
for (const key in unknowns) {
|
|
if (unknowns[key].size > 0) {
|
|
console.log(`${key}:`);
|
|
console.log([...unknowns[key]].sort());
|
|
}
|
|
}
|
|
}
|
|
|
|
testAllBtn.addEventListener('click', testAllMetars);
|
|
</script>
|
|
</body>
|
|
</html>
|
|
|