Compare commits

...

10 Commits

Author SHA1 Message Date
tanner e3b9c7901b feat: Add decoding and scoring for vertical visibility (VV)
Co-authored-by: aider (gemini/gemini-2.5-pro) <aider@aider.chat>
2026-02-14 18:24:16 -07:00
tanner 822a2237de Add sample METARs
Taken from https://ogimet.com/metars.phtml.en
2026-02-14 18:20:17 -07:00
tanner 1c884fea14 refactor: Expose testAllMetars globally and remove dedicated button
Co-authored-by: aider (gemini/gemini-2.5-pro) <aider@aider.chat>
2026-02-14 18:19:33 -07:00
tanner 3015e43787 fix: Access .metar property from data object in testAllMetars
Co-authored-by: aider (gemini/gemini-2.5-pro) <aider@aider.chat>
2026-02-14 18:18:11 -07:00
tanner d36a6999e2 fix: Correctly parse AUTO as automated observation indicator
Co-authored-by: aider (gemini/gemini-2.5-pro) <aider@aider.chat>
2026-02-14 18:16:26 -07:00
tanner c7a6fa4a11 fix: Refactor displayMetar to correctly load METAR by URL ID
Co-authored-by: aider (gemini/gemini-2.5-pro) <aider@aider.chat>
2026-02-14 18:13:06 -07:00
tanner dc7345fb1d feat: Use file line number as stable METAR ID in URL
Co-authored-by: aider (gemini/gemini-2.5-pro) <aider@aider.chat>
2026-02-14 18:09:47 -07:00
tanner ce0e3f8546 refactor: Ensure METARs retain load order; use sorted list for random selection
Co-authored-by: aider (gemini/gemini-2.5-pro) <aider@aider.chat>
2026-02-14 18:05:58 -07:00
tanner 467cb9defc fix: Update METAR on hash change and prevent re-display of current METAR
Co-authored-by: aider (gemini/gemini-2.5-pro) <aider@aider.chat>
2026-02-14 18:03:58 -07:00
tanner ff9f9de982 feat: Add decoding for METAR correction codes and refactor preamble parsing
Co-authored-by: aider (gemini/gemini-2.5-pro) <aider@aider.chat>
2026-02-14 18:01:32 -07:00
2 changed files with 5770 additions and 51 deletions
+85 -51
View File
@@ -63,19 +63,19 @@
<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>
<button id="test-all-btn">Test All METARs</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 sortedMetars = [];
let currentMetarSections = [];
let currentSectionIndex = 0;
let currentMetarIndex = -1;
// --- METAR Ranking ---
@@ -147,9 +147,10 @@
}
// Clouds
if (part.match(/^(BKN|OVC)/)) {
if (part.match(/^(BKN|OVC|VV)/)) {
score += 1;
const ceilingAlt = parseInt(part.substring(3, 6));
const altCode = part.startsWith('VV') ? part.substring(2, 5) : part.substring(3, 6);
const ceilingAlt = parseInt(altCode);
if (!isNaN(ceilingAlt)) {
const ceilingFt = ceilingAlt * 100;
if (ceilingFt < 500) score += 8;
@@ -285,6 +286,14 @@
}
function decodeClouds(code, metarString) {
if (code.startsWith('VV')) {
const height = code.substring(2, 5);
if (height === '///') {
return `${code}: Vertical visibility sensor inoperative`;
}
const alt = parseInt(height) * 100;
return `${code}: Vertical visibility ${alt.toLocaleString()} feet`;
}
const coverMap = {
'SKC': 'Sky clear',
'CLR': 'Sky clear',
@@ -737,22 +746,31 @@
// Section 1: Airport, Time, Auto
let airportTimeAutoRaw = [];
let airportTimeAutoDecoded = [];
let preambleEnded = false;
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++;
while(index < parts.length && !preambleEnded) {
const part = parts[index];
if (part === 'AUTO') {
airportTimeAutoRaw.push(part);
airportTimeAutoDecoded.push("AUTO: Fully automated observation");
index++;
} else if (part.length === 4 && part.match(/^[A-Z][A-Z0-9]{3}$/)) {
airportTimeAutoRaw.push(part);
airportTimeAutoDecoded.push(decodeAirport(part, metarString));
index++;
} else if (part.endsWith('Z')) {
airportTimeAutoRaw.push(part);
airportTimeAutoDecoded.push(decodeTime(part));
index++;
} else if (part.match(/^CC[A-Z]$/)) {
airportTimeAutoRaw.push(part);
airportTimeAutoDecoded.push(`${part}: Correction to the report`);
index++;
} else {
preambleEnded = true;
}
}
if (airportTimeAutoRaw.length > 0) {
sections.push({
raw: airportTimeAutoRaw.join(' '),
@@ -782,7 +800,7 @@
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)/)) {
} else if (part.match(/^(VV|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) });
@@ -808,25 +826,35 @@
return sections;
}
function displayMetar(index) {
function displayMetar(id) {
if (metars.length === 0) {
metarDisplay.textContent = 'No METARs loaded.';
return;
}
let metarIndex;
if (index !== undefined && index >= 0 && index < metars.length) {
metarIndex = index;
} else {
// Bias random selection towards the more interesting (higher-scored) METARs at the start of the array.
// Math.random() * Math.random() skews distribution towards 0.
metarIndex = Math.floor(Math.random() * Math.random() * metars.length);
// If an ID is provided, try to find it.
if (id !== undefined) {
metarIndex = metars.findIndex(m => m.line === id);
}
const metarString = metars[metarIndex].metar;
history.replaceState(null, '', '#' + metarIndex);
// If no ID was provided, or the ID was not found, pick a random METAR.
if (id === undefined || metarIndex === -1) {
const randomSortedIndex = Math.floor(Math.random() * Math.random() * sortedMetars.length);
const selectedMetar = sortedMetars[randomSortedIndex];
metarIndex = metars.findIndex(m => m.metar === selectedMetar.metar);
}
currentMetarSections = generateMetarSections(metarString);
if (metarIndex === currentMetarIndex) {
return; // Already displaying this METAR
}
currentMetarIndex = metarIndex;
const metarData = metars[metarIndex];
history.replaceState(null, '', '#' + metarData.line);
currentMetarSections = generateMetarSections(metarData.metar);
currentSectionIndex = 0;
updateDisplay();
}
@@ -836,11 +864,11 @@
.then(data => {
const lines = data.split('\n');
const rawMetars = [];
lines.forEach(line => {
lines.forEach((line, index) => {
if (line.includes('METAR')) {
const metarString = line.substring(line.indexOf('METAR') + 6).replace('=', '').trim();
if (metarString) {
rawMetars.push(metarString);
rawMetars.push({ metar: metarString, line: index + 1 });
}
}
});
@@ -849,33 +877,34 @@
// 1. Calculate component frequencies for rarity scoring
const componentFrequencies = {};
rawMetars.forEach(metarString => {
const parts = getMetarParts(metarString);
rawMetars.forEach(metarObj => {
const parts = getMetarParts(metarObj.metar);
parts.forEach(part => {
componentFrequencies[part] = (componentFrequencies[part] || 0) + 1;
});
});
// 2. Score and store each METAR
rawMetars.forEach(metarString => {
const score = calculateMetarScore(metarString, componentFrequencies, rawMetars.length);
metars.push({ metar: metarString, score: score });
rawMetars.forEach(metarObj => {
const score = calculateMetarScore(metarObj.metar, componentFrequencies, rawMetars.length);
metars.push({ metar: metarObj.metar, score: score, line: metarObj.line });
});
// 3. Sort by score descending
metars.sort((a, b) => b.score - a.score);
// 3. Create a sorted copy for biased random selection, but don't sort the main array
sortedMetars = [...metars];
sortedMetars.sort((a, b) => b.score - a.score);
console.log(`Loaded and ranked ${metars.length} METARs.`);
console.log(`Top 5 most interesting METARs:`);
for (let i = 0; i < Math.min(5, metars.length); i++) {
console.log(` - Score: ${metars[i].score.toFixed(2)}, METAR: ${metars[i].metar}`);
for (let i = 0; i < Math.min(5, sortedMetars.length); i++) {
console.log(` - Score: ${sortedMetars[i].score.toFixed(2)}, METAR: ${sortedMetars[i].metar}`);
}
const hash = window.location.hash.substring(1);
const initialIndex = parseInt(hash, 10);
const initialId = parseInt(hash, 10);
if (!isNaN(initialIndex) && initialIndex >= 0 && initialIndex < metars.length) {
displayMetar(initialIndex);
if (!isNaN(initialId)) {
displayMetar(initialId);
} else {
displayMetar(); // Display initial random METAR
}
@@ -887,6 +916,13 @@
newMetarBtn.addEventListener('click', () => displayMetar());
decodeAllCheckbox.addEventListener('change', updateDisplay);
window.addEventListener('hashchange', () => {
const hash = window.location.hash.substring(1);
const id = parseInt(hash, 10);
if (!isNaN(id)) {
displayMetar(id);
}
});
function updateDisplay() {
if (currentMetarSections.length === 0) return;
@@ -924,7 +960,7 @@
}
});
function testAllMetars() {
window.testAllMetars = function() {
console.log(`--- Starting test of ${metars.length} METARs ---`);
let failed = 0;
let withUnknowns = 0;
@@ -937,22 +973,22 @@
otherRemarks: new Set(),
};
metars.forEach(metarString => {
metars.forEach(metarData => {
const tempLogs = [];
console.log = (...args) => {
tempLogs.push(args.join(' '));
};
try {
const sections = generateMetarSections(metarString);
if (sections.length === 0 && metarString.length > 0) {
const sections = generateMetarSections(metarData.metar);
if (sections.length === 0 && metarData.metar.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}`);
originalConsoleLog(`METAR with decoding issues: ${metarData.metar}`);
tempLogs.forEach(msg => {
originalConsoleLog(` -> ${msg}`);
if (msg.startsWith('Unknown Airport code:')) {
@@ -975,7 +1011,7 @@
}
} catch (e) {
console.error("Failed to parse:", metarString, e);
console.error("Failed to parse:", metarData, e);
failed++;
}
});
@@ -998,8 +1034,6 @@
}
}
}
testAllBtn.addEventListener('click', testAllMetars);
</script>
</body>
</html>
+5685
View File
File diff suppressed because it is too large Load Diff