initial
This commit is contained in:
578
metar.html
Normal file
578
metar.html
Normal file
@@ -0,0 +1,578 @@
|
||||
<!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>
|
||||
Reference in New Issue
Block a user