View Source
The actual code and article source behind this site. No minification. No transpilation. What you see is what runs.
|
| 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&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 (≥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'] ?>% · <?= $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'] ?> °C·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₀</td><td><?= $day['et0'] ?> mm</td></tr> |
| </table> |
| </div> |
| <?php endforeach ?> |
| </div> |
| <!-- ── Field Conditions & Soil ───────────────────────────── --> |
| <h2 class="wx-section-title">Field Conditions & 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">⚠ Freeze risk: <?= htmlspecialchars($nextFreeze['date']) ?> — low of <?= $nextFreeze['low'] ?>°C</p> |
| <?php elseif ($nextFrost): ?> |
| <p class="wx-frost-alert">⚠ 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">°C·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 ?> °C·d</span></div> |
| <div class="wx-stat"><span class="wx-stat-label">Base temp</span><span class="wx-stat-val">10 °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'] ?> °C·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">✓</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> · Ardill, SK (49.94, −105.96) · Observed <?= htmlspecialchars($weather['time']) ?></p> |
| <?php endif ?> |
| <?php require_once __DIR__ . '/includes/footer.php'; ?> |
dispelled