View Source

The actual code and article source behind this site. No minification. No transpilation. What you see is what runs.

↓ Download PHP edition
weather.php 786 lines
<?php
require_once __DIR__ . '/includes/config.php';
$pageTitle = 'Weather — dispelled.ca';
$currentPage = 'weather';
$uvLabel = function(float $uv): string {
if ($uv < 3) return 'Low';
if ($uv < 6) return 'Moderate';
if ($uv < 8) return 'High';
if ($uv < 11) return 'Very High';
return 'Extreme';
};
$uvClass = function(float $uv): string {
if ($uv < 3) return 'uv-low';
if ($uv < 6) return 'uv-moderate';
if ($uv < 8) return 'uv-high';
if ($uv < 11) return 'uv-veryhigh';
return 'uv-extreme';
};
$deltaTLabel = function(float $dt): string {
if ($dt < 1) return 'Inversion risk';
if ($dt < 2) return 'Low — marginal';
if ($dt <= 8) return 'Ideal';
if ($dt <= 10) return 'Acceptable';
if ($dt <= 12) return 'High — marginal';
return 'Too hot/dry';
};
$deltaTClass = function(float $dt): string {
if ($dt < 1) return 'dt-poor';
if ($dt < 2) return 'dt-marginal';
if ($dt <= 8) return 'dt-ideal';
if ($dt <= 10) return 'dt-good';
if ($dt <= 12) return 'dt-marginal';
return 'dt-poor';
};
$soilMoistLabel = function(float $pct): string {
if ($pct < 15) return 'Dry';
if ($pct < 30) return 'Moist';
if ($pct < 40) return 'Wet';
return 'Saturated';
};
$soilMoistClass = function(float $pct): string {
if ($pct < 15) return 'moist-dry';
if ($pct < 30) return 'moist-good';
if ($pct < 40) return 'moist-wet';
return 'moist-sat';
};
$cacheFile = sys_get_temp_dir() . '/dispelled_weather_full.json';
$weather = null;
$staleWeather = null;
$cacheTtl = 300;
if (file_exists($cacheFile)) {
$cachedWeather = json_decode(file_get_contents($cacheFile), true);
if (is_array($cachedWeather) && $cachedWeather !== [] && empty($cachedWeather['error'])) {
$staleWeather = $cachedWeather;
if ((time() - filemtime($cacheFile)) < $cacheTtl) {
$weather = $cachedWeather;
}
}
}
if (!$weather) {
$ctx = stream_context_create(['http' => ['timeout' => 10]]);
$raw = @file_get_contents('http://localhost:8080/api/weather', false, $ctx);
if ($raw) {
$weather = json_decode($raw, true);
if ($weather && empty($weather['error'])) {
file_put_contents($cacheFile, $raw);
} else {
$weather = null;
}
}
}
// Open-Meteo can briefly reject requests when its shared rate limit is hit.
// Keep showing the last complete reading rather than replacing the page with
// an avoidable "Could not load weather data" error.
if (!$weather && $staleWeather) {
$weather = $staleWeather;
}
// ── Next good spray window ────────────────────────────────
$nextSprayWindow = null;
if ($weather) {
$goodCls = ['spray-ideal', 'spray-good'];
$hours = $weather['hourly'] ?? [];
$n = count($hours);
$start = null;
$end = null;
for ($i = 0; $i < $n; $i++) {
$cls = $hours[$i]['spray_cls'] ?? '';
if (in_array($cls, $goodCls, true)) {
if ($start === null) { $start = $i; $end = $i; }
else { $end = $i; }
} elseif ($start !== null) {
break;
}
}
if ($start !== null) {
$startTime = $hours[$start]['time'];
$endTime = $hours[$end]['time'];
// End label is start of next hour, so add 1h display text
$endDisplay = date('H:i', strtotime('+1 hour', strtotime(substr($endTime, -5) !== '23:00' ? 'today '.$endTime : 'today '.$endTime)));
$nextSprayWindow = [
'start' => $startTime,
'end' => $endTime,
'endPlus' => $endDisplay,
'hours' => ($end - $start + 1),
'cls' => $hours[$start]['spray_cls'],
'rating' => $hours[$start]['spray_rating'],
];
}
}
$days = ['Sun','Mon','Tue','Wed','Thu','Fri','Sat'];
require_once __DIR__ . '/includes/header.php';
?>
<h1 class="page-title">Weather</h1>
 
