203 lines
7.9 KiB
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']
|
|
];
|
|
}
|
|
}
|