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'] ]; } }