<?php if (!$weather): ?>
<p class="weather-error">Could not load weather data.</p>
<?php else:
$dt = (float)($weather['deltaT'] ?? 0);
$spray = $weather['spray'] ?? ['rating' => '—', 'cls' => ''];
$sources = $weather['sources'] ?? [];
$currentSource = $sources['current'] ?? [];
// Frost risk from forecast
$frostDays = array_filter($weather['forecast'] ?? [], fn($d) => $d['frost'] ?? false);
$freezeDays = array_filter($weather['forecast'] ?? [], fn($d) => $d['freeze'] ?? false);
$nextFrost = reset($frostDays);
$nextFreeze = reset($freezeDays);
?>
<!-- ── Current conditions ─────────────────────────────────── -->
<div class="wx-current">
<div class="wx-hero">
<div class="wx-temp-block">
<span class="wx-temp"><?= $weather['temp'] ?>°</span>
<span class="wx-icon-lg"><?= $weather['icon'] ?></span>
</div>
<p class="wx-label"><?= htmlspecialchars($weather['label']) ?></p>
<p class="wx-location"><?= htmlspecialchars($weather['location']) ?></p>
<p class="wx-current-source">
Live: <?php if (!empty($currentSource['url'])): ?><a href="<?= htmlspecialchars($currentSource['url']) ?>" target="_blank" rel="noopener"><?= htmlspecialchars($currentSource['label'] ?? 'Ardill weather station') ?> ↗</a><?php else: ?><?= htmlspecialchars($currentSource['label'] ?? 'Open-Meteo location model') ?><?php endif; ?>
<?php if (!empty($currentSource['observedAt'])): ?> · <?= htmlspecialchars($currentSource['observedAt']) ?><?php endif; ?><br>
Condition details: Open-Meteo
</p>
</div>
<div class="wx-grid">
<div class="wx-stat"><span class="wx-stat-label">Feels like</span><span class="wx-stat-val"><?= $weather['feelsLike'] ?>°C</span></div>
<div class="wx-stat"><span class="wx-stat-label">Dew Point</span><span class="wx-stat-val"><?= $weather['dewPoint'] ?>°C</span></div>
<div class="wx-stat"><span class="wx-stat-label">Humidity</span><span class="wx-stat-val"><?= $weather['humidity'] ?>%</span></div>
<div class="wx-stat"><span class="wx-stat-label">Wind</span><span class="wx-stat-val"><?= $weather['windSpeed'] ?> km/h <?= $weather['windDir'] ?></span></div>
<div class="wx-stat"><span class="wx-stat-label">Gusts</span><span class="wx-stat-val"><?= $weather['windGusts'] ?> km/h</span></div>
<div class="wx-stat"><span class="wx-stat-label"><?= htmlspecialchars($weather['precipLabel'] ?? 'Precip now') ?></span><span class="wx-stat-val"><?= $weather['precip'] ?> <?= htmlspecialchars($weather['precipUnit'] ?? 'mm') ?></span></div>
<?php if ($weather['precipToday'] !== null): ?><div class="wx-stat"><span class="wx-stat-label">Station precip today</span><span class="wx-stat-val"><?= $weather['precipToday'] ?> mm</span></div><?php endif; ?>
<div class="wx-stat"><span class="wx-stat-label">Pressure</span><span class="wx-stat-val"><?= $weather['pressure'] ?> hPa</span></div>
<div class="wx-stat"><span class="wx-stat-label">Cloud cover</span><span class="wx-stat-val"><?= $weather['cloud'] ?>%</span></div>
<div class="wx-stat"><span class="wx-stat-label">Visibility</span><span class="wx-stat-val"><?= $weather['visibility'] ?> km</span></div>
<div class="wx-stat"><span class="wx-stat-label">UV Index</span><span class="wx-stat-val <?= $uvClass($weather['uv']) ?>"><?= $weather['uv'] ?><?= $uvLabel($weather['uv']) ?></span></div>
</div>
</div>
 
<div class="wx-radar-space">
<span class="wx-radar-label">Radar</span>
<a class="wx-radar-link" href="https://www.theweathernetwork.com/en/maps/radar?lat=49.9007&amp;lng=-105.79557" target="_blank" rel="noopener">Weather Network radar for Ardill ↗</a>
</div>
 
<!-- ── Next Good Spray Window ────────────────────────────── -->
<?php if ($nextSprayWindow): ?>
<div class="wx-next-spray <?= htmlspecialchars($nextSprayWindow['cls']) ?>">
<span class="wx-next-spray-label">Next good spray window</span>
<span class="wx-next-spray-time"><?= htmlspecialchars($nextSprayWindow['start']) ?><?= htmlspecialchars($nextSprayWindow['endPlus']) ?></span>
<span class="wx-next-spray-dur"><?= $nextSprayWindow['hours'] ?>h window</span>
</div>
<?php else: ?>
<div class="wx-next-spray spray-poor">
<span class="wx-next-spray-label">Next good spray window</span>
<span class="wx-next-spray-time">None in the next 24 hours</span>
</div>
<?php endif ?>
<!-- ── Spray Window ───────────────────────────────────────── -->
<h2 class="wx-section-title">Spray Window</h2>
<p class="wx-section-source">Live station readings now; Open-Meteo hourly forecast for upcoming windows.</p>
<div class="wx-spray-now wx-spray-now--compact">
<div class="wx-spray-rating <?= htmlspecialchars($spray['cls']) ?>"><?= htmlspecialchars($spray['rating']) ?></div>
<div class="wx-spray-stats">
<div class="wx-stat">
<span class="wx-stat-label">Delta T</span>
<span class="wx-stat-val <?= $deltaTClass($dt) ?>"><?= $dt ?>°C — <?= $deltaTLabel($dt) ?></span>
</div>
<div class="wx-stat">
<span class="wx-stat-label">Wind</span>
<span class="wx-stat-val"><?= $weather['windSpeed'] ?> km/h <?= $weather['windDir'] ?></span>
</div>
<div class="wx-stat">
<span class="wx-stat-label">Humidity</span>
<span class="wx-stat-val"><?= $weather['humidity'] ?>%</span>
</div>
<div class="wx-stat">
<span class="wx-stat-label">Temp</span>
<span class="wx-stat-val"><?= $weather['temp'] ?>°C</span>
</div>
<div class="wx-stat">
<span class="wx-stat-label">Precip now</span>
<span class="wx-stat-val"><?= $weather['precip'] > 0 ? $weather['precip'].' mm' : 'None' ?></span>
</div>
</div>
</div>
 
