View Source
The actual code and article source behind this site. No minification. No transpilation. What you see is what runs.
|
| function weatherRound1(float $value): float |
| { |
| return round($value, 1); |
| } |
| function weatherWmo(int $code): array |
| { |
| static $codes = [ |
| 0 => ['label' => 'Clear sky', 'icon' => '☀️'], |
| 1 => ['label' => 'Mainly clear', 'icon' => '🌤️'], |
| 2 => ['label' => 'Partly cloudy', 'icon' => '⛅'], |
| 3 => ['label' => 'Overcast', 'icon' => '☁️'], |
| 45 => ['label' => 'Foggy', 'icon' => '🌫️'], |
| 48 => ['label' => 'Icy fog', 'icon' => '🌫️'], |
| 51 => ['label' => 'Light drizzle', 'icon' => '🌦️'], |
| 53 => ['label' => 'Drizzle', 'icon' => '🌦️'], |
| 55 => ['label' => 'Heavy drizzle', 'icon' => '🌧️'], |
| 61 => ['label' => 'Light rain', 'icon' => '🌧️'], |
| 63 => ['label' => 'Rain', 'icon' => '🌧️'], |
| 65 => ['label' => 'Heavy rain', 'icon' => '🌧️'], |
| 71 => ['label' => 'Light snow', 'icon' => '🌨️'], |
| 73 => ['label' => 'Snow', 'icon' => '❄️'], |
| 75 => ['label' => 'Heavy snow', 'icon' => '❄️'], |
| 77 => ['label' => 'Snow grains', 'icon' => '🌨️'], |
| 80 => ['label' => 'Light showers', 'icon' => '🌦️'], |
| 81 => ['label' => 'Showers', 'icon' => '🌧️'], |
| 82 => ['label' => 'Heavy showers', 'icon' => '🌧️'], |
| 85 => ['label' => 'Snow showers', 'icon' => '🌨️'], |
| 86 => ['label' => 'Heavy snow showers', 'icon' => '❄️'], |
| 95 => ['label' => 'Thunderstorm', 'icon' => '⛈️'], |
| 96 => ['label' => 'Thunderstorm w/ hail', 'icon' => '⛈️'], |
| 99 => ['label' => 'Thunderstorm w/ heavy hail', 'icon' => '⛈️'], |
| ]; |
| return $codes[$code] ?? ['label' => 'Unknown', 'icon' => '🌡️']; |
| } |
| function weatherWindDirection(float $degrees): string |
| { |
| $directions = ['N', 'NE', 'E', 'SE', 'S', 'SW', 'W', 'NW']; |
| return $directions[((int)round($degrees / 45)) % 8]; |
| } |
| function weatherDewPoint(float $tempC, float $humidity): float |
| { |
| $a = 17.625; |
| $b = 243.04; |
| $safeHumidity = max(0.1, min(100, $humidity)); |
| $gamma = log($safeHumidity / 100) + ($a * $tempC) / ($b + $tempC); |
| return weatherRound1(($b * $gamma) / ($a - $gamma)); |
| } |
| function weatherWetBulb(float $tempC, float $humidity): float |
| { |
| return $tempC * atan(0.151977 * sqrt($humidity + 8.313659)) |
| + atan($tempC + $humidity) |
| - atan($humidity - 1.676331) |
| + 0.00391838 * pow($humidity, 1.5) * atan(0.023101 * $humidity) |
| - 4.686035; |
| } |
| function weatherDeltaT(float $tempC, float $humidity): float |
| { |
| return weatherRound1($tempC - weatherWetBulb($tempC, $humidity)); |
| } |
| function weatherSprayRating( |
| float $deltaT, |
| float $wind, |
| float $tempC, |
| float $precipMm, |
| float $precipProbability |
| ): array { |
| if ($precipMm > 0) return ['rating' => 'Poor — raining', 'cls' => 'spray-poor']; |
| if ($wind > 24) return ['rating' => 'Poor — wind too high', 'cls' => 'spray-poor']; |
| if ($deltaT < 1) return ['rating' => 'Poor — inversion risk', 'cls' => 'spray-poor']; |
| if ($deltaT > 12) return ['rating' => 'Poor — too hot/dry', 'cls' => 'spray-poor']; |
| if ($tempC < 5) return ['rating' => 'Poor — too cold', 'cls' => 'spray-poor']; |
| if ($deltaT < 2) return ['rating' => 'Marginal — low Delta T', 'cls' => 'spray-marginal']; |
| if ($deltaT > 10) return ['rating' => 'Marginal — high Delta T', 'cls' => 'spray-marginal']; |
| if ($wind > 15) return ['rating' => 'Marginal — windy', 'cls' => 'spray-marginal']; |
| if ($wind < 3) return ['rating' => 'Marginal — calm/inversion risk', 'cls' => 'spray-marginal']; |
| if ($tempC < 10) return ['rating' => 'Marginal — cool', 'cls' => 'spray-marginal']; |
| if ($precipProbability >= 50) return ['rating' => 'Marginal — rain likely', 'cls' => 'spray-marginal']; |
| if ($deltaT >= 2 && $deltaT <= 8 && $wind >= 3 && $wind <= 15 && $tempC >= 10 && $precipProbability < 30) { |
| return ['rating' => 'Ideal', 'cls' => 'spray-ideal']; |
| } |
| return ['rating' => 'Good', 'cls' => 'spray-good']; |
| } |
| function weatherFetch(string $url, int $timeout, array $headers = []): ?string |
| { |
| $headerLines = array_merge([ |
| 'User-Agent: dispelled.ca weather dashboard (local farm conditions)', |
| 'Accept: application/json,text/html,application/xhtml+xml', |
| ], $headers); |
| $context = stream_context_create([ |
| 'http' => [ |
| 'method' => 'GET', |
| 'timeout' => $timeout, |
| 'ignore_errors' => true, |
| 'header' => implode("\r\n", $headerLines), |
| ], |
| ]); |
| $body = @file_get_contents($url, false, $context); |
| if ($body === false) return null; |
| $status = 0; |
| foreach ($http_response_header ?? [] as $line) { |
| if (preg_match('#^HTTP/\S+\s+(\d{3})#', $line, $match)) { |
| $status = (int)$match[1]; |
| } |
| } |
| return $status >= 200 && $status < 300 ? $body : null; |
| } |
| function weatherJsonObjectAt(string $text, int $start): ?array |
| { |
| $depth = 0; |
| $inString = false; |
| $escaped = false; |
| $length = strlen($text); |
| for ($i = $start; $i < $length; $i++) { |
| $char = $text[$i]; |
| if ($inString) { |
| if ($escaped) { |
| $escaped = false; |
| } elseif ($char === '\\') { |
| $escaped = true; |
| } elseif ($char === '"') { |
| $inString = false; |
| } |
| continue; |
| } |
| if ($char === '"') { |
| $inString = true; |
| } elseif ($char === '{') { |
| $depth++; |
| } elseif ($char === '}') { |
| $depth--; |
| if ($depth === 0) { |
| $decoded = json_decode(substr($text, $start, $i - $start + 1), true); |
| return is_array($decoded) ? $decoded : null; |
| } |
| } |
| } |
| return null; |
| } |
| function weatherArdillRecordToObservation(array $record): ?array |
| { |
| if (!isset($record['obsTimeLocal'], $record['obsTimeUtc'], $record['imperial'])) return null; |
| $observed = strtotime((string)$record['obsTimeUtc']); |
| if ($observed === false || time() - $observed > 90 * 60 || !is_array($record['imperial'])) return null; |
| $values = $record['imperial']; |
| $number = static fn($value): ?float => is_numeric($value) ? (float)$value : null; |
| $fToC = static fn(?float $value): ?float => $value === null ? null : ($value - 32) * 5 / 9; |
| $miToKm = static fn(?float $value): ?float => $value === null ? null : $value * 1.609344; |
| $inToMm = static fn(?float $value): ?float => $value === null ? null : $value * 25.4; |
| $inHgToHpa = static fn(?float $value): ?float => $value === null ? null : $value * 33.8638866667; |
| $pressureLow = $number($values['pressureMin'] ?? $values['pressure'] ?? null); |
| $pressureHigh = $number($values['pressureMax'] ?? $values['pressure'] ?? null); |
| $averagePressure = ($pressureLow === null && $pressureHigh === null) |
| ? null |
| : (($pressureLow ?? $pressureHigh) + ($pressureHigh ?? $pressureLow)) / 2; |
| return [ |
| 'observedAt' => (string)$record['obsTimeLocal'], |
| 'tempC' => $fToC($number($values['tempAvg'] ?? $values['temp'] ?? null)), |
| 'humidity' => $number($record['humidityAvg'] ?? $record['humidity'] ?? null), |
| 'dewPointC' => $fToC($number($values['dewptAvg'] ?? $values['dewpt'] ?? null)), |
| 'windKmh' => $miToKm($number($values['windspeedAvg'] ?? $values['windSpeed'] ?? null)), |
| 'windGustKmh' => $miToKm($number($values['windgustAvg'] ?? $values['windGust'] ?? null)), |
| 'windDirection' => $number($record['winddirAvg'] ?? $record['winddir'] ?? null), |
| 'pressureHpa' => $inHgToHpa($averagePressure), |
| 'precipRateMm' => $inToMm($number($values['precipRate'] ?? null)), |
| 'precipTodayMm' => $inToMm($number($values['precipTotal'] ?? null)), |
| 'uv' => $number($record['uvHigh'] ?? $record['uv'] ?? null), |
| ]; |
| } |
| function weatherArdillObservation(): ?array |
| { |
| $stationId = 'IARDIL4'; |
| $body = weatherFetch("https://www.wunderground.com/dashboard/pws/{$stationId}", 10); |
| if ($body === null) return null; |
| $marker = '{"stationID":"' . $stationId . '"'; |
| $start = strrpos($body, $marker); |
| if ($start !== false) { |
| $record = weatherJsonObjectAt($body, $start); |
| $observation = is_array($record) ? weatherArdillRecordToObservation($record) : null; |
| if ($observation !== null) return $observation; |
| } |
| if (!preg_match('/apiKey=([A-Za-z0-9]+)/', $body, $match)) return null; |
| $params = http_build_query([ |
| 'apiKey' => $match[1], |
| 'stationId' => $stationId, |
| 'numericPrecision' => 'decimal', |
| 'format' => 'json', |
| 'units' => 'e', |
| ]); |
| $currentBody = weatherFetch("https://api.weather.com/v2/pws/observations/current?{$params}", 10); |
| if ($currentBody === null) return null; |
| $current = json_decode($currentBody, true); |
| $record = $current['observations'][0] ?? null; |
| return is_array($record) ? weatherArdillRecordToObservation($record) : null; |
| } |
| function getFullWeather(): ?array |
| { |
| $cacheFile = CACHE_DIR . '/weather_full.json'; |
| $cached = null; |
| if (is_file($cacheFile)) { |
| $decoded = json_decode((string)file_get_contents($cacheFile), true); |
| if (is_array($decoded) && empty($decoded['error'])) { |
| $cached = $decoded; |
| if (time() - filemtime($cacheFile) < 900) return $cached; |
| } |
| } |
| $latitude = '49.9007'; |
| $longitude = '-105.79557'; |
| $timezone = new DateTimeZone('America/Regina'); |
| $today = new DateTimeImmutable('today', $timezone); |
| $yesterday = $today->modify('-1 day'); |
| $seasonStart = $today->format('Y') . '-05-01'; |
| $fetchHistory = $yesterday->format('Y-m-d') >= $seasonStart; |
| $weatherParams = http_build_query([ |
| 'latitude' => $latitude, |
| 'longitude' => $longitude, |
| 'current' => 'temperature_2m,relative_humidity_2m,apparent_temperature,weather_code,wind_speed_10m,wind_direction_10m,wind_gusts_10m,precipitation,surface_pressure,cloud_cover,visibility,uv_index,is_day', |
| 'hourly' => 'temperature_2m,relative_humidity_2m,dew_point_2m,precipitation_probability,precipitation,snowfall,weather_code,wind_speed_10m,wind_direction_10m,visibility,uv_index,shortwave_radiation,cape,lifted_index,freezing_level_height,soil_temperature_0cm,soil_temperature_6cm,soil_temperature_18cm,soil_moisture_0_to_1cm,soil_moisture_3_to_9cm,soil_moisture_9_to_27cm', |
| 'daily' => 'weather_code,temperature_2m_max,temperature_2m_min,apparent_temperature_max,apparent_temperature_min,sunrise,sunset,uv_index_max,precipitation_sum,precipitation_hours,precipitation_probability_max,snowfall_sum,wind_speed_10m_max,wind_gusts_10m_max,wind_direction_10m_dominant,et0_fao_evapotranspiration,sunshine_duration,shortwave_radiation_sum', |
| 'temperature_unit' => 'celsius', |
| 'wind_speed_unit' => 'kmh', |
| 'timezone' => 'America/Regina', |
| 'forecast_days' => 7, |
| 'past_days' => 7, |
| ]); |
| $weatherBody = weatherFetch("https://api.open-meteo.com/v1/forecast?{$weatherParams}", 10); |
| if ($weatherBody === null) return $cached; |
| $wx = json_decode($weatherBody, true); |
| if (!is_array($wx) || empty($wx['current']) || empty($wx['daily']) || empty($wx['hourly'])) return $cached; |
| $history = null; |
| if ($fetchHistory) { |
| $historyParams = http_build_query([ |
| 'latitude' => $latitude, |
| 'longitude' => $longitude, |
| 'start_date' => $seasonStart, |
| 'end_date' => $yesterday->format('Y-m-d'), |
| 'daily' => 'temperature_2m_max,temperature_2m_min', |
| 'timezone' => 'America/Regina', |
| ]); |
| $historyBody = weatherFetch("https://archive-api.open-meteo.com/v1/archive?{$historyParams}", 15); |
| if ($historyBody !== null) $history = json_decode($historyBody, true); |
| } |
| $station = weatherArdillObservation(); |
| $seasonGdd = 0.0; |
| $seasonGddDays = 0; |
| if (is_array($history['daily']['time'] ?? null)) { |
| foreach ($history['daily']['time'] as $index => $_date) { |
| $high = $history['daily']['temperature_2m_max'][$index] ?? null; |
| $low = $history['daily']['temperature_2m_min'][$index] ?? null; |
| if ($high !== null && $low !== null) { |
| $seasonGdd += max(0, ((float)$high + (float)$low) / 2 - 10); |
| $seasonGddDays++; |
| } |
| } |
| } |
| $current = $wx['current']; |
| $daily = $wx['daily']; |
| $hourlyData = $wx['hourly']; |
| $currentWmo = weatherWmo((int)$current['weather_code']); |
| $currentTemp = (float)($station['tempC'] ?? $current['temperature_2m']); |
| $currentHumidity = (float)($station['humidity'] ?? $current['relative_humidity_2m']); |
| $currentWind = (float)($station['windKmh'] ?? $current['wind_speed_10m']); |
| $currentWindDirection = (float)($station['windDirection'] ?? $current['wind_direction_10m']); |
| $currentPrecip = (float)($station['precipRateMm'] ?? $current['precipitation'] ?? 0); |
| $todayString = substr((string)$current['time'], 0, 10); |
| $precipHistory = []; |
| foreach ($daily['time'] as $index => $date) { |
| if ($date < $todayString) { |
| $precipHistory[] = ['date' => $date, 'precip' => weatherRound1((float)($daily['precipitation_sum'][$index] ?? 0))]; |
| } |
| } |
| $sevenDayPastTotal = weatherRound1(array_sum(array_column($precipHistory, 'precip'))); |
| $daysSinceRain = 0; |
| for ($index = count($precipHistory) - 1; $index >= 0; $index--) { |
| if ($precipHistory[$index]['precip'] >= 1) break; |
| $daysSinceRain++; |
| } |
| $currentDeltaT = weatherDeltaT($currentTemp, $currentHumidity); |
| $currentSpray = weatherSprayRating($currentDeltaT, round($currentWind), $currentTemp, $currentPrecip, 0); |
| $startIndex = 0; |
| foreach ($hourlyData['time'] as $index => $time) { |
| if ($time >= $current['time']) { |
| $startIndex = $index; |
| break; |
| } |
| } |
| $hourly = []; |
| $end = min($startIndex + 24, count($hourlyData['time'])); |
| for ($index = $startIndex; $index < $end; $index++) { |
| $wmo = weatherWmo((int)$hourlyData['weather_code'][$index]); |
| $temp = (float)$hourlyData['temperature_2m'][$index]; |
| $humidity = (float)($hourlyData['relative_humidity_2m'][$index] ?? 50); |
| $deltaT = weatherDeltaT($temp, $humidity); |
| $spray = weatherSprayRating( |
| $deltaT, |
| round((float)($hourlyData['wind_speed_10m'][$index] ?? 0)), |
| $temp, |
| (float)($hourlyData['precipitation'][$index] ?? 0), |
| (float)($hourlyData['precipitation_probability'][$index] ?? 0) |
| ); |
| $hourly[] = [ |
| 'time' => substr((string)$hourlyData['time'][$index], 11, 5), |
| 'temp' => (int)round($temp), |
| 'rh' => (int)round($humidity), |
| 'dew_point' => weatherRound1((float)($hourlyData['dew_point_2m'][$index] ?? 0)), |
| 'delta_t' => $deltaT, |
| 'spray_rating' => $spray['rating'], |
| 'spray_cls' => $spray['cls'], |
| 'precip_prob' => $hourlyData['precipitation_probability'][$index] ?? 0, |
| 'precip' => weatherRound1((float)($hourlyData['precipitation'][$index] ?? 0)), |
| 'snowfall' => weatherRound1((float)($hourlyData['snowfall'][$index] ?? 0)), |
| 'wind' => (int)round((float)($hourlyData['wind_speed_10m'][$index] ?? 0)), |
| 'wind_dir' => weatherWindDirection((float)($hourlyData['wind_direction_10m'][$index] ?? 0)), |
| 'icon' => $wmo['icon'], |
| 'uv' => weatherRound1((float)($hourlyData['uv_index'][$index] ?? 0)), |
| 'shortwave' => (int)round((float)($hourlyData['shortwave_radiation'][$index] ?? 0)), |
| 'cape' => (int)round((float)($hourlyData['cape'][$index] ?? 0)), |
| 'lifted_index' => weatherRound1((float)($hourlyData['lifted_index'][$index] ?? 0)), |
| 'freezing_level' => isset($hourlyData['freezing_level_height'][$index]) ? (int)round((float)$hourlyData['freezing_level_height'][$index]) : null, |
| 'soil_temp_0' => weatherRound1((float)($hourlyData['soil_temperature_0cm'][$index] ?? 0)), |
| 'soil_temp_6' => weatherRound1((float)($hourlyData['soil_temperature_6cm'][$index] ?? 0)), |
| 'soil_temp_18' => weatherRound1((float)($hourlyData['soil_temperature_18cm'][$index] ?? 0)), |
| 'soil_moist_0' => weatherRound1((float)($hourlyData['soil_moisture_0_to_1cm'][$index] ?? 0) * 100), |
| 'soil_moist_3' => weatherRound1((float)($hourlyData['soil_moisture_3_to_9cm'][$index] ?? 0) * 100), |
| 'soil_moist_9' => weatherRound1((float)($hourlyData['soil_moisture_9_to_27cm'][$index] ?? 0) * 100), |
| 'visibility' => round((float)($hourlyData['visibility'][$index] ?? 0) / 100) / 10, |
| ]; |
| } |
| $forecast = []; |
| $gddCumulative = 0.0; |
| foreach ($daily['time'] as $index => $date) { |
| if ($date < $todayString) continue; |
| $wmo = weatherWmo((int)$daily['weather_code'][$index]); |
| $low = (int)round((float)$daily['temperature_2m_min'][$index]); |
| $high = (int)round((float)$daily['temperature_2m_max'][$index]); |
| $gdd = max(0, weatherRound1(($high + $low) / 2 - 10)); |
| $gddCumulative += $gdd; |
| $forecast[] = [ |
| 'date' => $date, |
| 'high' => $high, |
| 'low' => $low, |
| 'feels_high' => (int)round((float)$daily['apparent_temperature_max'][$index]), |
| 'feels_low' => (int)round((float)$daily['apparent_temperature_min'][$index]), |
| 'label' => $wmo['label'], |
| 'icon' => $wmo['icon'], |
| 'precip_prob' => $daily['precipitation_probability_max'][$index] ?? 0, |
| 'precip_sum' => weatherRound1((float)($daily['precipitation_sum'][$index] ?? 0)), |
| 'precip_hrs' => $daily['precipitation_hours'][$index] ?? 0, |
| 'snowfall_sum' => weatherRound1((float)($daily['snowfall_sum'][$index] ?? 0)), |
| 'wind_max' => (int)round((float)($daily['wind_speed_10m_max'][$index] ?? 0)), |
| 'gust_max' => (int)round((float)($daily['wind_gusts_10m_max'][$index] ?? 0)), |
| 'wind_dir' => weatherWindDirection((float)($daily['wind_direction_10m_dominant'][$index] ?? 0)), |
| 'uv_max' => weatherRound1((float)($daily['uv_index_max'][$index] ?? 0)), |
| 'sunrise' => !empty($daily['sunrise'][$index]) ? substr((string)$daily['sunrise'][$index], 11, 5) : '—', |
| 'sunset' => !empty($daily['sunset'][$index]) ? substr((string)$daily['sunset'][$index], 11, 5) : '—', |
| 'et0' => weatherRound1((float)($daily['et0_fao_evapotranspiration'][$index] ?? 0)), |
| 'sunshine_hrs' => weatherRound1((float)($daily['sunshine_duration'][$index] ?? 0) / 3600), |
| 'solar_sum' => weatherRound1((float)($daily['shortwave_radiation_sum'][$index] ?? 0)), |
| 'gdd' => $gdd, |
| 'gdd_cum' => weatherRound1($gddCumulative), |
| 'frost' => $low <= 2, |
| 'freeze' => $low <= 0, |
| ]; |
| } |
| if ($forecast) { |
| $seasonGdd += $forecast[0]['gdd']; |
| $seasonGddDays++; |
| } |
| $data = [ |
| 'location' => 'Ardill, SK', |
| 'temp' => (int)round($currentTemp), |
| 'feelsLike' => (int)round((float)$current['apparent_temperature']), |
| 'dewPoint' => isset($station['dewPointC']) ? weatherRound1((float)$station['dewPointC']) : weatherDewPoint($currentTemp, $currentHumidity), |
| 'deltaT' => $currentDeltaT, |
| 'spray' => $currentSpray, |
| 'humidity' => (int)round($currentHumidity), |
| 'windSpeed' => (int)round($currentWind), |
| 'windDir' => weatherWindDirection($currentWindDirection), |
| 'windGusts' => (int)round((float)($station['windGustKmh'] ?? $current['wind_gusts_10m'] ?? 0)), |
| 'precip' => weatherRound1($currentPrecip), |
| 'precipLabel' => isset($station['precipRateMm']) ? 'Precip rate' : 'Precip now', |
| 'precipUnit' => isset($station['precipRateMm']) ? 'mm/h' : 'mm', |
| 'precipToday' => isset($station['precipTodayMm']) ? weatherRound1((float)$station['precipTodayMm']) : null, |
| 'pressure' => (int)round((float)($station['pressureHpa'] ?? $current['surface_pressure'] ?? 0)), |
| 'cloud' => $current['cloud_cover'] ?? 0, |
| 'visibility' => round((float)($current['visibility'] ?? 0) / 100) / 10, |
| 'uv' => weatherRound1((float)($station['uv'] ?? $current['uv_index'] ?? 0)), |
| 'label' => $currentWmo['label'], |
| 'icon' => $currentWmo['icon'], |
| 'time' => $station['observedAt'] ?? $current['time'], |
| 'sources' => [ |
| 'current' => $station |
| ? ['label' => 'Ardill weather station IARDIL4', 'observedAt' => $station['observedAt'], 'url' => 'https://www.wunderground.com/dashboard/pws/IARDIL4'] |
| : ['label' => 'Open-Meteo location model for Ardill', 'observedAt' => $current['time'], 'url' => null], |
| 'enrichment' => 'Open-Meteo forecast, soil, history, and unavailable local fields for Ardill', |
| ], |
| 'precipHistory' => $precipHistory, |
| 'sevenDayPastTotal' => $sevenDayPastTotal, |
| 'daysSinceRain' => $daysSinceRain, |
| 'forecast' => $forecast, |
| 'hourly' => $hourly, |
| 'seasonGdd' => weatherRound1($seasonGdd), |
| 'seasonGddDays' => $seasonGddDays, |
| 'seasonGddStart' => $seasonStart, |
| ]; |
| if (!is_dir(CACHE_DIR)) mkdir(CACHE_DIR, 0755, true); |
| file_put_contents($cacheFile, json_encode($data, JSON_UNESCAPED_SLASHES | JSON_UNESCAPED_UNICODE), LOCK_EX); |
| return $data; |
| } |
dispelled