578 lines
24 KiB
HTML
578 lines
24 KiB
HTML
<!DOCTYPE html>
|
|
<html>
|
|
<head>
|
|
<title>RLX METAR Cove</title>
|
|
|
|
<script src="https://ajax.googleapis.com/ajax/libs/jquery/3.3.1/jquery.min.js"></script>
|
|
<script src="https://code.jquery.com/ui/1.13.1/jquery-ui.js" integrity="sha256-6XMVI0zB8cRzfZjqKcD01PBsAy3FlDASrlC8SxCpInY=" crossorigin="anonymous"></script>
|
|
<link rel="stylesheet" href="https://code.jquery.com/ui/1.13.1/themes/smoothness/jquery-ui.css">
|
|
<script src="https://d3js.org/d3.v7.min.js"></script>
|
|
|
|
</head>
|
|
<body>
|
|
|
|
<style type="text/css">
|
|
#weatherGraph {
|
|
width: 100%;
|
|
height: 600px;
|
|
border: 1px solid black;
|
|
position: relative;
|
|
}
|
|
|
|
.icao-label {
|
|
text-orientation: horizontal;
|
|
font-size: 12px;
|
|
font-weight: bold;
|
|
fill: #333;
|
|
text-anchor: left;
|
|
/* Dominant-baseline helps center vertically */
|
|
dominant-baseline: middle;
|
|
}
|
|
|
|
.time-label { /* General class if needed */
|
|
font-size: 10px;
|
|
font-weight: bold;
|
|
fill: #333;
|
|
}
|
|
|
|
.time-day {
|
|
font-size: 10px;
|
|
fill: #333;
|
|
text-anchor: middle;
|
|
}
|
|
|
|
.time-hour {
|
|
font-size: 10px;
|
|
fill: #333;
|
|
text-anchor: middle;
|
|
}
|
|
|
|
.tooltip {
|
|
position: absolute;
|
|
display: none;
|
|
background: rgba(0, 0, 0, 0.8);
|
|
color: white;
|
|
padding: 5px;
|
|
border-radius: 3px;
|
|
pointer-events: none;
|
|
font-size: 12px;
|
|
}
|
|
|
|
#timeLabels {
|
|
/* Increased height for 4 rows of text (Zulu Date, Zulu Hour, EST Date, EST Hour) */
|
|
height: 75px; /* ADJUSTED */
|
|
width: 100%;
|
|
}
|
|
|
|
.legend {
|
|
display: flex;
|
|
flex-wrap: wrap;
|
|
gap: 10px;
|
|
margin: 10px 0 0 0;
|
|
width: 100%;
|
|
}
|
|
|
|
.legend-item {
|
|
display: inline-flex;
|
|
align-items: center;
|
|
gap: 5px;
|
|
}
|
|
|
|
.legend-color {
|
|
width: 20px;
|
|
height: 20px;
|
|
border: 1px solid #000;
|
|
}
|
|
</style>
|
|
|
|
<div id="metar">
|
|
<label for="start">Start Date/Time (Zulu):</label>
|
|
<input type="datetime-local" id="start" name="start" step="1">
|
|
<br><br>
|
|
<label for="end">End Date/Time (Zulu):</label>
|
|
<input type="datetime-local" id="end" name="end" step="1">
|
|
<button id="submitButton">Submit</button>
|
|
</div>
|
|
<div id="weatherGraph"></div>
|
|
<!-- Ensure the div height matches the CSS -->
|
|
<div id="timeLabels" style="width: 100%; height: 75px;"></div>
|
|
<div class="legend"></div>
|
|
|
|
<script>
|
|
function getUrlParams() {
|
|
const params = new URLSearchParams(window.location.search);
|
|
return {
|
|
start: params.get('start'),
|
|
end: params.get('end')
|
|
};
|
|
}
|
|
|
|
const startPicker = document.getElementById('start');
|
|
const endPicker = document.getElementById('end');
|
|
|
|
document.addEventListener('DOMContentLoaded', (event) => {
|
|
const urlParams = getUrlParams();
|
|
|
|
function adjustEndDate() {
|
|
if (startPicker.value) {
|
|
let startDate = new Date(startPicker.value + 'Z'); // Treat as Zulu
|
|
let endDate = endPicker.value ? new Date(endPicker.value + 'Z') : new Date(startDate); // Treat as Zulu
|
|
|
|
if (!endPicker.value || endDate < startDate) {
|
|
endDate = new Date(startDate);
|
|
}
|
|
|
|
// Keep same day/month/year relative to start if end is invalid or before start
|
|
if (endDate < startDate) {
|
|
endDate.setUTCDate(startDate.getUTCDate()); // Use UTC functions
|
|
endDate.setUTCMonth(startDate.getUTCMonth());
|
|
endDate.setUTCFullYear(startDate.getUTCFullYear());
|
|
}
|
|
// Format back to ISO string suitable for datetime-local, removing the 'Z'
|
|
let formattedEndDate = endDate.toISOString().slice(0, 19);
|
|
endPicker.value = formattedEndDate;
|
|
}
|
|
}
|
|
|
|
if (urlParams.start) {
|
|
const startDate = new Date(urlParams.start); // Assuming URL param is Zulu
|
|
// Check if date is valid before formatting
|
|
if (!isNaN(startDate)) {
|
|
startPicker.value = startDate.toISOString().slice(0, 19);
|
|
}
|
|
}
|
|
|
|
if (urlParams.end) {
|
|
const endDate = new Date(urlParams.end); // Assuming URL param is Zulu
|
|
if (!isNaN(endDate)) {
|
|
endPicker.value = endDate.toISOString().slice(0, 19);
|
|
}
|
|
}
|
|
|
|
if (startPicker.value && !endPicker.value) {
|
|
adjustEndDate(); // Adjust end date if start is set but end isn't
|
|
}
|
|
|
|
|
|
if (urlParams.start && urlParams.end) {
|
|
getValues();
|
|
}
|
|
|
|
startPicker.addEventListener('change', adjustEndDate);
|
|
});
|
|
|
|
|
|
function getmetars(startDateStr, endDateStr, startZulu, endZulu) {
|
|
const graphContainer = document.getElementById('weatherGraph');
|
|
const timeLabelsContainer = document.getElementById('timeLabels');
|
|
const legendContainer = document.querySelector('.legend');
|
|
const startTime = startZulu;
|
|
const endTime = endZulu;
|
|
|
|
// --- Clear previous state ---
|
|
graphContainer.innerHTML = "";
|
|
timeLabelsContainer.innerHTML = "";
|
|
// legendContainer.innerHTML = ""; // Clear legend at start of function or before calling generateLegend
|
|
|
|
// --- Date Validation ---
|
|
if (!startTime || !endTime || isNaN(startTime) || isNaN(endTime) || startTime >= endTime) {
|
|
graphContainer.innerHTML = "<p>Error: Invalid date range selected.</p>";
|
|
generateLegend(); // Show legend even on error
|
|
return;
|
|
}
|
|
|
|
// --- Fetch Data ---
|
|
$.getJSON(`lsr.php?metars=true&start=${startDateStr}&end=${endDateStr}`, function(weatherdata) {
|
|
// Ensure weatherdata is an array
|
|
weatherdata = weatherdata || [];
|
|
|
|
const icaos = [...new Set(weatherdata.map(data => data.icao))].sort();
|
|
const stationNames = {};
|
|
// Pre-process data: Convert times and group by ICAO
|
|
const dataByIcao = {};
|
|
icaos.forEach(icao => { dataByIcao[icao] = []; }); // Initialize empty array for each ICAO
|
|
|
|
weatherdata.forEach(data => {
|
|
if (data.icao && data.obtime) { // Basic check for valid data
|
|
stationNames[data.icao] = data.stationname;
|
|
data.obtimeDate = new Date(data.obtime); // Convert string time to Date object
|
|
if (dataByIcao[data.icao]) { // Add to the correct ICAO group
|
|
dataByIcao[data.icao].push(data);
|
|
}
|
|
}
|
|
});
|
|
|
|
// Sort data within each ICAO group by time (important for efficient search)
|
|
icaos.forEach(icao => {
|
|
dataByIcao[icao].sort((a, b) => a.obtimeDate - b.obtimeDate);
|
|
});
|
|
|
|
// --- Calculate graph dimensions ---
|
|
const totalMillis = endTime - startTime;
|
|
const hours = totalMillis / (1000 * 60 * 60);
|
|
if (hours <= 0) {
|
|
graphContainer.innerHTML = "<p>Error: End time must be after start time.</p>";
|
|
generateLegend();
|
|
return;
|
|
}
|
|
const containerWidth = graphContainer.clientWidth;
|
|
const containerHeight = graphContainer.clientHeight;
|
|
const hourWidth = containerWidth / hours;
|
|
const icaoHeight = icaos.length > 0 ? containerHeight / icaos.length : containerHeight;
|
|
const hourMillis = 1000 * 60 * 60;
|
|
|
|
// --- Create SVG & Tooltip ---
|
|
const svg = document.createElementNS("http://www.w3.org/2000/svg", "svg");
|
|
svg.setAttribute('width', containerWidth);
|
|
svg.setAttribute('height', containerHeight);
|
|
graphContainer.appendChild(svg);
|
|
|
|
const tooltip = document.createElement('div');
|
|
tooltip.className = 'tooltip';
|
|
document.body.appendChild(tooltip); // Append to body
|
|
|
|
// --- Draw Rectangles: Iterate through ICAOs and Time Slots ---
|
|
icaos.forEach((icao, index) => {
|
|
const y = index * icaoHeight;
|
|
const icaoSpecificData = dataByIcao[icao]; // Get pre-processed data for this ICAO
|
|
let dataPointer = 0; // Index to track position in sorted icaoSpecificData
|
|
|
|
// Draw horizontal line separator
|
|
const line = document.createElementNS("http://www.w3.org/2000/svg", "line");
|
|
line.setAttribute('x1', 0); line.setAttribute('y1', y);
|
|
line.setAttribute('x2', '100%'); line.setAttribute('y2', y);
|
|
line.setAttribute('stroke', '#e0e0e0'); // Lighter gray for less visual noise
|
|
svg.appendChild(line);
|
|
|
|
// Loop through each *hour slot* on the graph
|
|
for (let i = 0; i < Math.ceil(hours); i++) { // Use Math.ceil to cover partial last hour
|
|
const slotStartTimeMillis = startTime.getTime() + i * hourMillis;
|
|
const slotEndTimeMillis = slotStartTimeMillis + hourMillis;
|
|
const x = i * hourWidth;
|
|
|
|
// Find the *most relevant* observation for this time slot.
|
|
// Strategy: Use the *latest* observation whose time is *less than or equal to* the slot's END time.
|
|
// This represents the conditions reported that would influence this hour.
|
|
let relevantObservation = null;
|
|
// Advance pointer past observations before the current slot potentially starts
|
|
while (dataPointer < icaoSpecificData.length && icaoSpecificData[dataPointer].obtimeDate.getTime() < slotStartTimeMillis) {
|
|
dataPointer++;
|
|
}
|
|
// Check observations within or just before the slot
|
|
let searchIndex = dataPointer;
|
|
while (searchIndex < icaoSpecificData.length && icaoSpecificData[searchIndex].obtimeDate.getTime() < slotEndTimeMillis) {
|
|
relevantObservation = icaoSpecificData[searchIndex]; // Update with the latest found within range
|
|
searchIndex++;
|
|
}
|
|
// If no observation *within* the slot, check the one immediately preceding it (if pointer > 0)
|
|
if (relevantObservation === null && dataPointer > 0 && icaoSpecificData.length > 0) {
|
|
// Check the observation pointed to by dataPointer-1 (the last one before this slot started)
|
|
if (icaoSpecificData[dataPointer - 1].obtimeDate.getTime() < slotStartTimeMillis) {
|
|
// This observation occurred *before* the slot began, use it if nothing else found
|
|
// relevantObservation = icaoSpecificData[dataPointer - 1]; // Uncomment this line if you want the previous ob to fill the gap
|
|
}
|
|
}
|
|
|
|
|
|
let color;
|
|
let rawData = null;
|
|
let obTimeStr = null;
|
|
|
|
// Determine color based on whether an observation was found for this slot
|
|
if (relevantObservation) {
|
|
// An observation exists, use wxToColor to get Precip Color or Light Gray
|
|
color = wxToColor(relevantObservation.wx);
|
|
rawData = relevantObservation.raw;
|
|
obTimeStr = relevantObservation.obtime; // Use original time string for display
|
|
} else {
|
|
// NO observation record found relevant to this time slot
|
|
color = 'white'; // Missing observation
|
|
}
|
|
|
|
// Draw the rectangle for this slot
|
|
const rect = document.createElementNS("http://www.w3.org/2000/svg", "rect");
|
|
rect.setAttribute('x', x);
|
|
rect.setAttribute('y', y);
|
|
// Ensure width doesn't exceed SVG boundary
|
|
rect.setAttribute('width', Math.min(hourWidth, containerWidth - x));
|
|
rect.setAttribute('height', icaoHeight);
|
|
rect.setAttribute('fill', color);
|
|
|
|
// Add border to white/light gray for visibility
|
|
if (color === 'white' || color === '#f0f0f0') {
|
|
rect.setAttribute('stroke', '#cccccc');
|
|
rect.setAttribute('stroke-width', '1');
|
|
}
|
|
|
|
// --- Tooltip Logic ---
|
|
rect.setAttribute('data-icao', icao);
|
|
if (relevantObservation) {
|
|
// Tooltip for existing observation
|
|
rect.setAttribute('data-raw', rawData);
|
|
rect.setAttribute('data-time', obTimeStr);
|
|
rect.addEventListener('mouseover', function(e) {
|
|
tooltip.innerHTML = `<b>${this.getAttribute('data-icao')}</b> (${this.getAttribute('data-time')})<br>${this.getAttribute('data-raw')}`;
|
|
tooltip.style.display = 'block';
|
|
});
|
|
} else {
|
|
// Tooltip for missing observation slot
|
|
const slotStartTime = new Date(slotStartTimeMillis);
|
|
const approxTimeStr = slotStartTime.toISOString().slice(11, 16) + "Z"; // HH:MMZ
|
|
const approxDateStr = `${slotStartTime.getUTCMonth() + 1}/${slotStartTime.getUTCDate()}`;
|
|
rect.setAttribute('data-time-slot', `${approxDateStr} ${approxTimeStr}`);
|
|
rect.addEventListener('mouseover', function(e) {
|
|
tooltip.innerHTML = `<b>${this.getAttribute('data-icao')}</b><br>No observation found for<br>${this.getAttribute('data-time-slot')} hour`;
|
|
tooltip.style.display = 'block';
|
|
});
|
|
}
|
|
// Common tooltip positioning and mouseout
|
|
rect.addEventListener('mousemove', function(e) {
|
|
// Position relative to page, offset from cursor
|
|
tooltip.style.left = (e.pageX + 15) + 'px';
|
|
tooltip.style.top = (e.pageY + 15) + 'px';
|
|
});
|
|
rect.addEventListener('mouseout', function() {
|
|
tooltip.style.display = 'none';
|
|
});
|
|
|
|
svg.appendChild(rect);
|
|
|
|
} // End of hour slot loop (i)
|
|
}); // End of ICAO loop (index)
|
|
|
|
// --- Draw ICAO Labels (Draw AFTER rectangles) ---
|
|
icaos.forEach((icao, index) => {
|
|
const y = index * icaoHeight;
|
|
const text = document.createElementNS("http://www.w3.org/2000/svg", "text");
|
|
text.textContent = icao; // Or stationNames[icao]
|
|
text.setAttribute('x', 5);
|
|
text.setAttribute('y', y + icaoHeight / 2); // Vertically center
|
|
text.setAttribute('class', 'icao-label'); // Ensure class with dominant-baseline is applied
|
|
svg.appendChild(text);
|
|
});
|
|
|
|
// --- Add final horizontal line at the bottom ---
|
|
if (icaos.length > 0) {
|
|
const finalY = containerHeight; // Bottom of the SVG
|
|
const finalLine = document.createElementNS("http://www.w3.org/2000/svg", "line");
|
|
finalLine.setAttribute('x1', 0); finalLine.setAttribute('y1', finalY);
|
|
finalLine.setAttribute('x2', '100%'); finalLine.setAttribute('y2', finalY);
|
|
finalLine.setAttribute('stroke', '#e0e0e0');
|
|
svg.appendChild(finalLine);
|
|
}
|
|
|
|
|
|
// --- Add Time Scale (No changes needed here) ---
|
|
const timeLabelsSVG = document.createElementNS("http://www.w3.org/2000/svg", "svg");
|
|
// ... (rest of time scale code is identical to previous correct version) ...
|
|
timeLabelsSVG.setAttribute('width', '100%');
|
|
timeLabelsSVG.setAttribute('height', '75'); // Match the container height
|
|
timeLabelsContainer.appendChild(timeLabelsSVG);
|
|
const yZuluDate = 15;
|
|
const yZuluHour = 30;
|
|
const yEstDate = 45;
|
|
const yEstHour = 60;
|
|
const estOffsetMillis = -5 * 60 * 60 * 1000; // UTC-5 in milliseconds
|
|
for (let i = 0; i <= hours; i++) { // Use <= hours to get label at the end time too
|
|
const x = i * hourWidth;
|
|
const tickTimeZulu = new Date(startTime.getTime() + i * hourMillis);
|
|
// --- Zulu Labels ---
|
|
const zuluDayText = document.createElementNS("http://www.w3.org/2000/svg", "text");
|
|
zuluDayText.textContent = `${tickTimeZulu.getUTCMonth() + 1}/${tickTimeZulu.getUTCDate()}`;
|
|
zuluDayText.setAttribute('x', x); zuluDayText.setAttribute('y', yZuluDate);
|
|
zuluDayText.setAttribute('class', 'time-day'); timeLabelsSVG.appendChild(zuluDayText);
|
|
const zuluHourText = document.createElementNS("http://www.w3.org/2000/svg", "text");
|
|
zuluHourText.textContent = `${String(tickTimeZulu.getUTCHours()).padStart(2, '0')}Z`;
|
|
zuluHourText.setAttribute('x', x); zuluHourText.setAttribute('y', yZuluHour);
|
|
zuluHourText.setAttribute('class', 'time-hour'); timeLabelsSVG.appendChild(zuluHourText);
|
|
// --- EST Labels (UTC-5) ---
|
|
const tickTimeEst = new Date(tickTimeZulu.getTime() + estOffsetMillis);
|
|
const estDayText = document.createElementNS("http://www.w3.org/2000/svg", "text");
|
|
estDayText.textContent = `${tickTimeEst.getUTCMonth() + 1}/${tickTimeEst.getUTCDate()}`;
|
|
estDayText.setAttribute('x', x); estDayText.setAttribute('y', yEstDate);
|
|
estDayText.setAttribute('class', 'time-day'); timeLabelsSVG.appendChild(estDayText);
|
|
const estHourText = document.createElementNS("http://www.w3.org/2000/svg", "text");
|
|
estHourText.textContent = `${String(tickTimeEst.getUTCHours()).padStart(2, '0')}E`;
|
|
estHourText.setAttribute('x', x); estHourText.setAttribute('y', yEstHour);
|
|
estHourText.setAttribute('class', 'time-hour'); timeLabelsSVG.appendChild(estHourText);
|
|
}
|
|
|
|
|
|
}).fail(function(jqXHR, textStatus, errorThrown) {
|
|
console.error("Failed to fetch METAR data:", textStatus, errorThrown);
|
|
graphContainer.innerHTML = `<p>Error fetching METAR data. Status: ${textStatus}.</p>`;
|
|
generateLegend(); // Show legend even on failure
|
|
}).always(function() {
|
|
// Final check to generate legend if it hasn't been done
|
|
if (!legendContainer.hasChildNodes()) {
|
|
generateLegend();
|
|
}
|
|
});
|
|
|
|
// Call generateLegend immediately so it shows while data loads/if errors occur before AJAX completes
|
|
generateLegend();
|
|
}
|
|
|
|
function wxToColor(wx) {
|
|
// --- This function is called ONLY when an observation record exists ---
|
|
// It determines the color based on the content of the 'wx' field.
|
|
|
|
// 1. Normalize the input string (handle null/undefined from DB field)
|
|
const normalizedWx = String(wx || "").toLowerCase().trim(); // Default to "" if wx is null/undefined
|
|
|
|
// 2. Handle cases indicating no precipitation or only other phenomena
|
|
if (normalizedWx === "") {
|
|
// Catches empty strings, null, undefined from the DB field for an existing record
|
|
return '#f0f0f0'; // Very light gray for No Reported WX / Clear
|
|
}
|
|
|
|
// 3. Check for specific precipitation types with intensity
|
|
// Helper functions (no changes needed)
|
|
const checkWx = (pattern) => {
|
|
const regex = new RegExp(`(^|\\s)[\\+\\-]?${pattern}(\\s|$)`);
|
|
return regex.test(normalizedWx);
|
|
};
|
|
const getIntensity = (pattern) => {
|
|
if (new RegExp(`(^|\\s)\\+${pattern}(\\s|$)`).test(normalizedWx)) return '+';
|
|
if (new RegExp(`(^|\\s)\\-${pattern}(\\s|$)`).test(normalizedWx)) return '-';
|
|
if (new RegExp(`(^|\\s)${pattern}(\\s|$)`).test(normalizedWx)) return '';
|
|
return null;
|
|
};
|
|
|
|
// Precipitation Checks (order matters)
|
|
let intensity = getIntensity('fzra|fzdz'); // Freezing Precip
|
|
if (intensity !== null) {
|
|
if (intensity === '+') return '#4b0082'; // Heavy FZRA/FZDZ
|
|
if (intensity === '-') return '#dda0dd'; // Light FZRA/FZDZ
|
|
return '#800080'; // Moderate FZRA/FZDZ
|
|
}
|
|
if (checkWx('blsn')) return 'red'; // Blowing Snow
|
|
intensity = getIntensity('sn'); // Snow
|
|
if (intensity !== null) {
|
|
if (intensity === '+') return '#00008b'; // Heavy SN
|
|
if (intensity === '-') return '#b0e0e6'; // Light SN
|
|
return '#4682b4'; // Moderate SN
|
|
}
|
|
intensity = getIntensity('pl|pe'); // Ice Pellets
|
|
if (intensity !== null) {
|
|
return 'pink'; // All PL intensity as pink
|
|
}
|
|
intensity = getIntensity('ra|dz'); // Rain/Drizzle
|
|
if (intensity !== null) {
|
|
if (intensity === '+') return '#006400'; // Heavy RA/DZ
|
|
if (intensity === '-') return '#90ee90'; // Light RA/DZ
|
|
return '#228b22'; // Moderate RA/DZ
|
|
}
|
|
if (checkWx('up')) return '#dda0dd'; // Unknown Precip
|
|
|
|
// 4. If the 'wx' field had content, but it didn't match any known precipitation,
|
|
// it represents other reported phenomena (FG, HZ, BR, clouds, etc.).
|
|
return '#f0f0f0'; // Very light gray for Other Reported WX (non-precip)
|
|
}
|
|
function getValues() {
|
|
let startDateStr = startPicker.value;
|
|
let endDateStr = endPicker.value;
|
|
|
|
if (!startDateStr || !endDateStr) {
|
|
alert("Please select both a start and end date/time.");
|
|
return;
|
|
}
|
|
|
|
// Convert input strings (assumed local but representing Zulu) to Zulu Date objects
|
|
// Appending 'Z' tells the Date constructor to parse it as UTC/Zulu
|
|
let startZulu = new Date(startDateStr + 'Z');
|
|
let endZulu = new Date(endDateStr + 'Z');
|
|
|
|
// Basic validation
|
|
if (isNaN(startZulu) || isNaN(endZulu)) {
|
|
alert("Invalid date format selected. Please check your input.");
|
|
console.error("Invalid Date object created:", startDateStr, endDateStr, startZulu, endZulu);
|
|
return;
|
|
}
|
|
if (startZulu >= endZulu) {
|
|
alert("Start date must be before the end date.");
|
|
return;
|
|
}
|
|
|
|
console.log("Raw Inputs:", startDateStr, endDateStr);
|
|
console.log("Parsed Zulu Dates:", startZulu, endZulu);
|
|
|
|
// Pass both the original strings (for PHP) and the Date objects (for JS)
|
|
getmetars(startDateStr, endDateStr, startZulu, endZulu);
|
|
}
|
|
|
|
document.getElementById('submitButton').addEventListener('click', getValues);
|
|
|
|
function generateLegend() {
|
|
const legendContainer = document.querySelector('.legend');
|
|
legendContainer.innerHTML = ''; // Clear previous legend items
|
|
|
|
// Define the very light gray color
|
|
const noPrecipColor = '#f0f0f0';
|
|
|
|
const legendData = [
|
|
// Grouped by Precipitation Type
|
|
{ group: 'Freezing', items: [
|
|
{ label: '-FZRA/DZ', color: '#dda0dd'},
|
|
{ label: 'FZRA/DZ', color: '#800080'},
|
|
{ label: '+FZRA/DZ', color: '#4b0082'}
|
|
]},
|
|
{ group: 'Snow', items: [
|
|
{ label: '-SN', color: '#b0e0e6'},
|
|
{ label: 'SN', color: '#4682b4'},
|
|
{ label: '+SN', color: '#00008b'}
|
|
]},
|
|
{ group: 'Ice Pellets', items: [
|
|
{ label: 'PL/PE', color: 'pink'}
|
|
]},
|
|
{ group: 'Rain/Drizzle', items: [
|
|
{ label: '-RA/DZ', color: '#90ee90'},
|
|
{ label: 'RA/DZ', color: '#228b22'},
|
|
{ label: '+RA/DZ', color: '#006400'}
|
|
]},
|
|
// Other Phenomena
|
|
{ group: 'Other WX', items: [
|
|
{ label: 'BLSN', color: 'red'},
|
|
{ label: 'UP', color: '#dda0dd'}
|
|
]},
|
|
// Status/Misc
|
|
{ group: 'Status', items: [
|
|
// Use the specific very light gray color here
|
|
{ label: 'No Precip / Other WX', color: noPrecipColor },
|
|
{ label: 'Missing Ob', color: 'white' }
|
|
]}
|
|
];
|
|
|
|
legendData.forEach(groupData => {
|
|
groupData.items.forEach(item => {
|
|
const legendItem = document.createElement('div');
|
|
legendItem.className = 'legend-item';
|
|
|
|
const colorBox = document.createElement('div');
|
|
colorBox.className = 'legend-color';
|
|
colorBox.style.backgroundColor = item.color;
|
|
// Add border to white and very light gray boxes so they are visible
|
|
if (item.color === 'white' || item.color === noPrecipColor) {
|
|
colorBox.style.borderColor = '#ccc'; // Use a light border for contrast
|
|
} else {
|
|
colorBox.style.borderColor = '#000'; // Keep black border for colored boxes
|
|
}
|
|
legendItem.appendChild(colorBox);
|
|
|
|
const label = document.createElement('span');
|
|
label.textContent = item.label;
|
|
legendItem.appendChild(label);
|
|
|
|
legendContainer.appendChild(legendItem);
|
|
});
|
|
// Optional spacer can still be added here if desired
|
|
});
|
|
}
|
|
|
|
// Call generateLegend once on load to show it initially
|
|
generateLegend();
|
|
|
|
</script>
|
|
|
|
</body>
|
|
</html> |