<!-- ── Spray Timeline ────────────────────────────────────── -->
<?php
$sprayClsColors = [
'spray-ideal' => '#7db85a',
'spray-good' => '#a8c96a',
'spray-marginal' => '#f0aa3f',
'spray-poor' => '#cc3333',
];
?>
<div class="wx-spray-timeline-wrap">
<div class="wx-spray-timeline">
<?php foreach ($weather['hourly'] as $h):
$bg = $sprayClsColors[$h['spray_cls']] ?? '#555';
?>
<div class="wx-spray-block" style="background:<?= $bg ?>" title="<?= htmlspecialchars($h['time'].': '.$h['spray_rating']) ?>"></div>
<?php endforeach ?>
</div>
<div class="wx-spray-timeline-labels">
<?php foreach ($weather['hourly'] as $i => $h): ?>
<div class="wx-spray-tl-label"><?= ($i % 4 === 0) ? $h['time'] : '' ?></div>
<?php endforeach ?>
</div>
<div class="wx-spray-tl-legend">
<span class="wx-stl-dot" style="background:#7db85a"></span>Ideal
<span class="wx-stl-dot" style="background:#a8c96a"></span>Good
<span class="wx-stl-dot" style="background:#f0aa3f"></span>Marginal
<span class="wx-stl-dot" style="background:#cc3333"></span>Poor
</div>
</div>
 
<div class="wx-hourly-scroll">
<table class="wx-hourly-table">
<thead>
<tr>
<th>Time</th>
<th>Rating</th>
<th>Delta T</th>
<th>Temp</th>
<th>RH</th>
<th>Wind</th>
<th>Dir</th>
<th>Precip %</th>
<th>Precip</th>
</tr>
</thead>
<tbody>
<?php foreach ($weather['hourly'] as $h): ?>
<tr>
<td class="wx-mono"><?= htmlspecialchars($h['time']) ?></td>
<td class="wx-mono <?= htmlspecialchars($h['spray_cls']) ?>"><?= htmlspecialchars($h['spray_rating']) ?></td>
<td class="wx-mono <?= $deltaTClass($h['delta_t']) ?>"><?= $h['delta_t'] ?>°C</td>
<td class="wx-mono"><?= $h['temp'] ?>°C</td>
<td class="wx-mono"><?= $h['rh'] ?>%</td>
<td class="wx-mono"><?= $h['wind'] ?> km/h</td>
<td class="wx-mono"><?= $h['wind_dir'] ?></td>
<td class="wx-mono<?= $h['precip_prob'] >= 50 ? ' wx-precip-hi' : '' ?>"><?= $h['precip_prob'] ?>%</td>
<td class="wx-mono"><?= $h['precip'] > 0 ? $h['precip'].' mm' : '—' ?></td>
</tr>
<?php endforeach ?>
</tbody>
</table>
</div>
 
 
<!-- ── Precipitation ─────────────────────────────────────── -->
<h2 class="wx-section-title">Precipitation</h2>
<p class="wx-section-source">Open-Meteo precipitation history and forecast for Ardill.</p>
<div class="wx-precip-summary">
<div class="wx-stat">
<span class="wx-stat-label">Past 7-day total</span>
<span class="wx-stat-val"><?= $weather['sevenDayPastTotal'] ?> mm</span>
</div>
<div class="wx-stat">
<span class="wx-stat-label">Days since rain (&ge;1 mm)</span>
<span class="wx-stat-val"><?= $weather['daysSinceRain'] === 0 ? 'Today' : $weather['daysSinceRain'].' day'.($weather['daysSinceRain'] === 1 ? '' : 's') ?></span>
</div>
</div>
<?php
// Build unified bar data: past (actual) + forecast
$pBars = [];
$nPast = count($weather['precipHistory']);
foreach ($weather['precipHistory'] as $p) {
$pBars[] = ['lbl' => date('M j', strtotime($p['date'])), 'val' => (float)$p['precip'], 'fc' => false, 'prob' => null];
}
foreach ($weather['forecast'] as $fi => $day) {
$pBars[] = ['lbl' => ($fi === 0 ? 'Today' : date('M j', strtotime($day['date']))), 'val' => (float)$day['precip_sum'], 'fc' => true, 'prob' => $day['precip_prob']];
}
$nBars = count($pBars);
$pMaxVal = max(array_column($pBars, 'val'));
$pMaxVal = max($pMaxVal, 5.0);
$psvgW = 660; $psvgH = 140;
$ppadL = 30; $ppadR = 10; $ppadT = 12; $ppadB = 26;
$pcW = $psvgW - $ppadL - $ppadR;
$pcH = $psvgH - $ppadT - $ppadB;
$slotW = $pcW / $nBars;
$barW = max(6, $slotW * 0.65);
$pxOf = fn(int $i) => $ppadL + ($i + 0.5) * $slotW;
$pyOf = fn(float $v) => $ppadT + $pcH - ($v / $pMaxVal) * $pcH;
// Y gridlines: pick nice steps
$pStep = $pMaxVal <= 10 ? 2 : ($pMaxVal <= 25 ? 5 : 10);
$pGrids = [];
for ($pv = $pStep; $pv <= $pMaxVal; $pv += $pStep) {
$pGrids[] = ['y' => round($pyOf((float)$pv), 1), 'label' => $pv];
}
// Divider x between past and forecast
$divX = round($ppadL + $nPast * $slotW, 1);
?>
<div class="wx-svg-wrap">
<svg viewBox="0 0 <?= $psvgW ?> <?= $psvgH ?>" width="100%" style="display:block;" aria-label="Precipitation chart">
<!-- bg -->
<rect width="<?= $psvgW ?>" height="<?= $psvgH ?>" fill="#111827" rx="8"/>
 
