Files
GSB-Construction/app/Services/WeatherService.php

203 lines
7.9 KiB
PHP

<?php
namespace App\Services;
use Illuminate\Support\Facades\Http;
use Illuminate\Support\Facades\Cache;
use Illuminate\Support\Facades\Log;
class WeatherService
{
/**
* Get weather data for a given location string.
*/
public function getWeather(string $location): array
{
$cacheKey = 'weather_' . md5($location);
return Cache::remember($cacheKey, 3600, function () use ($location) {
try {
$coords = $this->geocode($location);
if (!$coords) {
return $this->getFallbackWeather($location);
}
$response = Http::withHeaders([
'User-Agent' => 'GSB-Dashboard/1.0'
])->get('https://api.open-meteo.com/v1/forecast', [
'latitude' => $coords['lat'],
'longitude' => $coords['lon'],
'current_weather' => true,
'daily' => 'temperature_2m_max,temperature_2m_min',
'temperature_unit' => 'fahrenheit',
'wind_speed_unit' => 'mph',
'timezone' => 'auto'
]);
if ($response->failed()) {
return $this->getFallbackWeather($location);
}
$data = $response->json();
$current = $data['current_weather'] ?? null;
$daily = $data['daily'] ?? null;
if (!$current) {
return $this->getFallbackWeather($location);
}
$code = $current['weathercode'] ?? 0;
$weatherDetails = $this->interpretWeatherCode($code);
return [
'condition' => $weatherDetails['condition'],
'temp' => round($current['temperature']) . '°F',
'high' => isset($daily['temperature_2m_max'][0]) ? round($daily['temperature_2m_max'][0]) . '°F' : 'N/A',
'low' => isset($daily['temperature_2m_min'][0]) ? round($daily['temperature_2m_min'][0]) . '°F' : 'N/A',
'forecast' => sprintf(
'%s. Winds at %s mph. Source: Live API.',
$weatherDetails['forecast'],
round($current['windspeed'])
),
'color' => $weatherDetails['color']
];
} catch (\Exception $e) {
Log::warning('Weather API failed: ' . $e->getMessage());
return $this->getFallbackWeather($location);
}
});
}
/**
* Geocode a location string to coordinates using cache and OSM Nominatim.
*/
private function geocode(string $location): ?array
{
$cacheKey = 'coords_' . md5($location);
return Cache::remember($cacheKey, 86400 * 30, function () use ($location) {
// Check major pre-defined keywords
$lowLoc = strtolower($location);
$presetCities = [
'new york' => ['lat' => 40.7128, 'lon' => -74.0060],
'san francisco' => ['lat' => 37.7749, 'lon' => -122.4194],
'chicago' => ['lat' => 41.8781, 'lon' => -87.6298],
'los angeles' => ['lat' => 34.0522, 'lon' => -118.2437],
'london' => ['lat' => 51.5074, 'lon' => -0.1278],
'tokyo' => ['lat' => 35.6762, 'lon' => 139.6503],
'paris' => ['lat' => 48.8566, 'lon' => 2.3522],
'berlin' => ['lat' => 52.5200, 'lon' => 13.4050],
'istanbul' => ['lat' => 41.0082, 'lon' => 28.9784],
];
foreach ($presetCities as $city => $coords) {
if (str_contains($lowLoc, $city)) {
return $coords;
}
}
try {
// Call OSM Nominatim API with descriptive User-Agent
$response = Http::withHeaders([
'User-Agent' => 'GSB-Construction-Management-System/1.0 (admin@example.com)'
])->timeout(3)->get('https://nominatim.openstreetmap.org/search', [
'q' => $location,
'format' => 'json',
'limit' => 1
]);
if ($response->successful() && !empty($response->json())) {
$first = $response->json()[0];
return [
'lat' => (float)$first['lat'],
'lon' => (float)$first['lon']
];
}
} catch (\Exception $e) {
Log::warning('Geocoding request failed: ' . $e->getMessage());
}
return null;
});
}
/**
* Map WMO weather code to condition, forecast description, and color class.
*/
private function interpretWeatherCode(int $code): array
{
return match ($code) {
0 => [
'condition' => 'Clear Sky',
'forecast' => 'Clear, sunny day. Ideal for outdoor operations',
'color' => 'emerald'
],
1, 2, 3 => [
'condition' => 'Partly Cloudy',
'forecast' => 'Mainly clear with some passing clouds',
'color' => 'blue'
],
45, 48 => [
'condition' => 'Foggy',
'forecast' => 'Visibility reduced. Exercise caution on elevated works',
'color' => 'amber'
],
51, 53, 55, 56, 57 => [
'condition' => 'Drizzle',
'forecast' => 'Light drizzle expected. Watch out for slippery surfaces',
'color' => 'blue'
],
61, 63, 65, 66, 67, 80, 81, 82 => [
'condition' => 'Rainy',
'forecast' => 'Moderate to heavy rain. Some outdoor concrete works may be delayed',
'color' => 'blue'
],
71, 73, 75, 77, 85, 86 => [
'condition' => 'Snowing',
'forecast' => 'Snowfall expected. Keep pathways clear and check structural loads',
'color' => 'sky'
],
95, 96, 99 => [
'condition' => 'Thunderstorm',
'forecast' => 'Thunderstorms expected. High risk for crane operations. Stop high works',
'color' => 'amber'
],
default => [
'condition' => 'Scattered Clouds',
'forecast' => 'Generally favorable working conditions',
'color' => 'emerald'
]
};
}
/**
* Fallback mock weather generation based on location hash so it's stable but responsive.
*/
private function getFallbackWeather(string $location): array
{
$hash = crc32($location);
$tempBase = 65 + ($hash % 25); // 65 to 90
$high = $tempBase + 5;
$low = $tempBase - 10;
$conditions = [
['condition' => 'Sunny / Clear', 'forecast' => 'Clear weather. Ideal for all site work.', 'color' => 'emerald'],
['condition' => 'Partly Cloudy', 'forecast' => 'Partly cloudy. Good working conditions.', 'color' => 'blue'],
['condition' => 'Overcast', 'forecast' => 'Overcast skies. Outdoor activities normal.', 'color' => 'blue'],
['condition' => 'Light Rain', 'forecast' => 'Damp conditions. Caution on structural scaffolding.', 'color' => 'blue'],
['condition' => 'Scattered Thunderstorms', 'forecast' => 'Thunderstorms nearby. Secure tower cranes.', 'color' => 'amber'],
];
$selected = $conditions[$hash % count($conditions)];
return [
'condition' => $selected['condition'],
'temp' => $tempBase . '°F',
'high' => $high . '°F',
'low' => $low . '°F',
'forecast' => $selected['forecast'] . ' (Offline Mode)',
'color' => $selected['color']
];
}
}