<!-- baseline -->
<line x1="<?= $ppadL ?>" y1="<?= $ppadT + $pcH ?>" x2="<?= $psvgW - $ppadR ?>" y2="<?= $ppadT + $pcH ?>"
stroke="#ffffff" stroke-opacity="0.08" stroke-width="1"/>
 
<!-- y gridlines -->
<?php foreach ($pGrids as $pg): ?>
<line x1="<?= $ppadL ?>" y1="<?= $pg['y'] ?>" x2="<?= $psvgW - $ppadR ?>" y2="<?= $pg['y'] ?>"
stroke="#ffffff" stroke-opacity="0.05" stroke-width="1"/>
<text x="<?= $ppadL - 3 ?>" y="<?= $pg['y'] + 4 ?>" text-anchor="end"
font-family="'Roboto Mono',monospace" font-size="8" fill="#6b7a8d"><?= $pg['label'] ?></text>
<?php endforeach ?>
<!-- past/forecast divider -->
<line x1="<?= $divX ?>" y1="<?= $ppadT ?>" x2="<?= $divX ?>" y2="<?= $ppadT + $pcH ?>"
stroke="#ffffff" stroke-opacity="0.12" stroke-width="1" stroke-dasharray="3 3"/>
<text x="<?= $divX - 4 ?>" y="<?= $ppadT + 9 ?>" text-anchor="end"
font-family="'Roboto Mono',monospace" font-size="7" fill="#6b7a8d">past</text>
<text x="<?= $divX + 4 ?>" y="<?= $ppadT + 9 ?>" text-anchor="start"
font-family="'Roboto Mono',monospace" font-size="7" fill="#6b7a8d">forecast</text>
 
<!-- bars -->
<?php foreach ($pBars as $bi => $bar):
$bx = round($pxOf($bi), 1);
$barTop = round($pyOf($bar['val']), 1);
$barH = round(($ppadT + $pcH) - $barTop, 1);
$col = $bar['fc'] ? '#7eb8e0' : '#5a9ab8';
$op = $bar['fc'] ? '0.45' : '0.85';
if ($barH < 1) { $barTop = $ppadT + $pcH - 1; $barH = 1; }
$tip = $bar['lbl'] . ': ' . ($bar['val'] > 0 ? $bar['val'].' mm' : 'No precip');
$tip .= $bar['fc'] && $bar['prob'] !== null ? ' ('.$bar['prob'].'% chance)' : ' (actual)';
?>
<g>
<title><?= htmlspecialchars($tip) ?></title>
<rect x="<?= $bx - $barW / 2 ?>" y="<?= $barTop ?>"
width="<?= $barW ?>" height="<?= $barH ?>"
fill="<?= $col ?>" fill-opacity="<?= $op ?>" rx="2"/>
<rect x="<?= $bx - $barW / 2 ?>" y="<?= $ppadT ?>"
width="<?= $barW ?>" height="<?= $pcH ?>"
fill="transparent"/>
</g>
<?php endforeach ?>
<!-- x labels every 2 bars -->
<?php foreach ($pBars as $bi => $bar): if ($bi % 2 === 0): ?>
<text x="<?= round($pxOf($bi), 1) ?>" y="<?= $psvgH - 5 ?>" text-anchor="middle"
font-family="'Roboto Mono',monospace" font-size="7.5" fill="#6b7a8d"><?= $bar['lbl'] ?></text>
<?php endif; endforeach ?>
</svg>
</div>
 
<div class="wx-hourly-scroll">
<table class="wx-hourly-table">
<thead>
<tr>
<th>Date</th>
<th>Precip</th>
<th>Bar</th>
</tr>
</thead>
<tbody>
<?php foreach ($weather['precipHistory'] as $p):
$barPct = min(100, (int)round($p['precip'] * 5));
?>
<tr>
<td class="wx-mono"><?= htmlspecialchars($p['date']) ?></td>
<td class="wx-mono"><?= $p['precip'] > 0 ? $p['precip'].' mm' : '—' ?></td>
<td><div class="wx-precip-bar" style="width:<?= $barPct ?>%"></div></td>
</tr>
<?php endforeach ?>
<tr class="wx-precip-divider"><td colspan="3"><em>— forecast below —</em></td></tr>
<?php foreach ($weather['forecast'] as $i => $day):
$ts = strtotime($day['date'] . 'T12:00:00');
$label = $i === 0 ? 'Today' : date('D M j', $ts);
$barPct = min(100, (int)round($day['precip_sum'] * 5));
?>
<tr>
<td class="wx-mono"><?= $label ?></td>
<td class="wx-mono<?= $day['precip_prob'] >= 50 ? ' wx-precip-hi' : '' ?>"><?= $day['precip_sum'] > 0 ? $day['precip_sum'].' mm' : '—' ?> (<?= $day['precip_prob'] ?>%)</td>
<td><div class="wx-precip-bar wx-precip-bar-fc" style="width:<?= $barPct ?>%"></div></td>
</tr>
<?php endforeach ?>
</tbody>
</table>
</div>
 
<!-- ── Next 24 Hours ──────────────────────────────────────── -->
<h2 class="wx-section-title">Next 24 Hours</h2>
<p class="wx-section-source">Open-Meteo hourly forecast for Ardill.</p>
<?php
$temps = array_column($weather['hourly'], 'temp');
$dews = array_column($weather['hourly'], 'dew_point');
$htimes = array_column($weather['hourly'], 'time');
$n = count($temps);
$allVals = array_merge($temps, $dews);
$rawMin = min($allVals);
$rawMax = max($allVals);
$minY = (int)(floor($rawMin / 5) * 5) - 2;
$maxY = (int)(ceil($rawMax / 5) * 5) + 2;
$range = $maxY - $minY;
$svgW = 660; $svgH = 200;
$padL = 36; $padR = 14; $padT = 14; $padB = 30;
$cW = $svgW - $padL - $padR;
$cH = $svgH - $padT - $padB;
$xOf = fn(int $i) => $padL + ($n > 1 ? ($i / ($n - 1)) * $cW : 0);
$yOf = fn(float $v) => $padT + $cH - (($v - $minY) / $range) * $cH;
$tPts = []; $dPts = [];
for ($i = 0; $i < $n; $i++) {
$tPts[] = round($xOf($i), 1) . ',' . round($yOf((float)$temps[$i]), 1);
$dPts[] = round($xOf($i), 1) . ',' . round($yOf((float)$dews[$i]), 1);
}
$areaPath = 'M ' . $tPts[0];
foreach (array_slice($tPts, 1) as $pt) $areaPath .= ' L ' . $pt;
$areaPath .= ' L ' . round($xOf($n - 1), 1) . ',' . ($padT + $cH);
$areaPath .= ' L ' . round($xOf(0), 1) . ',' . ($padT + $cH) . ' Z';
$grids = [];
for ($t = (int)ceil($minY / 5) * 5; $t <= $maxY; $t += 5) {
$grids[] = ['y' => round($yOf((float)$t), 1), 'label' => $t];
}
$hitW = round($cW / max(1, $n), 1); // width of each hit zone
?>
<div class="wx-svg-wrap">
<svg viewBox="0 0 <?= $svgW ?> <?= $svgH ?>" width="100%" style="display:block;" aria-label="24-hour temperature chart">
<defs>
<linearGradient id="tGrad" x1="0" y1="0" x2="0" y2="1">
<stop offset="0%" stop-color="#7db85a" stop-opacity="0.25"/>
<stop offset="100%" stop-color="#7db85a" stop-opacity="0.02"/>
</linearGradient>
</defs>
 
<!-- bg -->
<rect width="<?= $svgW ?>" height="<?= $svgH ?>" fill="#111827" rx="8"/>
 
<!-- gridlines -->
<?php foreach ($grids as $g): ?>
<line x1="<?= $padL ?>" y1="<?= $g['y'] ?>" x2="<?= $svgW - $padR ?>" y2="<?= $g['y'] ?>"
stroke="#ffffff" stroke-opacity="0.06" stroke-width="1"/>
<text x="<?= $padL - 4 ?>" y="<?= $g['y'] + 4 ?>" text-anchor="end"
font-family="'Roboto Mono',monospace" font-size="9" fill="#6b7a8d"><?= $g['label'] ?>°</text>
<?php endforeach ?>
<!-- x-axis hour labels every 4h -->
<?php foreach ($htimes as $i => $ht): if ($i % 4 === 0): ?>
<text x="<?= round($xOf($i), 1) ?>" y="<?= $svgH - 6 ?>"
text-anchor="<?= $i === 0 ? 'start' : ($i >= $n - 2 ? 'end' : 'middle') ?>"
font-family="'Roboto Mono',monospace" font-size="9" fill="#6b7a8d"><?= $ht ?></text>
<?php endif; endforeach ?>
<!-- area under temp -->
<path d="<?= $areaPath ?>" fill="url(#tGrad)"/>
 
<!-- dew point line -->
<polyline points="<?= implode(' ', $dPts) ?>"
fill="none" stroke="#7eb8e0" stroke-width="1.5" stroke-opacity="0.55" stroke-dasharray="4 3"/>
 
<!-- temp line -->
<polyline points="<?= implode(' ', $tPts) ?>"
fill="none" stroke="#7db85a" stroke-width="2" stroke-linejoin="round"/>
 
<!-- legend -->
<line x1="<?= $svgW - 110 ?>" y1="18" x2="<?= $svgW - 96 ?>" y2="18"
stroke="#7db85a" stroke-width="2"/>
<text x="<?= $svgW - 92 ?>" y="22" font-family="'Roboto Mono',monospace" font-size="9" fill="#9aacbe">Temp</text>
<line x1="<?= $svgW - 60 ?>" y1="18" x2="<?= $svgW - 46 ?>" y2="18"
stroke="#7eb8e0" stroke-width="1.5" stroke-opacity="0.55" stroke-dasharray="4 3"/>
<text x="<?= $svgW - 42 ?>" y="22" font-family="'Roboto Mono',monospace" font-size="9" fill="#9aacbe">Dew pt</text>
 
<!-- hover hit areas (transparent, full chart height) -->
<?php for ($i = 0; $i < $n; $i++):
$hx = round($xOf($i), 1);
$tip = $htimes[$i] . ' — ' . $temps[$i] . '°C | Dew pt ' . $dews[$i] . '°C';
?>
<g>
<title><?= htmlspecialchars($tip) ?></title>
<rect x="<?= $hx - $hitW / 2 ?>" y="<?= $padT ?>" width="<?= $hitW ?>" height="<?= $cH ?>" fill="transparent"/>
</g>
<?php endfor ?>
</svg>
</div>
<div class="wx-hourly-scroll">
<table class="wx-hourly-table">
<thead>
<tr>
<th>Time</th><th>Cond.</th><th>Temp</th><th>Dew Pt</th>
<th>Precip %</th><th>Precip</th><th>Snow</th>
<th>Wind</th><th>Dir</th><th>UV</th><th>Visibility</th>
</tr>
</thead>
<tbody>
<?php foreach ($weather['hourly'] as $h): ?>
<tr>
<td class="wx-mono"><?= htmlspecialchars($h['time']) ?></td>
<td><?= $h['icon'] ?></td>
<td class="wx-mono"><?= $h['temp'] ?>°</td>
<td class="wx-mono"><?= $h['dew_point'] ?>°</td>
<td class="wx-mono<?= $h['precip_prob'] >= 50 ? ' wx-precip-hi' : '' ?>"><?= $h['precip_prob'] ?>%</td>
<td class="wx-mono"><?= $h['precip'] > 0 ? $h['precip'].' mm' : '—' ?></td>
<td class="wx-mono"><?= $h['snowfall'] > 0 ? $h['snowfall'].' cm' : '—' ?></td>
<td class="wx-mono"><?= $h['wind'] ?> km/h</td>
<td class="wx-mono"><?= $h['wind_dir'] ?></td>
<td class="wx-mono <?= $uvClass($h['uv']) ?>"><?= $h['uv'] ?></td>
<td class="wx-mono"><?= $h['visibility'] ?> km</td>
</tr>
<?php endforeach ?>
</tbody>
</table>
</div>
 
<!-- ── 7-Day Forecast ─────────────────────────────────────── -->
<h2 class="wx-section-title">7-Day Forecast</h2>
<p class="wx-section-source">Open-Meteo daily forecast for Ardill.</p>
<div class="wx-forecast-grid">
<?php foreach ($weather['forecast'] as $i => $day):
$ts = strtotime($day['date'] . 'T12:00:00');
$dow = $i === 0 ? 'Today' : $days[(int) date('w', $ts)];
?>
<div class="wx-forecast-card<?= $day['frost'] ? ' wx-frost-card' : '' ?>">
<p class="wx-fc-dow"><?= $dow ?></p>
<p class="wx-fc-date"><?= date('M j', $ts) ?></p>
<p class="wx-fc-icon"><?= $day['icon'] ?></p>
<p class="wx-fc-label"><?= htmlspecialchars($day['label']) ?></p>
<p class="wx-fc-temps"><strong><?= $day['high'] ?>°</strong> / <span class="<?= $day['frost'] ? 'frost-risk' : '' ?> <?= $day['freeze'] ? 'frost-freeze' : '' ?>"><?= $day['low'] ?>°</span></p>
<p class="wx-fc-feels">feels <?= $day['feels_high'] ?>° / <?= $day['feels_low'] ?>°</p>
<table class="wx-fc-detail">
<tr><td>Precip</td><td><?= $day['precip_prob'] ?>% &middot; <?= $day['precip_sum'] ?> mm</td></tr>
<tr><td>Precip hrs</td><td><?= $day['precip_hrs'] ?>h</td></tr>
<?php if ($day['snowfall_sum'] > 0): ?>
<tr><td>Snowfall</td><td><?= $day['snowfall_sum'] ?> cm</td></tr>
<?php endif ?>
<tr><td>Wind max</td><td><?= $day['wind_max'] ?> km/h <?= $day['wind_dir'] ?></td></tr>
<tr><td>Gusts max</td><td><?= $day['gust_max'] ?> km/h</td></tr>
<tr><td>UV max</td><td class="<?= $uvClass($day['uv_max']) ?>"><?= $day['uv_max'] ?><?= $uvLabel($day['uv_max']) ?></td></tr>
<tr><td>Sunshine</td><td><?= $day['sunshine_hrs'] ?> hrs</td></tr>
<tr><td>Solar</td><td><?= $day['solar_sum'] ?> MJ/m²</td></tr>
<tr><td>GDD</td><td><?= $day['gdd'] ?> &deg;C&middot;d (cum. <?= $day['gdd_cum'] ?>)</td></tr>
<tr><td>Sunrise</td><td><?= $day['sunrise'] ?></td></tr>
<tr><td>Sunset</td><td><?= $day['sunset'] ?></td></tr>
<tr><td>ET&#8320;</td><td><?= $day['et0'] ?> mm</td></tr>
</table>
</div>
<?php endforeach ?>
</div>
 
<!-- ── Field Conditions & Soil ───────────────────────────── -->
<h2 class="wx-section-title">Field Conditions &amp; Soil</h2>
<p class="wx-section-source">Open-Meteo soil temperature and moisture model for Ardill.</p>
<?php $soil = $weather['hourly'][0] ?? null; ?>
<?php if ($soil): ?>
<div class="wx-field-summary">
<div class="wx-stat">
<span class="wx-stat-label">Days since rain</span>
<span class="wx-stat-val"><?= $weather['daysSinceRain'] === 0 ? 'Today' : $weather['daysSinceRain'].' day'.($weather['daysSinceRain'] === 1 ? '' : 's') ?></span>
</div>
<div class="wx-stat">
<span class="wx-stat-label">Surface soil (0 cm)</span>
<span class="wx-stat-val <?= $soilMoistClass($soil['soil_moist_0']) ?>"><?= $soil['soil_moist_0'] ?>% — <?= $soilMoistLabel($soil['soil_moist_0']) ?></span>
</div>
<div class="wx-stat">
<span class="wx-stat-label">6 cm soil</span>
<span class="wx-stat-val <?= $soilMoistClass($soil['soil_moist_3']) ?>"><?= $soil['soil_moist_3'] ?>% — <?= $soilMoistLabel($soil['soil_moist_3']) ?></span>
</div>
<div class="wx-stat">
<span class="wx-stat-label">18 cm soil</span>
<span class="wx-stat-val <?= $soilMoistClass($soil['soil_moist_9']) ?>"><?= $soil['soil_moist_9'] ?>% — <?= $soilMoistLabel($soil['soil_moist_9']) ?></span>
</div>
<div class="wx-stat">
<span class="wx-stat-label">Soil temp (0 cm)</span>
<span class="wx-stat-val"><?= $soil['soil_temp_0'] ?>°C</span>
</div>
<div class="wx-stat">
<span class="wx-stat-label">Soil temp (18 cm)</span>
<span class="wx-stat-val"><?= $soil['soil_temp_18'] ?>°C</span>
</div>
</div>
 
<div class="wx-hourly-scroll">
<table class="wx-hourly-table">
<thead>
<tr>
<th>Time</th>
<th>0 cm Temp</th><th>0 cm Moist</th>
<th>6 cm Temp</th><th>6 cm Moist</th>
<th>18 cm Temp</th><th>18 cm Moist</th>
</tr>
</thead>
<tbody>
<?php foreach ($weather['hourly'] as $h): ?>
<tr>
<td class="wx-mono"><?= htmlspecialchars($h['time']) ?></td>
<td class="wx-mono"><?= $h['soil_temp_0'] ?>°C</td>
<td class="wx-mono <?= $soilMoistClass($h['soil_moist_0']) ?>"><?= $h['soil_moist_0'] ?>%</td>
<td class="wx-mono"><?= $h['soil_temp_6'] ?>°C</td>
<td class="wx-mono <?= $soilMoistClass($h['soil_moist_3']) ?>"><?= $h['soil_moist_3'] ?>%</td>
<td class="wx-mono"><?= $h['soil_temp_18'] ?>°C</td>
<td class="wx-mono <?= $soilMoistClass($h['soil_moist_9']) ?>"><?= $h['soil_moist_9'] ?>%</td>
</tr>
<?php endforeach ?>
</tbody>
</table>
</div>
<?php endif ?>
<!-- ── Frost Risk ─────────────────────────────────────────── -->
<h2 class="wx-section-title">Frost Risk</h2>
<p class="wx-section-source">Open-Meteo seven-day forecast for Ardill.</p>
<?php if (!$nextFrost && !$nextFreeze): ?>
<p class="wx-frost-clear">No frost or freeze risk in the next 7 days. Lowest forecast minimum: <?= min(array_column($weather['forecast'], 'low')) ?>°C.</p>
<?php else: ?>
<?php if ($nextFreeze): ?>
<p class="wx-frost-alert wx-freeze-alert">&#9888; Freeze risk: <?= htmlspecialchars($nextFreeze['date']) ?> — low of <?= $nextFreeze['low'] ?>°C</p>
<?php elseif ($nextFrost): ?>
<p class="wx-frost-alert">&#9888; Frost risk: <?= htmlspecialchars($nextFrost['date']) ?> — low of <?= $nextFrost['low'] ?>°C</p>
<?php endif ?>
<?php endif ?>
<div class="wx-hourly-scroll">
<table class="wx-hourly-table">
<thead>
<tr>
<th>Date</th>
<th>Low</th>
<th>High</th>
<th>Risk</th>
<th>Precip %</th>
<th>Snow</th>
</tr>
</thead>
<tbody>
<?php foreach ($weather['forecast'] as $i => $day):
$ts = strtotime($day['date'] . 'T12:00:00');
$label = $i === 0 ? 'Today' : date('D M j', $ts);
if ($day['freeze']) { $risk = 'Freeze'; $riskCls = 'frost-freeze'; }
elseif ($day['frost']) { $risk = 'Frost'; $riskCls = 'frost-risk'; }
else { $risk = '—'; $riskCls = ''; }
?>
<tr>
<td class="wx-mono"><?= $label ?></td>
<td class="wx-mono <?= $day['low'] <= 2 ? 'frost-risk' : '' ?> <?= $day['low'] <= 0 ? 'frost-freeze' : '' ?>"><?= $day['low'] ?>°C</td>
<td class="wx-mono"><?= $day['high'] ?>°C</td>
<td class="wx-mono <?= $riskCls ?>"><?= $risk ?></td>
<td class="wx-mono"><?= $day['precip_prob'] ?>%</td>
<td class="wx-mono"><?= $day['snowfall_sum'] > 0 ? $day['snowfall_sum'].' cm' : '—' ?></td>
</tr>
<?php endforeach ?>
</tbody>
</table>
</div>
 
 
<!-- ── Season GDD ──────────────────────────────────────────── -->
<?php if (!empty($weather['seasonGdd'])): ?>
<h2 class="wx-section-title">Growing Degree Days — Season to Date</h2>
<p class="wx-section-source">Open-Meteo daily forecast and archive data for Ardill.</p>
<?php
$sgdd = (float)$weather['seasonGdd'];
$sgddDays = (int)$weather['seasonGddDays'];
$sgddAvg = $sgddDays > 0 ? round($sgdd / $sgddDays, 1) : 0;
$sgddStart = htmlspecialchars($weather['seasonGddStart'] ?? '');
// Rough crop staging benchmarks (base 10°C, prairie)
$stages = [
['crop'=>'Green Lentils', 'stages'=>[
['gdd'=> 0,'label'=>'Seeding'],
['gdd'=> 100,'label'=>'Emergence'],
['gdd'=> 250,'label'=>'Vegetative'],
['gdd'=> 500,'label'=>'Flowering'],
['gdd'=> 700,'label'=>'Pod fill'],
['gdd'=> 900,'label'=>'Maturity'],
]],
['crop'=>'Chickpeas', 'stages'=>[
['gdd'=> 0,'label'=>'Seeding'],
['gdd'=> 150,'label'=>'Emergence'],
['gdd'=> 300,'label'=>'Vegetative'],
['gdd'=> 600,'label'=>'Flowering'],
['gdd'=> 850,'label'=>'Pod fill'],
['gdd'=>1100,'label'=>'Maturity'],
]],
['crop'=>'Durum Wheat', 'stages'=>[
['gdd'=> 0,'label'=>'Seeding'],
['gdd'=> 90,'label'=>'Emergence'],
['gdd'=> 325,'label'=>'Tillering'],
['gdd'=> 530,'label'=>'Stem extension'],
['gdd'=> 760,'label'=>'Heading'],
['gdd'=> 900,'label'=>'Flowering'],
['gdd'=>1300,'label'=>'Maturity'],
]],
['crop'=>'Canola', 'stages'=>[
['gdd'=> 0,'label'=>'Seeding'],
['gdd'=> 75,'label'=>'Emergence'],
['gdd'=> 200,'label'=>'Rosette'],
['gdd'=> 350,'label'=>'Bolting'],
['gdd'=> 500,'label'=>'Flowering'],
['gdd'=> 800,'label'=>'Pod fill'],
['gdd'=>1100,'label'=>'Maturity'],
]],
];
?>
<div class="wx-gdd-summary">
<div class="wx-gdd-total">
<span class="wx-gdd-num"><?= number_format($sgdd, 1) ?></span>
<span class="wx-gdd-unit">&deg;C&middot;d</span>
</div>
<div class="wx-gdd-meta">
<div class="wx-stat"><span class="wx-stat-label">Since</span><span class="wx-stat-val"><?= $sgddStart ?></span></div>
<div class="wx-stat"><span class="wx-stat-label">Days counted</span><span class="wx-stat-val"><?= $sgddDays ?></span></div>
<div class="wx-stat"><span class="wx-stat-label">Daily avg</span><span class="wx-stat-val"><?= $sgddAvg ?> &deg;C&middot;d</span></div>
<div class="wx-stat"><span class="wx-stat-label">Base temp</span><span class="wx-stat-val">10 &deg;C</span></div>
</div>
</div>
 
<?php foreach ($stages as $crop): ?>
<h3 class="wx-gdd-crop"><?= htmlspecialchars($crop['crop']) ?></h3>
<div class="wx-gdd-stages">
<?php
// find current stage
$curStage = $crop['stages'][0];
$nextStage = null;
for ($si = 0; $si < count($crop['stages']); $si++) {
if ($sgdd >= $crop['stages'][$si]['gdd']) {
$curStage = $crop['stages'][$si];
$nextStage = $crop['stages'][$si + 1] ?? null;
}
}
?>
<?php foreach ($crop['stages'] as $si => $stage):
$isReached = $sgdd >= $stage['gdd'];
$isCurrent = $stage === $curStage;
$remaining = $stage['gdd'] - $sgdd;
?>
<div class="wx-gdd-stage <?= $isReached ? 'gdd-reached' : 'gdd-future' ?> <?= $isCurrent ? 'gdd-current' : '' ?>">
<span class="wx-gdd-stage-name"><?= htmlspecialchars($stage['label']) ?></span>
<span class="wx-gdd-stage-val"><?= $stage['gdd'] ?> &deg;C&middot;d</span>
<?php if (!$isReached): ?>
<span class="wx-gdd-stage-rem"><?= ceil($remaining) ?> to go<?= $sgddAvg > 0 ? ' (~'.ceil($remaining/$sgddAvg).'d)' : '' ?></span>
<?php else: ?>
<span class="wx-gdd-stage-rem gdd-done">&#10003;</span>
<?php endif ?>
</div>
<?php endforeach ?>
</div>
<?php endforeach ?>
<?php endif ?>
<p class="data-attr">Data: <a href="https://open-meteo.com/" target="_blank" rel="noreferrer">Open-Meteo</a> &middot; Ardill, SK (49.94, &minus;105.96) &middot; Observed <?= htmlspecialchars($weather['time']) ?></p>
 
<?php endif ?>
<?php require_once __DIR__ . '/includes/footer.php'; ?>