diff --git a/api/curl_log.txt b/api/curl_log.txt index 6cfe931..283494a 100644 --- a/api/curl_log.txt +++ b/api/curl_log.txt @@ -1,6 +1,16 @@ -2025-10-14 18:45:03 - Script started for lat: 12.7413, lon: -163.8591 -2025-10-14 18:45:03 - Fetching URL: https://api.open-meteo.com/v1/forecast?latitude=12.7413&longitude=-163.8591&hourly=temperature_2m,relative_humidity_2m,wind_speed_10m&models=gfs_global -2025-10-14 18:45:33 - cURL HTTP code: 0 -2025-10-14 18:45:33 - cURL error: Connection timed out after 30000 milliseconds -2025-10-14 18:45:33 - Raw API response: -No response body +Array +( + [success] => + [error] => An unexpected error occurred. + [message] => SQLSTATE[42S22]: Column not found: 1054 Unknown column 'city' in 'SELECT' + [file] => /home/ubuntu/executor/workspace/api/facilities.php + [line] => 165 +) +Array +( + [success] => + [error] => An unexpected error occurred. + [message] => SQLSTATE[42S22]: Column not found: 1054 Unknown column 'city' in 'SELECT' + [file] => /home/ubuntu/executor/workspace/api/facilities.php + [line] => 12 +) diff --git a/api/facilities.php b/api/facilities.php new file mode 100644 index 0000000..6e42394 --- /dev/null +++ b/api/facilities.php @@ -0,0 +1,164 @@ + $y) != ($yj > $y)) && ($x < ($xj - $xi) * ($y - $yi) / ($yj - $yi) + $xi); + if ($intersect) $inside = !$inside; + } + return $inside; +} + +function haversineDistance($lat1, $lon1, $lat2, $lon2) { + $earthRadius = 6371; // km + $dLat = deg2rad($lat2 - $lat1); + $dLon = deg2rad($lon2 - $lon1); + $a = sin($dLat / 2) * sin($dLat / 2) + cos(deg2rad($lat1)) * cos(deg2rad($lat2)) * sin($dLon / 2) * sin($dLon / 2); + $c = 2 * atan2(sqrt($a), sqrt(1 - $a)); + return $earthRadius * $c; +} + +// --- Risk Calculation Functions --- + +function getWildfireRisk($lat, $lon) { + try { + $wildfireUrl = 'https://services3.arcgis.com/T4QMspbfLg3qTGWY/arcgis/rest/services/WFIGS_Interagency_Fire_Perimeters_Current/FeatureServer/0/query?where=1%3D1&outFields=poly_IncidentName,poly_GISAcres,attr_InitialLatitude,attr_InitialLongitude&outSR=4326&f=json'; + $context = stream_context_create(['http' => ['timeout' => 10]]); + $wildfireDataJson = @file_get_contents($wildfireUrl, false, $context); + if (!$wildfireDataJson) throw new Exception('Could not fetch wildfire data from source.'); + + $wildfireData = json_decode($wildfireDataJson, true); + if (!$wildfireData || !isset($wildfireData['features'])) return ['score' => 0, 'details' => 'No active wildfires found or data invalid.']; + + $risk = 0; + $closest_distance = PHP_INT_MAX; + $closest_fire = null; + + foreach ($wildfireData['features'] as $fire) { + $fireLat = $fire['attributes']['attr_InitialLatitude']; + $fireLon = $fire['attributes']['attr_InitialLongitude']; + if ($fireLat && $fireLon) { + $distance = haversineDistance($lat, $lon, $fireLat, $fireLon); + if ($distance < $closest_distance) { + $closest_distance = $distance; + $closest_fire = $fire['attributes']['poly_IncidentName']; + } + } + } + + if ($closest_distance < 50) { // 50km threshold + $risk = 100 - ($closest_distance / 50) * 100; + } + + return ['score' => round($risk), 'details' => $closest_fire ? "Closest fire: {$closest_fire} (" . round($closest_distance) . " km away)" : "No fires within 50km."]; + } catch (Exception $e) { + error_log("Wildfire Risk Error: " . $e->getMessage()); + return ['score' => 0, 'details' => 'Error calculating wildfire risk.']; + } +} + +function getHurricaneRisk($lat, $lon) { + try { + $localApiUrl = 'http://localhost/api/hurricanes.php'; + $context = stream_context_create(['http' => ['timeout' => 15]]); + $geoJsonData = @file_get_contents($localApiUrl, false, $context); + if (!$geoJsonData) throw new Exception('Could not fetch local hurricane data.'); + + $hurricaneData = json_decode($geoJsonData, true); + if (!$hurricaneData || empty($hurricaneData['features'])) return ['score' => 0, 'details' => 'No active hurricane data found.']; + + $risk = 0; + $details = []; + $point = [(float)$lon, (float)$lat]; + + foreach ($hurricaneData['features'] as $feature) { + if (isset($feature['properties']['layer']) && $feature['properties']['layer'] === 'cone' && $feature['geometry']['type'] === 'Polygon') { + $polygon = $feature['geometry']['coordinates'][0]; + if (pointInPolygon($point, $polygon)) { + $risk = 100; + $stormName = $feature['properties']['STORMNAME'] ?? 'Unnamed Storm'; + $details[] = "Inside the forecast cone for " . $stormName; + break; + } + } + } + return ['score' => $risk, 'details' => !empty($details) ? implode(', ', $details) : 'Not in any active hurricane forecast cones.']; + } catch (Exception $e) { + error_log("Hurricane Risk Error: " . $e->getMessage()); + return ['score' => 0, 'details' => 'Error calculating hurricane risk.']; + } +} + +function getHistoricalHurricaneRisk($lat, $lon) { + try { + $pdo = db(); + if (!$pdo) return ['score' => 0, 'details' => 'Database connection failed.']; + + $radius = 1.0; + $stmt = $pdo->prepare("SELECT COUNT(DISTINCT storm_id) as storm_count FROM hurricanes WHERE ABS(lat - :lat) < :radius AND ABS(lon - :lon) < :radius"); + $stmt->execute(['lat' => $lat, 'lon' => $lon, 'radius' => $radius]); + $result = $stmt->fetch(PDO::FETCH_ASSOC); + $count = $result['storm_count'] ?? 0; + $score = min($count * 10, 100); + return ['score' => $score, 'details' => "{$count} historical storms passed within ~111km."]; + } catch (Exception $e) { + error_log("Historical Hurricane Risk Error: " . $e->getMessage()); + return ['score' => 0, 'details' => 'Error calculating historical hurricane risk.']; + } +} + +function calculateRiskScore($lat, $lon) { + $wildfireRisk = getWildfireRisk((float)$lat, (float)$lon); + $hurricaneRisk = getHurricaneRisk((float)$lat, (float)$lon); + $historicalRisk = getHistoricalHurricaneRisk((float)$lat, (float)$lon); + $totalScore = ($wildfireRisk['score'] * 0.4) + ($hurricaneRisk['score'] * 0.4) + ($historicalRisk['score'] * 0.2); + return round($totalScore); +} + +try { + $pdo = db(); + $method = $_SERVER['REQUEST_METHOD']; + + if ($method === 'GET') { + $stmt = $pdo->query('SELECT id, name, latitude, longitude, city, state FROM facilities ORDER BY created_at DESC'); + $allFacilities = $stmt->fetchAll(PDO::FETCH_ASSOC); + + $sampleSize = ceil(count($allFacilities) * 0.1); + if ($sampleSize < 1 && count($allFacilities) > 0) $sampleSize = 1; + + shuffle($allFacilities); + $facilitiesSample = array_slice($allFacilities, 0, $sampleSize); + + $facilitiesWithRisk = []; + foreach ($facilitiesSample as $facility) { + $facility['risk'] = calculateRiskScore($facility['latitude'], $facility['longitude']); + $facilitiesWithRisk[] = $facility; + } + + echo json_encode(['success' => true, 'facilities' => $facilitiesWithRisk]); + + } else { + http_response_code(405); + echo json_encode(['success' => false, 'error' => 'Method Not Allowed']); + } +} catch (Exception $e) { + http_response_code(500); + $error = [ + 'success' => false, + 'error' => 'An unexpected error occurred in facilities API.', + 'message' => $e->getMessage(), + ]; + error_log(print_r($error, true)); + echo json_encode($error); +} +?> \ No newline at end of file diff --git a/api/hurricanes.php b/api/hurricanes.php index 5678950..a1e469b 100644 --- a/api/hurricanes.php +++ b/api/hurricanes.php @@ -1,43 +1,57 @@ prepare('SELECT name, season, lat, lon, wind_speed FROM hurricanes WHERE storm_id = ? ORDER BY iso_time ASC'); - $stmt->execute([$hurricaneId]); - $trackData = $stmt->fetchAll(PDO::FETCH_ASSOC); +// Function to fetch data from a URL +function get_json($url) { + $ch = curl_init(); + curl_setopt($ch, CURLOPT_URL, $url); + curl_setopt($ch, CURLOPT_RETURNTRANSFER, true); + curl_setopt($ch, CURLOPT_USERAGENT, 'worldsphere.ai bot'); + curl_setopt($ch, CURLOPT_TIMEOUT, 15); + $response = curl_exec($ch); + $http_code = curl_getinfo($ch, CURLINFO_HTTP_CODE); + curl_close($ch); - if (!$trackData) { - http_response_code(404); - echo json_encode(['error' => 'Hurricane not found']); - exit; + if ($http_code !== 200) { + return null; + } + return json_decode($response, true); +} + +// Main function to get all active hurricane data +function get_active_hurricanes() { + global $baseUrl; + $features = []; + + // Layer IDs for forecast points, tracks, and cones + $layerIds = [ + 0, // Forecast Position + 2, // Forecast Track + 4, // Forecast Error Cone + ]; + + foreach ($layerIds as $layerId) { + $queryUrl = "{$baseUrl}{$layerId}/query?where=1%3D1&outFields=*&f=geojson"; + $data = get_json($queryUrl); + + if ($data && !empty($data['features'])) { + foreach($data['features'] as $feature) { + $features[] = $feature; + } } - - // Format for existing frontend: [lon, lat, wind] - $formattedTrack = array_map(function($point) { - return [(float)$point['lon'], (float)$point['lat'], (int)$point['wind_speed']]; - }, $trackData); - - echo json_encode([ - 'name' => $trackData[0]['name'] . ' (' . $trackData[0]['season'] . ')', - 'track' => $formattedTrack - ]); - - } else { - // Fetch list of all hurricanes - $stmt = $pdo->query('SELECT storm_id as id, name, season as year FROM hurricanes GROUP BY storm_id, name, season ORDER BY season DESC, name ASC'); - $hurricanes = $stmt->fetchAll(PDO::FETCH_ASSOC); - echo json_encode($hurricanes); } -} catch (PDOException $e) { - http_response_code(500); - echo json_encode(['error' => 'Database error: ' . $e->getMessage()]); + return [ + 'type' => 'FeatureCollection', + 'features' => $features + ]; } + +$data = get_active_hurricanes(); +echo json_encode($data); + ?> \ No newline at end of file diff --git a/api/risk_score.php b/api/risk_score.php new file mode 100644 index 0000000..af4f16f --- /dev/null +++ b/api/risk_score.php @@ -0,0 +1,210 @@ + $y) != ($yj > $y)) + && ($x < ($xj - $xi) * ($y - $yi) / ($yj - $yi) + $xi); + if ($intersect) { + $inside = !$inside; + } + } + + return $inside; +} + +/** + * Haversine formula for distance between two points on a sphere. + * @param float $lat1 + * @param float $lon1 + * @param float $lat2 + * @param float $lon2 + * @return float Distance in kilometers. + */ +function haversineDistance($lat1, $lon1, $lat2, $lon2) { + $earthRadius = 6371; // km + + $dLat = deg2rad($lat2 - $lat1); + $dLon = deg2rad($lon2 - $lon1); + + $a = sin($dLat / 2) * sin($dLat / 2) + + cos(deg2rad($lat1)) * cos(deg2rad($lat2)) * + sin($dLon / 2) * sin($dLon / 2); + + $c = 2 * atan2(sqrt($a), sqrt(1 - $a)); + + return $earthRadius * $c; +} + + +// --- Risk Calculation Functions --- + +function getWildfireRisk($lat, $lon) { + $wildfireUrl = 'https://services3.arcgis.com/T4QMspbfLg3qTGWY/arcgis/rest/services/WFIGS_Interagency_Fire_Perimeters_Current/FeatureServer/0/query?where=1%3D1&outFields=poly_IncidentName,poly_GISAcres,attr_InitialLatitude,attr_InitialLongitude&outSR=4326&f=json'; + + $context = stream_context_create(['http' => ['timeout' => 10]]); + $wildfireDataJson = @file_get_contents($wildfireUrl, false, $context); + if (!$wildfireDataJson) { + return ['score' => 0, 'details' => 'Could not fetch wildfire data.']; + } + $wildfireData = json_decode($wildfireDataJson, true); + + if (!$wildfireData || empty($wildfireData['features'])) { + return ['score' => 0, 'details' => 'No active wildfires found.']; + } + + $risk = 0; + $closest_distance = PHP_INT_MAX; + $closest_fire = null; + + foreach ($wildfireData['features'] as $fire) { + $fireLat = $fire['attributes']['attr_InitialLatitude']; + $fireLon = $fire['attributes']['attr_InitialLongitude']; + + if ($fireLat && $fireLon) { + $distance = haversineDistance($lat, $lon, $fireLat, $fireLon); + if ($distance < $closest_distance) { + $closest_distance = $distance; + $closest_fire = $fire['attributes']['poly_IncidentName']; + } + } + } + + if ($closest_distance < 50) { // 50km threshold + $risk = 100 - ($closest_distance / 50) * 100; + } + + return [ + 'score' => round($risk), + 'details' => $closest_fire ? "Closest fire: {$closest_fire} (" . round($closest_distance) . " km away)" : "No fires within 50km." + ]; +} + +function getHurricaneRisk($lat, $lon) { + // Construct the URL to the local hurricanes API + $localApiUrl = 'http://localhost/api/hurricanes.php'; + + $context = stream_context_create(['http' => ['timeout' => 15]]); + $geoJsonData = @file_get_contents($localApiUrl, false, $context); + + if (!$geoJsonData) { + return ['score' => 0, 'details' => 'Could not fetch local hurricane data.']; + } + + $hurricaneData = json_decode($geoJsonData, true); + + if (!$hurricaneData || empty($hurricaneData['features'])) { + return ['score' => 0, 'details' => 'No active hurricane data found.']; + } + + $risk = 0; + $details = []; + $point = [(float)$lon, (float)$lat]; + + foreach ($hurricaneData['features'] as $feature) { + // We are only interested in the forecast cone polygons + if (isset($feature['properties']['layer']) && $feature['properties']['layer'] === 'cone' && $feature['geometry']['type'] === 'Polygon') { + $polygon = $feature['geometry']['coordinates'][0]; + if (pointInPolygon($point, $polygon)) { + $risk = 100; // Max risk if inside the cone + $stormName = $feature['properties']['STORMNAME'] ?? 'Unnamed Storm'; + $details[] = "Inside the forecast cone for " . $stormName; + break; // Exit after finding the first cone + } + } + } + + return [ + 'score' => $risk, + 'details' => !empty($details) ? implode(', ', $details) : 'Not in any active hurricane forecast cones.' + ]; +} + +function getHistoricalHurricaneRisk($lat, $lon) { + $pdo = db(); + if (!$pdo) { + return ['score' => 0, 'details' => 'Database connection failed.']; + } + + // Search for storms within a 1-degree radius (~111 km) + $radius = 1.0; + $stmt = $pdo->prepare( + "SELECT COUNT(DISTINCT storm_id) as storm_count FROM hurricanes WHERE ABS(lat - :lat) < :radius AND ABS(lon - :lon) < :radius" + ); + $stmt->execute(['lat' => $lat, 'lon' => $lon, 'radius' => $radius]); + $result = $stmt->fetch(PDO::FETCH_ASSOC); + + $count = $result['storm_count'] ?? 0; + + // Simple scoring: 10 points per storm, max 100 + $score = min($count * 10, 100); + + return [ + 'score' => $score, + 'details' => "{$count} historical storms passed within ~111km." + ]; +} + + +// --- Main Execution --- + +$lat = $_GET['lat'] ?? null; +$lon = $_GET['lon'] ?? null; + +if (!$lat || !$lon) { + http_response_code(400); + echo json_encode(['error' => 'Latitude and longitude are required.']); + exit; +} + +$wildfireRisk = getWildfireRisk((float)$lat, (float)$lon); +$hurricaneRisk = getHurricaneRisk((float)$lat, (float)$lon); +$historicalRisk = getHistoricalHurricaneRisk((float)$lat, (float)$lon); + +// Weighted average for the total score +$totalScore = ($wildfireRisk['score'] * 0.4) + ($hurricaneRisk['score'] * 0.4) + ($historicalRisk['score'] * 0.2); + +$response = [ + 'latitude' => (float)$lat, + 'longitude' => (float)$lon, + 'risk_score' => round($totalScore), + 'factors' => [ + 'wildfire' => $wildfireRisk, + 'live_hurricane' => $hurricaneRisk, + 'historical_hurricane' => $historicalRisk, + ] +]; + +echo json_encode($response, JSON_PRETTY_PRINT); diff --git a/api/wind.json b/api/wind.json index c3b2242..87a8911 100644 --- a/api/wind.json +++ b/api/wind.json @@ -1 +1 @@ -[{"header":{"nx":36,"ny":18,"lo1":-180,"la1":90,"dx":10,"dy":10,"parameterCategory":2,"parameterNumber":2,"forecastTime":0,"refTime":"2025-10-14T18:25:20.0000"},"data":[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0]},{"header":{"nx":36,"ny":18,"lo1":-180,"la1":90,"dx":10,"dy":10,"parameterCategory":2,"parameterNumber":3,"forecastTime":0,"refTime":"2025-10-14T18:25:20.0000"},"data":[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0]}] \ No newline at end of file +[{"header":{"nx":36,"ny":18,"lo1":-180,"la1":90,"dx":10,"dy":10,"parameterCategory":2,"parameterNumber":2,"forecastTime":0,"refTime":"2025-10-17T01:04:06.0000"},"data":[15.631140304931264,-18.566470520431366,-18.566470520431366,-18.566470520431366,-18.566470520431366,-18.566470520431366,-18.566470520431366,-18.566470520431366,-18.566470520431366,-18.566470520431366,-18.566470520431366,-18.566470520431366,-18.566470520431366,-18.566470520431366,-18.566470520431366,-18.566470520431366,-18.566470520431366,-18.566470520431366,-18.566470520431366,-18.566470520431366,-18.566470520431366,-18.566470520431366,-18.566470520431366,-18.566470520431366,-18.566470520431366,-18.566470520431366,-18.566470520431366,-18.566470520431366,-18.566470520431366,-18.566470520431366,-18.566470520431366,-18.566470520431366,-18.566470520431366,-18.566470520431366,-18.566470520431366,-18.566470520431366,14.397806810252035,16.178292833385008,12.959121861643629,14.429919596680659,28.7416827761431,20.521610780689958,13.289195940914214,1.1090110249868483,-19.062384973486154,1.761396867937202,6.855693556378477,-4.667902132486008,14.740222359306339,9.404564036679584,4.445393348273419,-14.749999999999977,-11.615029946834511,-6.145870741395559,-18.031222920256972,-33.00872062702496,-45.886342477544524,-31.12642476746552,-14.31939497607957,7.893418014601631,8.274112912870576,17.579856113501858,23.76643364979769,26.551994420813354,12.578604399058943,16.425262206400753,9.125209044764896,5.094649670456408,10.037637416962871,-8.227681403987694,11.485038046337667,2.4724035165450187,10.365732658050018,-24.8,-4.476525981442919,-2.79080796397787,1.2459120322792958,-6.263420891480999,11.765492930402985,-6.532584737520286,6.811735915548056,21.63186405613234,10.064046815345089,6.157082177128489,0.3908743630680098,7.190132650232932,20.543917484378866,6.348579011914556,22.698127676088202,18.107229200138004,-15.193623209846594,-18.9853800432196,-22.91025971044415,-18.15961998958189,-31.58296462013812,45.37207772584746,44.96841694648919,31.313907151980477,24.656914697595475,39.656861636759075,2.1976149610516402,-3.914148345583247,6.817795543758906,-2.5655252266597666,-0.6474842788232326,-17.369610557542718,-5.728786224524993,-10.771960632878907,-10.785062665736136,-23.938142438193722,-2.535709570444192,-4.345436483624567,0.7045403848156647,9.599999999999994,2.907803098047883,7.9062875991444335,1.7191169589026114,15.476313304606935,37.35638546162586,2.910887178827989,-22.32175649745011,-15.506706947343663,-19.885865653018755,-2.2322071598120186,-0.7293994810822845,2.893608934030595,-22.479085160433645,3.5223537904426645,-9.062123666477303,-9.266467851559028,10.060016436106814,9.659258262890685,8.273435647318815,10.800307988846612,5.78819864428265,3.1974091017491033,2.164994795668674,3.549999999999998,-5.0886554215837965,-1.8376152202278933,-1.076618701534965,-6.0916401620029,-13.98986857575662,-20.524802069691198,-30.188422279787645,37.442535593732934,12.269756073814623,4.013892989477893,6.165368372530882,-5.278904601074667,2.0069464947389464,1.469229264399401,-9.3568256707543,12.244675899951542,5.046787864462486,-14.4,-21.910442715746296,-12.68427486009214,-8.661379172793739,8.323222045346812,22.87802836333199,10.571854511586336,-5.578786147727858,-1.0549740469210382,7.919595949289341,5.750175672340511,4.705788684431239,-0.4013442120787531,3.633958072659535,5.874393838195725,0.34345619167777963,-5.362307134053096,-6.858073366652385,3.092233167847067e-15,-12.592324420440606,-2.4853631043215887,-3.9467090073008158,-10.393448017814672,-7.9383558475295795,-23.234675550938416,-22.38466433911759,-8.773965628975434,33.23554887129704,13.97612142582763,-1.7712114122026845,-32.08468542722687,5.046787864462486,-2.4772811970891473,25.114736709748712,5.769234480931405,-8.651355263444872,-49.33145471270043,12.730856928272557,-29.660086502754933,24.20780752359455,9.063305296580207,17.85272012079621,-6.798696465013737,-4.642135200797147,-17.31747020136003,-1.1330246557195571,5.0627766939884244,-7.548139798310009,-29.183110742580133,-7.548139798310009,9.335804264972015,-5.388744221607772,-12.020128301138287,2.9731371786939476,-13.69406641027328,0.36288284339950116,-3.197393327477187,-9.383039235653806,-23.37968382673736,4.356118060050049,-15.09840383788765,1.7489675652259422,3.659615533584649,-0.9038969645947926,-4.105606478321915,-18.162564295837424,-29.594731416258615,-29.248790264462578,7.494262791219219,10.419875556884948,7.5481397983100065,-28.543671113380338,-28.99046901471671,-7.694523159936183,37.926739650580316,18.5265188941388,9.178723948706093,0.34555764745821566,-9.636084130697562,-4.766900247265243,5.7247447850283795,12.241509132410853,-3.233592155403522,-4.664264528860345,-8.302838622659698,-5.8156061960957715,-4.7,-5.34744757120448,-1.0549740469210394,3.209533994354169,0.7128052193506941,-7.347880794884119e-16,-5.904038040675632,-11.924411367519795,11.923183445247115,13.085113941215461,-15.344878891196684,-2.036889033328467,-3.4123416153441206,1.194033675642913,-6.430263144189583,-13.029527342524997,-13.343656357868161,-18.735813371014522,-14.09521775338918,1.2246467991473533e-15,-4.981083229470293,-13.305703520511367,-4.7409625200523875,22.48278837365766,21.98844890922767,4.981192763194492,-10.374925873271607,-30.828825736917537,1.1133848076805237,1.0886485301985036,0.688311646177015,-15.459257596345674,-19.8515246001705,-7.947845481490624,-10.513876985422744,-8.367841582465859,-12.60970042210831,-2.8991378028648453,-2.9055391983331003,-2.1874299874788528,-2.4137096695703315,-12.794828470166255,-5.9734973117424035,-1.5857794741612126,-1.0051055050320474,-5.426495130343531,-6.973482684659823,9.918584156444377,2.9529542176996406,0.7154396824012763,3.5767217640674525,-14.8556297014921,-7.539983724772137,-12.279324052395472,2.1532374030699377,12.216844969986582,6.446249458251704,5.3989495089621515,-2.180852323535913,12.382715069002499,-5.675989159324843,-5.085105302753302,-4.403707847473605,-15.542919229139416,2.4905416147351485,4.676976310969129,3.9467090073008153,3.5748714227020684,1.0494494745709935,0.4045875477159291,5.4091846272187825,-13.719512263474263,-16.55320897903347,-1.0975488643103604,8.684621396487758,-13.347504918136176,-14.462721217947138,-1.7546182238821488,15.539877634317115,0.9422864109675346,-6.45931461251113,-2.9529542176996446,-3.363340998364967,2.4137096695703266,-4.371989072956051e-15,-0.36999101647041016,-0.868997467892291,9.013854005489096,12.190405295662268,15.154346248410926,12.900622960185713,7.171881598543045,22.650578319280783,-0.28966582263075225,0.7263847995832753,1.089577199374912,-2.9055391983331003,11.355068758412184,19.416407864998735,20.905226363192508,23.454785380894975,20.83454389563006,0.3535533905932737,1.11094064359043,1.417191642366352,14.360376748884748,10.093247807352347,-2.144031271592381,5.01843233457429,0.48343165831275625,9.752576548893726,-0.7263847995832757,3.233246532964664,1.7191169589026067,4.342310698243879,0.6910100347095254,1.1393602496174176,-13.289195940914201,-5.435802220184564,-7.571776238003074,-11.899669099704655,-6.07991022532108,-16.60233505397004,3.395823663425597,8.826185154744872,10.815799573111137,2.4137096695703266,8.069459050347534,15.637801473271725,19.416407864998735,-2.8833635269318485,0.3535533905932742,-2.1920310216782988,10.111626970967627,11.646958360222236,13.441150820031007,17.30527931697782,15.931492209867896,8.741911491736937,-0.7117525274902309,-1.0494494745710001,4.685893304218875,24.938235499477976,18.962373440030284,20.686225570942128,9.023367381490722,9.05866657858824,23.333190231621373,16.926322947955082,5.3887442216077694,11.704509619268366,12.37158986937194,3.230844089787558,10.73943250042598,12.56692902780532,19.552541357039456,7.072233425655851,2.7417100889132895,18.54737849125521,-13.37972028629891,6.629807066588041,8.6351515192795,-1.024378940023003,-2.393447315115535,-1.776983875550107,13.383604191406343,1.761649323676896,-6.145870741395559,1.4142135623730947,-1.7677669529663698,0.5148459898998523,7.790453257585886,16.066851532943286,24.954030933906747,23.47199757882436,7.9268888068611,12.920645690537338,24.16210876558583,-29.07166458127628,2.46240352316762,7.256118009539773,29.698484809834987,28.095464026171705,11.486041829408302,24.912908406197182,3.580278823325784,0.5898014942722716,-10.558280535656117,17.279060717246058,20.1382525135948,11.800080080376059,19.14650095181075,38.12476994697232,-16.972539193066606,-0.7782587764657868,-1.0938214854775359,16.150941378401587,26.218754634342652,8.85168835376891,-23.085928104141114,-6.052094287760657,29.489473594403698,-7.347880794884119e-16,-8.625263508510766,-2.1874299874788528,0.7352419856890842,-17.30018116159109,5.8827464652014925,7.619257005540833,15.770815478134109,20.98092633019306,-3.5499933986218797,0.3441558230885075,39.11457836365047,20.24999999999999,-36.73894535031226,-7.8934180146016315,17.673001847214675,29.66692866271868,1.783364281497795,-23.542394536310965,-15.80476191217479,7.276809223181301,29.53730029243772,-20.46177136048994,-12.229903587113343,3.701694510304958,18.764966572593764,25.738686835190357,-10.777488443215537,0.3699910164704127,-10.255396865400394,2.4903061126652877,17.9748288143609,-1.745625336569803,-35.278496193774075,-15.524731620415945,2.877261607343093,3.87037385508924,12.238826880281591,11.149766905249093,-17.010936364007325,-23.04372956100194,-2.1490212066910037,13.85252823853486,-20.9620496488392,-14.886855995441316,-6.582862246223436,20.13825251359479,55.36085473314163,-2.310079038215129,-52.18712722268076,-18.669940412954993,30.587740034811326,14.154435179037845,13.45994931152341,-24.03432798500502,-17.718927921218935,7.11057982596736,3.9363797919862855,5.654737533998875,-42.55556164035508,-11.869106705587738,-21.794416226528284,30.212527350768674,-5.705346533499433,26.597936042437425,-9.619743718013261,6.079910225321092,28.58840634568581,23.475423405530986,-30.909464183188263,-34.16947814454687,-16.619293322620518,2.7957356604375803,13.413407963634858,-8.994517443171862,-23.2862662219125,-20.805211137574016,-7.143405644829557,26.413641358793754,-5.299069159057591,-32.99945507745525,-28.90825239209268,-12.542746132605364,-20.696283330571006,5.772490067120012,25.210672251689576,-22.623286051429613,34.56035559908081,26.753312063625486,-15.129806469249067,-12.938717261796304,17.83825362071682,25.030850440628647,-18.89470767648472,-1.31920097535451,-12.771054268020249,-49.18018092224223,-32.48859884210261,0.6910100347095174,-24.854929088613392,-9.363214929560899,7.489890145496769,30.40385393817332,24.249999999999986,38.70748271043993,-43.70312734751073,-26.997497682907554,4.98445614527056,-5.305790424601664,-22.29189329476226,-17.36645926147494,12.573316505307838,17.3,16.174543075434908,39.19402965013054,40.66136973819088,15.142417919667615,-6.46535998165333,-9.772803256173448,-34.553276857357766,-25.056358063396026,-16.888446590311162,-41.78619298409966,8.012711868593264,14.980617265611835,7.25611800953979,10.21348656204105,8.346305398188573,21.889118183677315,-1.3436306230462602,16.449780509398398,-25.34123854877912,-34.15073006308553,1.5076582575480555,5.564203598792352,6.251698055828376,6.643865379061365,21.795995950261123,32.66408035866863,16.869124423923044,1.6995369835793288,-1.8904004384658033,5.506524307543258,-11.349762493488672,-1.4375439180851262,-7.210124886344504,-6.713443347838232,11.736789069647271,22.291893294762264,19.89493661329847,16.07344621026743,14.779717114367694,24.884084204448207,22.95255488922952,-4.505951900053931,-8.05213960489022,-11.870291211743126,19.457157176333837,46.52012084203099,12.394049257547506,8.398824245181613,23.334747519990177,33.90763671750119,28.382349636347673,35.33230847922444,20.94050857194948,4.383933860207192,9.476700578344996,48.646466228152434,0.7423384793347542,2.187429987478853,2.4807362023529986,-3.94023344208421,-4.716281451675525,-13.64991608232413,-10.806465139037947,-3.939231012048832,17.196374960382123,19.605526766314565,47.28064521640331,9.31394178753759,5.7801404222038135,10.081199072728626,15.531726852477535,-4.383711467890778,-8.789917683469703,-15.825159168222147,-24.452590399217808,-28.512602452744307,-29.97619460307048,-23.01313833586671,-16.9,-0.34862297099063105,14.788937659455698,17.57263980542989,18.002044413295977,7.173049895884837,-1.4500000000000013,-0.7212463965468049,3.571702822414786,4.917561856947894,5.935145605871548,-1.549144304791137,6.101049646137003,8.66025403784439]},{"header":{"nx":36,"ny":18,"lo1":-180,"la1":90,"dx":10,"dy":10,"parameterCategory":2,"parameterNumber":3,"forecastTime":0,"refTime":"2025-10-17T01:04:06.0000"},"data":[-24.069845300033677,-21.35828112030444,-21.35828112030444,-21.35828112030444,-21.35828112030444,-21.35828112030444,-21.35828112030444,-21.35828112030444,-21.35828112030444,-21.35828112030444,-21.35828112030444,-21.35828112030444,-21.35828112030444,-21.35828112030444,-21.35828112030444,-21.35828112030444,-21.35828112030444,-21.35828112030444,-21.35828112030444,-21.35828112030444,-21.35828112030444,-21.35828112030444,-21.35828112030444,-21.35828112030444,-21.35828112030444,-21.35828112030444,-21.35828112030444,-21.35828112030444,-21.35828112030444,-21.35828112030444,-21.35828112030444,-21.35828112030444,-21.35828112030444,-21.35828112030444,-21.35828112030444,-21.35828112030444,-0.25131465269689607,7.890680642203392,2.9918490227734025,2.804892232035212,3.0208725884351697,1.7954083006017696,10.761378686952211,9.032169979936029,3.0191851752764793,5.421022142882375,-5.356254835333232,1.7918397477265045,7.189286807340868,12.944271909999149,25.211078477112526,25.547749411640954,15.413665343912768,-2.116193003971519,18.03122292025695,35.397519158367814,39.888388962622855,24.3186282753635,15.903299265216232,2.8729692039356194,-6.464445490919418,-9.744673366951373,3.3401544230415485,13.528916892238511,17.31296367962386,20.28350959402695,17.909231136186193,12.609700422108306,8.725585085573744,9.80536887192293,-2.2324652459055723,-6.1194134401407965,11.924411367519797,9.111372185656307e-15,-19.389964289226178,-22.729306872586275,-35.67825252458172,-23.375404996195453,-32.32542615503525,-17.94812905701085,-2.891410350820633,-9.182181519497956,-6.535668420180324,11.107670280670321,11.193177262613872,-0.37681888494919946,3.253836872836797,18.437612224186676,22.69812767608815,20.829984414148505,25.28643537071232,27.11393266596565,22.910259710444127,35.6402609675347,35.076435762533,29.46497179431294,18.168419780671726,6.086806952511787,16.012387629441783,9.155507911795299,4.712800492590579,16.95403912726309,5.720809726210199,16.19808878576026,37.094349490302115,37.249250047206324,6.143371093601027,5.727553065987869,5.029157314714322,25.670514926832862,5.437846722219902,10.75533271297474,-10.075396907624226,16.627687752661224,4.3109953772862175,-9.422346650363432,9.74959675482086,4.4377614286536895,-9.313992980587212,0.7257656867990047,3.5354189099092457,-2.4560211011316224,-25.452747341497123,-31.922049608314374,-20.887268284699097,-3.7036505419515926,3.159229391793498,2.466378676309498,7.08010696624507,4.721501197291282,4.691062705321765,2.5881904510252025,-5.79312200714557,-6.489479743866687,-2.2218812871808646,-1.4235782507653005,2.490541614735147,6.148780366869516,7.544241910250879,6.858073366652386,6.105808068675691,-0.31924933308195574,22.38846973852964,11.849999999999994,22.748607875147457,-12.892498916503438,8.276054971367044,38.189640782141694,43.86887544525156,29.938155691571126,19.094820391070847,7.558529312547011,-16.880160347790344,-6.787334683448717,-5.412017391981866,5.290474172316566e-15,-12.650000000000004,-8.555651423102425,9.288192010563456,7.494262791219215,6.560169068444584,-27.54062258166746,-24.16437760667383,2.4853631043215896,7.919595949289322,4.333068166694745,5.046340541172278,11.492994510719601,1.1110124779463992,-12.597678239809435,-1.7669289302057953,4.342310698243882,1.8376152202278975,10.1,-0.4397336584515113,1.0549740469210398,1.4364846019678086,24.485429101834917,27.684336843023583,22.437465365825624,34.469360342556946,32.74488551119942,18.422765569360813,10.53176290516084,10.045039080724523,4.509208471106138,-5.412017391981866,17.626771623599954,-14.500000000000012,-10.407974514958813,2.480736202352984,21.96377872609326,-28.593972824213406,-25.78311983932694,-22.574145718068692,10.801226647977586,-18.48703286870335,-3.61493103345136,0.7352419856890908,9.599230480877488,-12.950531075192693,2.1490212066909993,3.2039952536120464,-13.608308028050518,3.2039952536120464,-3.5836794954530076,-3.6347538725598536,-14.325031086324886,-13.987510690493423,2.9107636714486276,-1.4554435894139948,-11.1506356728845,1.4861274178822048,-43.97079012437456,-30.995390551611152,8.027963723638736,-3.9282454678631833,-10.05471104240922,-25.884222419794575,-29.212908027876324,-24.99862512618587,-10.190283234509007,5.157350876707835,8.323222045346808,-10.790097005079776,-3.203995253612052,-17.150767894504803,21.845885340419382,23.681307255749324,26.556589003053432,26.45861103053442,32.01001447474601,19.79698436409655,13.761754344055072,-9.355568503977862,-1.4273391840380405,-5.450271017215722,-1.0506577808748223,2.9145559532826244,5.391926446648768,-8.621990754572431,1.726751986797768e-15,0.7515347451843564,-2.4853631043215887,3.9634444034305525,0.3631923997916379,-4,-33.48346360241507,10.36573265805002,19.081082163519582,-19.399479197787983,-30.116020517566827,-29.128870267586866,-27.791292245957017,-13.647867363856914,-33.08083608218627,-25.571887244206156,-20.547428914662888,-6.08763478918647,11.827292018232342,4,4.329989591337349,-15.857119972562842,16.533701170139086,24.96966613604043,4.274121496434609,-14.46643420666955,-18.716861732783073,-15.7080712909883,-7.922144549932563,-4.366330768241984,-4.3458286986186065,-7.539983724772125,1.3881538275081202,3.5386087947594693,-5.357087896926646,-17.944894183325665,-5.094649670456404,-2.8991378028648445,-5.702441754805554,0.7107390870623799,-13.688827766869691,-37.15888002105315,-33.877386703619955,-30.258474903063586,-28.782455818149955,-25.529652379152328,-30.20547200834229,-27.251086002791347,-9.658678035226657,-20.487511953891463,-15.49248403008524,-21.21603794708488,-15.459257596345669,1.7257464519048185,12.21161613735138,16.212300853960045,18.72126779686647,-1.0494494745709957,3.2332465329646625,-26.554818160173845,-26.703429500032893,-10.426010937070334,-8.642763284627167,-12.586407820996753,2.1649947956686733,1.0797650608505511,1.4364846019678097,2.894873798829249,-5.398949508962152,5.78587149150698,1.4493866525741133,4.993494092554763,13.404524329361548,-10.442479901366871,-10.724614268106198,-9.003005745879022,-17.235999970177,-7.600086505324834,-8.262699505031678,-26.983552329515586,-25.906895891569103,-9.658678035226657,-21.23529932279546,-13.688827766869693,-23.8,-21.196771137315494,-24.88483159277548,-16.952593782891398,-18.07301908169992,-12.271747625471837,-10.446718491427308,-3.9754388860199623,-3.9939080863394194,8.294943864258496,1.4256104387013884,-2.1384156580520832,-5.702441754805554,-31.197795010092165,-14.10684605501936,-16.928718519240636,-6.725551481934794,-4.428519014418286,-0.35355339059327384,2.894099322141326,6.1385314081469815,-3.5804440548750844,2.894192236078493,24.506389573056943,28.46094406205281,27.695781155832037,10.458357933154142,1.4256104387013881,2.1808523235359103,-9.74959675482086,-5.362307134053099,-19.787938374978097,-10.840288659514181,-10.761378686952224,-11.14504617410967,-7.840803823691295,8.967043844965533,8.683011669463319,-17.192221228093754,-24.162540877294315,-19.823936430844437,-18.00051331474436,-13.688827766869693,-26.39401126457978,-20.015473141610734,-14.10684605501936,-1.8017254983928963,0.35355339059327334,2.192031021678296,-10.111626970967633,-30.341363861159067,-31.66536695876395,-28.800821303590975,-12.901068016521675,10.418204426418098,1.7616493236768969,-5.3989495089621515,-1.4326213531414078,-25.824298832157595,-24.270731211087032,-26.477161321185854,-42.45160587184717,-33.80740392011739,-27.807413285218903,-19.471507169747525,-3.6347538725598576,-23.99780103618776,-35.92970587277404,-2.524212048835201,-29.506348292677526,-20.91488213713154,-30.10827338924073,-16.661137847489176,-19.508280954208935,-19.206372669042,-12.920645690537352,-17.27123789019824,-1.0602632876247886,-9.74631457460908,-15.111631611105606,-16.90687222126065,-15.396075436544553,0.7117525274902325,-2.116193003971519,-1.4142135623730954,1.7677669529663678,-29.49550700711354,-29.074367371300955,-24.740781754390017,-16.831706394469496,-6.289302795991253,-6.4190679887083455,-13.379720286298923,-13.950000000000012,-8.888099823571185,-35.21401097417179,-34.13735126560982,-29.698484809835,-18.245407673003402,-6.9015102037947305,-3.058920519469218,-0.3763024677635523,-16.889704976622717,-17.5719296643933,-25.617260991950797,-24.868670766623065,-27.7992465742637,-8.127207472576915,6.038370350552902,-2.0839657722280265,-22.286415442525833,-20.871357276370592,-6.199765527133703,11.67334165627545,23.059436534480884,0.8061783738277718,-19.79550844843483,-20.648751708637676,-4,-6.499602250042119,-0.7107390870623798,-4.6421352007971475,-10.810352990357377,-16.162713077517626,-17.94984464232259,-8.03563184538998,-18.238441005936107,-7.612985411107858,-2.1729143493093033,20.79759023141494,35.07402885326977,5.1633220456184485,-2.8729692039356167,8.241056103943642,31.813886020433923,51.06887126067579,17.104550841710974,-8.403540973867445,-13.68568768931337,3.104495359049292,-4.723972141221164,-4.694620139043436,-19.043567358884683,7.203195785860521,25.738686835190297,7.269507745119717,21.196771137315494,29.783835131378495,35.61303659427573,29.91513879450371,16.608515652650166,1.231952233598283,3.870750329594674,3.1955227495527927,10.082668606169781,-4.944807033090043,10.767204742114458,12.818659993138647,1.6113745434893256,5.062776693988423,28.401891863053674,34.88670913857598,35.07123491653798,12.91959460073133,24.868670766623076,24.64824057039346,21.978933887638842,7.334422420595478,8.705936191858406,23.049515386723435,31.791381925962508,21.540421642373214,18.777674997432577,-14.348504915936298,33.45264794509616,32.0592406980147,46.05414143615734,13.010540860161768,11.461863112573463,27.895580677677973,32.39896898172926,12.235155124994778,-28.52279436314767,-33.54803318824733,8.683011669463308,20.017817628651507,37.56853065972966,-6.570009429841186,-2.989441976244667,31.25634478720602,15.855404823496551,38.95536531469185,0.31409547032250845,49.937559065719434,32.037215695515215,26.65955280557829,56.64423668979061,60.56863764397813,42.237376393320325,41.28526303216521,41.02547403081423,31.869481581926127,-3.199743493625823,-3.095481322490762,-9.140400879348253,4.857141223506251,-14.224988352412492,-11.82070032625263,-2.0492914920270224,26.446298564505835,41.65833081412265,19.566042569211312,37.776973261321814,20.43795911736986,26.149566047174126,12.471204644176469,19.787938374978097,14.349999999999993,28.817012443743156,19.511830913791517,36.233902159527645,42.00223208354528,44.527865233143544,-13.361386905829054,-13.75591214210825,40.595137602130066,19.801479438925902,-18.705119441878292,-17.98349500846627,-13.020050386129597,-4.237277925049842e-15,3.7341875021081568,-0.6841343323415504,-11.65946015105909,-21.62561396922938,-5.821436275322066,3.557009490586955,6.716476637254407,21.78115976248485,33.145442699807276,-5.13069935735672,17.996845515559237,27.025748950607316,34.137351265609816,26.60704215517025,33.47520255652187,36.429610279839764,38.47654684023519,38.75325433034775,22.81735367683709,14.496124866952066,-43.17368372722494,-22.31680170434792,-14.728077655239048,-20.447715100345803,-21.04814862490763,-22.032200396747445,-21.591494648824177,-19.42579661278904,-27.033985762041237,-15.12905119465313,-22.275163104709193,1.0832670416736885,1.1419715947936941,12.62615057788266,22.07368982147317,18.70511944187829,16.110602010875823,18.49038471545791,-0.7745721523955768,-36.89217197869937,-66.65905957975184,-51.50326589134325,-41.42466714149142,-38.82597309209923,-12.635625612348626,7.368063303394862,-23.309816451475672,-16.48362069748481,-21.010701039668145,-22.870989751953566,-35.049282861709386,-50.4597659282019,-51.82957747028342,-50.1085933140148,-15.771878332918865,-60.07338282062384,-3.2154212137912763,0.7107390870623784,8.651355263444868,5.043268823083024,-7.547628055792192,3.9140504526013746,3.93323164824519,-0.6945927106677219,6.258968622859742,21.774143386487637,36.9396885195395,11.501760229563173,15.880805291281845,-3.2755801403744447,13.98482967290014,-8.987940462991668,-24.15010035419784,-23.461763303307674,-21.256312539292438,-15.804793620030585,-10.91044257208883,-3.644923035437376,6.2089592716770796e-15,3.984778792366982,-1.815853216736707,-13.729250899762192,-15.105508827633681,-14.077903082176215,-2.5114736709748713,6.862201078041087,13.329776402789143,18.352590699492296,19.412986546049623,29.559434228735388,13.703181864639014,4.999999999999993]}] \ No newline at end of file diff --git a/assets/css/custom.css b/assets/css/custom.css index 90eb3d1..ce6cec3 100644 --- a/assets/css/custom.css +++ b/assets/css/custom.css @@ -35,3 +35,39 @@ text-decoration: none; cursor: pointer; } + +/* Split-screen layout */ +#split-container { + display: flex; + width: 100%; + height: calc(100vh - 100px); /* Fill space between top/bottom bars */ +} + +#left-panel { + width: 50%; + height: 100%; + overflow-y: auto; + background-color: var(--primary-bg); + padding: 2rem; + box-sizing: border-box; +} + +#right-panel { + width: 50%; + height: 100%; +} + +#cesiumContainer { + width: 100%; + height: 100%; +} + +/* Remove padding from content divs as it's now on the parent */ +#dashboard-content, +#analytics-content, +#reports-content, +#settings-content, +#summary-content, +#help-content { + padding: 0 !important; /* Override dashboard.css */ +} \ No newline at end of file diff --git a/assets/css/dashboard.css b/assets/css/dashboard.css new file mode 100644 index 0000000..2d138ac --- /dev/null +++ b/assets/css/dashboard.css @@ -0,0 +1,357 @@ +:root { + --primary-bg: #1a1d21; + --secondary-bg: #2a2d31; + --primary-text: #f0f0f0; + --secondary-text: #a0a0a0; + --accent-color: #00aaff; + --border-color: #3a3d41; +} + +body { + font-family: 'Inter', sans-serif; + margin: 0; + padding: 0; + background-color: var(--primary-bg); + color: var(--primary-text); + overflow: hidden; +} + +#app-container { + display: flex; + height: 100vh; +} + +#sidebar { + width: 320px; + background-color: var(--primary-bg); + border-right: 1px solid var(--border-color); + display: flex; + flex-direction: column; + padding: 1.5rem; + box-shadow: 2px 0 10px rgba(0,0,0,0.2); +} + +#app-header h1 { + font-size: 1.5rem; + font-weight: 700; + margin: 0 0 2rem 0; +} + +#app-nav { + overflow-y: auto; +} + +.control-group { + margin-bottom: 2rem; + border-top: 1px solid var(--border-color); + padding-top: 1.5rem; +} + +.control-group:first-child { + border-top: none; + padding-top: 0; +} + +.control-group h3 { + font-size: 1rem; + font-weight: 600; + margin: 0 0 1rem 0; + color: var(--accent-color); +} + +.control-group label { + display: block; + margin-bottom: 0.75rem; + font-size: 0.9rem; + cursor: pointer; +} + +#main-content { + flex: 1; + position: relative; +} + +#cesiumContainer { + width: 100%; + height: 100%; +} + +/* Facility Portfolio */ +#facility-list-container { + max-height: 200px; + overflow-y: auto; + margin-bottom: 1rem; +} + +#facility-list { + list-style: none; + padding: 0; + margin: 0; +} + +#facility-list li { + display: flex; + justify-content: space-between; + align-items: center; + padding: 0.5rem; + border-radius: 4px; + margin-bottom: 0.5rem; + background-color: var(--secondary-bg); +} + +#facility-list .delete-facility-btn { + background: none; + border: none; + color: #ff4d4d; + cursor: pointer; + font-size: 1rem; +} + +#add-facility-form input { + width: calc(100% - 1rem); + padding: 0.5rem; + margin-bottom: 0.5rem; + background-color: var(--secondary-bg); + border: 1px solid var(--border-color); + color: var(--primary-text); + border-radius: 4px; +} + +#add-facility-form button { + width: 100%; + padding: 0.75rem; + background-color: var(--accent-color); + border: none; + color: white; + font-weight: 600; + border-radius: 4px; + cursor: pointer; +} + +#add-facility-form small { + display: block; + text-align: center; + margin-top: 0.5rem; + color: var(--secondary-text); +} + +/* Modal */ +.modal { + display: none; + position: fixed; + z-index: 1000; + left: 0; + top: 0; + width: 100%; + height: 100%; + background-color: rgba(0, 0, 0, 0.6); + align-items: center; + justify-content: center; +} + +.modal-content { + background-color: var(--secondary-bg); + padding: 2rem; + border-radius: 8px; + width: 80%; + max-width: 800px; + box-shadow: 0 5px 15px rgba(0,0,0,0.3); +} + +.close-button { + color: var(--secondary-text); + float: right; + font-size: 28px; + font-weight: bold; + cursor: pointer; +} + +/* New styles for hamburger menu and layout */ +#top-bar { + position: absolute; + top: 0; + left: 0; + width: 100%; + display: flex; + align-items: center; + padding: 1rem; + background: linear-gradient(to bottom, rgba(26, 29, 33, 0.7), transparent); + z-index: 100; +} + +#hamburger-menu { + background: none; + border: none; + cursor: pointer; + padding: 0.5rem; + margin-right: 1rem; +} + +#hamburger-menu span { + display: block; + width: 24px; + height: 3px; + background-color: var(--primary-text); + margin: 4px 0; + transition: all 0.3s; +} + +#top-header h2 { + margin: 0; + font-size: 1.2rem; + font-weight: 600; +} + +#sidebar.sidebar-hidden { + transform: translateX(-100%); +} + +#main-content.sidebar-hidden { + margin-left: 0; +} + +#sidebar { + transition: transform 0.3s ease-in-out; + z-index: 200; + position: absolute; + left: 0; + top: 0; + height: 100%; +} + +#main-content { + transition: margin-left 0.3s ease-in-out; + margin-left: 320px; +} + +/* New Nav Bars */ +.top-nav, .bottom-nav { + display: flex; + align-items: center; + background-color: var(--secondary-bg); + padding: 0 1rem; + height: 50px; + z-index: 99; +} + +.top-nav { + flex-grow: 1; + margin-left: 2rem; +} + +.bottom-nav { + position: absolute; + bottom: 0; + left: 0; + width: 100%; + border-top: 1px solid var(--border-color); +} + +.nav-item { + color: var(--primary-text); + text-decoration: none; + padding: 0 1rem; + font-size: 0.9rem; + font-weight: 500; + transition: color 0.2s; +} + +.nav-item:hover { + color: var(--accent-color); +} + +.bottom-nav .nav-item:last-child { + margin-left: auto; + color: var(--secondary-text); +} + + + +#dashboard-content { + padding: 2rem; +} + +.table { + width: 100%; + border-collapse: collapse; + margin-top: 2rem; +} + +.table th, .table td { + padding: 1rem; + text-align: left; + border-bottom: 1px solid var(--border-color); +} + +.table th { + background-color: var(--secondary-bg); + font-weight: 600; +} + +.table tbody tr:nth-child(even) { + background-color: var(--secondary-bg); +} + +.risk-high { + color: #ff4d4d; + font-weight: 700; +} + +.risk-medium { + color: #ffb347; + font-weight: 700; +} + +.risk-low { + color: #fdfd96; +} + +.risk-very-low { + color: #a4d0a4; +} + + +#cesiumContainer { + height: calc(100vh - 100px); +} + +#top-bar { + background: var(--secondary-bg); + border-bottom: 1px solid var(--border-color); + padding: 0 1rem; + height: 50px; +} + +#top-header { + display: none; /* Replaced by top-nav */ +} + +#settings-form .form-group { + margin-bottom: 1rem; +} + +#settings-form label { + display: block; + margin-bottom: 0.5rem; +} + +#settings-form input, +#settings-form select { + width: 100%; + padding: 0.5rem; + border-radius: 4px; + border: 1px solid var(--border-color); + background-color: var(--secondary-bg); + color: var(--primary-text); +} + +#settings-form button { + padding: 0.75rem 1.5rem; + background-color: var(--accent-color); + border: none; + color: white; + font-weight: 600; + border-radius: 4px; + cursor: pointer; +} + diff --git a/assets/js/main.js b/assets/js/main.js index c8e0858..5ddd595 100644 --- a/assets/js/main.js +++ b/assets/js/main.js @@ -1,12 +1,9 @@ -// Set your Cesium Ion default access token immediately Cesium.Ion.defaultAccessToken = 'eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJqdGkiOiJjZTY0ZTQ1Yi0zYmYxLTQ5MjItODdkOS05ZDY0ZGRjYjQwM2QiLCJpZCI6MjA5ODgwLCJpYXQiOjE3MTM4MTY3OTB9.A-3Jt_G0K81s-A-XLpT2bn5aY2H3s-n2p-2jYf-i-g'; -function initializeGlobe() { - let epCurveChart = null; // To hold the chart instance - console.log('Cesium is defined, initializing globe.'); + +function initializeGlobe() { try { - console.log('Initializing Cesium Viewer'); const viewer = new Cesium.Viewer('cesiumContainer', { imageryProvider: new Cesium.OpenStreetMapImageryProvider({ url : 'https://a.tile.openstreetmap.org/' @@ -16,7 +13,7 @@ function initializeGlobe() { fullscreenButton: false, geocoder: false, homeButton: false, - infoBox: true, + infoBox: false, // Disable default infobox sceneModePicker: false, selectionIndicator: false, timeline: false, @@ -24,945 +21,558 @@ function initializeGlobe() { scene3DOnly: true }); viewer.scene.globe.depthTestAgainstTerrain = false; - console.log('Cesium Viewer initialized successfully'); - // Add Weather Layer - const weatherImageryProvider = new Cesium.UrlTemplateImageryProvider({ - url: `api/weather.php?layer=clouds_new&z={z}&x={x}&y={y}`, - credit: 'Weather data © OpenWeatherMap' - }); - const weatherLayer = viewer.imageryLayers.addImageryProvider(weatherImageryProvider); + + + // --- Layer Toggles --- + const layers = { + weather: new Cesium.UrlTemplateImageryProvider({ url: `api/weather.php?layer=clouds_new&z={z}&x={x}&y={y}` }), + wildfires: new Cesium.GeoJsonDataSource('wildfires'), + spc: new Cesium.CustomDataSource('spcOutlook'), + weatherAlerts: new Cesium.CustomDataSource('weatherAlerts'), + hurricanes: new Cesium.CustomDataSource('hurricanes') + }; + + const weatherLayer = viewer.imageryLayers.addImageryProvider(layers.weather); weatherLayer.alpha = 0.6; + viewer.dataSources.add(layers.wildfires); + viewer.dataSources.add(layers.spc); + viewer.dataSources.add(layers.weatherAlerts); + viewer.dataSources.add(layers.hurricanes); - // UI Controls - const weatherCheckbox = document.getElementById('weatherLayerCheckbox'); - weatherCheckbox.addEventListener('change', function() { - weatherLayer.show = this.checked; - }); + document.getElementById('weatherLayerCheckbox').addEventListener('change', e => { weatherLayer.show = e.target.checked; }); + document.getElementById('wildfireLayerCheckbox').addEventListener('change', e => { layers.wildfires.show = e.target.checked; }); + document.getElementById('spcLayerCheckbox').addEventListener('change', e => { layers.spc.show = e.target.checked; }); + document.getElementById('weatherAlertsLayerCheckbox').addEventListener('change', e => { layers.weatherAlerts.show = e.target.checked; }); + document.getElementById('hurricaneLayerCheckbox').addEventListener('change', e => { layers.hurricanes.show = e.target.checked; }); - let wildfireDataSource; - const wildfireCheckbox = document.getElementById('wildfireLayerCheckbox'); - wildfireCheckbox.addEventListener('change', function() { - if (wildfireDataSource) { - wildfireDataSource.show = this.checked; - } - }); - - let spcDataSource = new Cesium.CustomDataSource('spcOutlook'); - viewer.dataSources.add(spcDataSource); - const spcCheckbox = document.getElementById('spcLayerCheckbox'); - spcCheckbox.addEventListener('change', function() { - spcDataSource.show = this.checked; - }); - - let weatherAlertsDataSource = new Cesium.CustomDataSource('weatherAlerts'); - viewer.dataSources.add(weatherAlertsDataSource); - const weatherAlertsCheckbox = document.getElementById('weatherAlertsLayerCheckbox'); - weatherAlertsCheckbox.addEventListener('change', function() { - weatherAlertsDataSource.show = this.checked; - }); - - // Wind Layer - const windLayer = new WindLayer(viewer, { - particleHeight: 10000, - particleCount: 10000, - maxAge: 120, - particleSpeed: 5 - }); - - const windCheckbox = document.getElementById('windLayerCheckbox'); - windCheckbox.addEventListener('change', function() { - windLayer.setVisible(this.checked); - }); - - const windAltitudeSlider = document.getElementById('windAltitudeSlider'); - const windAltitudeLabel = document.getElementById('windAltitudeLabel'); - windAltitudeSlider.addEventListener('input', function() { - const altitude = parseInt(this.value, 10); - windAltitudeLabel.textContent = `${altitude} m`; + // --- Wind Layer --- + const windLayer = new WindLayer(viewer, { particleHeight: 10000 }); + document.getElementById('windLayerCheckbox').addEventListener('change', e => { windLayer.setVisible(e.target.checked); }); + document.getElementById('windAltitudeSlider').addEventListener('input', e => { + const altitude = parseInt(e.target.value, 10); + document.getElementById('windAltitudeLabel').textContent = `${altitude} m`; windLayer.setOptions({ particleHeight: altitude }); }); - - const playPauseButton = document.getElementById('playPauseWind'); - let isPlaying = true; - playPauseButton.addEventListener('click', function() { - if (isPlaying) { - windLayer.pause(); - this.textContent = 'Play'; - } else { - windLayer.play(); - this.textContent = 'Pause'; - } - isPlaying = !isPlaying; + document.getElementById('playPauseWind').addEventListener('click', e => { + const isPlaying = windLayer.isPlaying(); + isPlaying ? windLayer.pause() : windLayer.play(); + e.target.textContent = isPlaying ? 'Play' : 'Pause'; }); + document.getElementById('particleDensitySlider').addEventListener('input', e => { windLayer.setParticleDensity(parseFloat(e.target.value)); }); - const particleDensitySlider = document.getElementById('particleDensitySlider'); - particleDensitySlider.addEventListener('input', function() { - const density = parseFloat(this.value); - windLayer.setParticleDensity(density); - }); - - // Function to load wildfire data - const loadWildfireData = async () => { - try { - console.log('Fetching wildfire data...'); - const response = await fetch('api/wildfires.php'); - if (!response.ok) { - throw new Error(`HTTP error! status: ${response.status}`); - } - const geojsonData = await response.json(); - console.log('Wildfire data fetched successfully.'); - - wildfireDataSource = new Cesium.GeoJsonDataSource(); - await wildfireDataSource.load(geojsonData, { + // --- Data Loading --- + const loadDataSources = () => { + // Wildfires + fetch('api/wildfires.php').then(res => res.json()).then(data => { + layers.wildfires.load(data, { stroke: Cesium.Color.RED, fill: Cesium.Color.RED.withAlpha(0.5), - strokeWidth: 2 - }); - - viewer.dataSources.add(wildfireDataSource); - console.log('Wildfire data source added to viewer.'); - - } catch (error) { - console.error('Error loading wildfire data:', error); - } - }; - - const loadSpcData = async () => { - try { - console.log('Fetching SPC data...'); - const response = await fetch('api/spc.php'); - if (!response.ok) { - throw new Error(`HTTP error! status: ${response.status}`); - } - const spcData = await response.json(); - console.log('SPC data fetched successfully.'); - - spcDataSource.entities.removeAll(); - - const selectedSpcLevels = Array.from(document.querySelectorAll('.spc-checkbox:checked')).map(cb => cb.value); - - const spcColors = { - 'TSTM': Cesium.Color.fromCssColorString('#00FF00').withAlpha(0.5), // General Thunderstorms - 'MRGL': Cesium.Color.fromCssColorString('#00C800').withAlpha(0.5), // Marginal - 'SLGT': Cesium.Color.fromCssColorString('#FFFF00').withAlpha(0.5), // Slight - 'ENH': Cesium.Color.fromCssColorString('#FFA500').withAlpha(0.5), // Enhanced - 'MDT': Cesium.Color.fromCssColorString('#FF0000').withAlpha(0.5), // Moderate - 'HIGH': Cesium.Color.fromCssColorString('#FF00FF').withAlpha(0.5) // High - }; - - if (Array.isArray(spcData)) { - spcData.forEach(feature => { - if (feature && feature.name && Array.isArray(feature.coordinates) && selectedSpcLevels.includes(feature.name)) { - const color = spcColors[feature.name] || Cesium.Color.GRAY.withAlpha(0.5); - spcDataSource.entities.add({ - name: `SPC Outlook: ${feature.name}`, - polygon: { - hierarchy: Cesium.Cartesian3.fromDegreesArray(feature.coordinates), - material: color, - outline: true, - outlineColor: Cesium.Color.BLACK - } - }); - } + strokeWidth: 3 + }).then(() => { + layers.wildfires.entities.values.forEach(entity => { + entity.point = new Cesium.PointGraphics({ + color: Cesium.Color.FIREBRICK, + pixelSize: 8 + }); }); - } - console.log('SPC data source updated.'); - - } catch (error) { - console.error('Error loading SPC data:', error); - } - }; - - const loadWeatherAlerts = async () => { - try { - console.log('Fetching weather alerts...'); - const response = await fetch('api/weather_alerts.php'); - if (!response.ok) { - throw new Error(`HTTP error! status: ${response.status}`); - } - const alertsData = await response.json(); - console.log('Weather alerts fetched successfully.'); - - weatherAlertsDataSource.entities.removeAll(); - - const selectedAlerts = Array.from(document.querySelectorAll('.alert-checkbox:checked')).map(cb => cb.value); - - const alertColors = { - 'tornado': Cesium.Color.RED.withAlpha(0.7), - 'thunderstorm': Cesium.Color.YELLOW.withAlpha(0.7), - 'flood': Cesium.Color.BLUE.withAlpha(0.7), - 'wind': Cesium.Color.CYAN.withAlpha(0.7), - 'snow_ice': Cesium.Color.WHITE.withAlpha(0.7), - 'fog': Cesium.Color.GRAY.withAlpha(0.7), - 'extreme_high_temperature': Cesium.Color.ORANGE.withAlpha(0.7) - }; - - if (Array.isArray(alertsData)) { - alertsData.forEach(alert => { - if (alert && alert.properties && typeof alert.properties.event === 'string') { - const eventType = alert.properties.event.toLowerCase().replace(/\s/g, '_'); - if (selectedAlerts.includes(eventType)) { - const alertColor = alertColors[eventType] || Cesium.Color.PURPLE.withAlpha(0.7); - - if (alert.geometry && alert.geometry.type === 'Polygon' && Array.isArray(alert.geometry.coordinates) && Array.isArray(alert.geometry.coordinates[0])) { - const coordinates = alert.geometry.coordinates[0].flat(); - weatherAlertsDataSource.entities.add({ - name: alert.properties.event || 'Weather Alert', - description: alert.properties.description || 'No description available.', - polygon: { - hierarchy: Cesium.Cartesian3.fromDegreesArray(coordinates), - material: alertColor, - outline: true, - outlineColor: Cesium.Color.BLACK - } - }); - } - } - } - }); - } - console.log('Weather alerts data source updated.'); - - } catch (error) { - console.error('Error loading weather alerts:', error); - } - }; - - document.querySelectorAll('.spc-checkbox').forEach(cb => { - cb.addEventListener('change', loadSpcData); - }); - - document.querySelectorAll('.alert-checkbox').forEach(cb => { - cb.addEventListener('change', loadWeatherAlerts); - }); - - // Load all data sources - loadWildfireData(); - loadSpcData(); - loadWeatherAlerts(); - loadHurricaneSelector(); - - // --- CAT Simulation Logic --- - - const catSimulationDataSource = new Cesium.CustomDataSource('catSimulation'); - viewer.dataSources.add(catSimulationDataSource); - - let customPortfolioData = null; - - const portfolio = [ - { id: 1, name: 'Miami Property', lat: 25.7617, lon: -80.1918, value: 1000000 }, - { id: 2, name: 'Homestead Property', lat: 25.4687, lon: -80.4776, value: 750000 }, - { id: 3, name: 'Fort Lauderdale Property', lat: 26.1224, lon: -80.1373, value: 1200000 }, - { id: 4, name: 'Naples Property', lat: 26.1420, lon: -81.7948, value: 1500000 } - ]; - - document.getElementById('portfolio-upload').addEventListener('change', handleFileUpload); - document.getElementById('portfolio-select').addEventListener('change', handlePortfolioSelect); - - async function handlePortfolioSelect(event) { - const selectedPortfolioUrl = event.target.value; - const portfolioStatus = document.getElementById('portfolio-status'); - const portfolioUpload = document.getElementById('portfolio-upload'); - - if (!selectedPortfolioUrl) { - customPortfolioData = null; - portfolioStatus.textContent = ''; - portfolioUpload.value = ''; // Clear file input - return; - } - - try { - const response = await fetch(selectedPortfolioUrl); - if (!response.ok) { - throw new Error(`Failed to load portfolio: ${response.statusText}`); - } - const contents = await response.text(); - - const lines = contents.split('\n').filter(line => line.trim() !== ''); - const headers = lines.shift().trim().split(',').map(h => h.toLowerCase()); - - const latIndex = headers.indexOf('lat'); - const lonIndex = headers.indexOf('lon'); - const tivIndex = headers.indexOf('tiv'); - const deductibleIndex = headers.indexOf('deductible'); - const limitIndex = headers.indexOf('limit'); - - if (latIndex === -1 || lonIndex === -1 || tivIndex === -1) { - throw new Error('CSV must contain "lat", "lon", and "tiv" headers.'); - } - - const parsedData = lines.map((line, index) => { - const values = line.split(','); - if (values.length <= Math.max(latIndex, lonIndex, tivIndex)) { - console.error(`Skipping malformed CSV line ${index + 2}: ${line}`); - return null; - } - const lat = parseFloat(values[latIndex]); - const lon = parseFloat(values[lonIndex]); - const tiv = parseFloat(values[tivIndex]); - - if (isNaN(lat) || isNaN(lon) || isNaN(tiv)) { - console.error(`Skipping line with invalid number ${index + 2}: ${line}`); - return null; - } - - const deductible = deductibleIndex > -1 ? parseFloat(values[deductibleIndex]) || 0 : 0; - const limit = limitIndex > -1 ? parseFloat(values[limitIndex]) || Number.POSITIVE_INFINITY : Number.POSITIVE_INFINITY; - - return { - id: `custom-${index + 1}`, - name: `Property ${index + 1}`, - lat: lat, - lon: lon, - value: tiv, - deductible: deductible, - limit: limit - }; - }).filter(p => p !== null); - - customPortfolioData = parsedData; - portfolioStatus.textContent = `${parsedData.length} properties loaded from sample portfolio.`; - portfolioUpload.value = ''; // Clear file input as we are using a sample - console.log('Sample portfolio loaded:', customPortfolioData); - - } catch (error) { - console.error('Error loading sample portfolio:', error); - portfolioStatus.textContent = `Error: ${error.message}`; - customPortfolioData = null; - } - } - - function handleFileUpload(event) { - const file = event.target.files[0]; - if (!file) { - return; - } - - // Clear the sample portfolio dropdown - document.getElementById('portfolio-select').value = ''; - - const reader = new FileReader(); - reader.onload = function(e) { - const contents = e.target.result; - try { - const lines = contents.split('\n').filter(line => line.trim() !== ''); - const headers = lines.shift().trim().split(',').map(h => h.toLowerCase()); - - const latIndex = headers.indexOf('lat'); - const lonIndex = headers.indexOf('lon'); - const tivIndex = headers.indexOf('tiv'); - const deductibleIndex = headers.indexOf('deductible'); - const limitIndex = headers.indexOf('limit'); - - if (latIndex === -1 || lonIndex === -1 || tivIndex === -1) { - throw new Error('CSV must contain "lat", "lon", and "tiv" headers.'); - } - - const parsedData = lines.map((line, index) => { - const values = line.split(','); - if (values.length <= Math.max(latIndex, lonIndex, tivIndex)) { - console.error(`Skipping malformed CSV line ${index + 2}: ${line}`); - return null; - } - const lat = parseFloat(values[latIndex]); - const lon = parseFloat(values[lonIndex]); - const tiv = parseFloat(values[tivIndex]); - - if (isNaN(lat) || isNaN(lon) || isNaN(tiv)) { - console.error(`Skipping line with invalid number ${index + 2}: ${line}`); - return null; - } - - const deductible = deductibleIndex > -1 ? parseFloat(values[deductibleIndex]) || 0 : 0; - const limit = limitIndex > -1 ? parseFloat(values[limitIndex]) || Number.POSITIVE_INFINITY : Number.POSITIVE_INFINITY; - - return { - id: `custom-${index + 1}`, - name: `Property ${index + 1}`, - lat: lat, - lon: lon, - value: tiv, - deductible: deductible, - limit: limit - }; - }).filter(p => p !== null); - - customPortfolioData = parsedData; - document.getElementById('portfolio-status').textContent = `${parsedData.length} properties loaded.`; - console.log('Custom portfolio loaded:', customPortfolioData); - - } catch (error) { - console.error('Error parsing CSV:', error); - document.getElementById('portfolio-status').textContent = `Error: ${error.message}`; - customPortfolioData = null; - } - }; - reader.readAsText(file); - } - - async function loadHurricaneSelector() { - try { - const response = await fetch('api/hurricanes.php'); - const hurricanes = await response.json(); - const selectElement = document.getElementById('hurricane-select'); - selectElement.innerHTML = ''; // Clear "Loading..." - hurricanes.forEach(h => { - const option = document.createElement('option'); - option.value = h.id; - option.textContent = `${h.name} (${h.year})`; - selectElement.appendChild(option); }); - } catch (error) { - console.error('Error loading hurricane list:', error); - const selectElement = document.getElementById('hurricane-select'); - selectElement.innerHTML = ''; - } - } + }); - function getEstimatedWindSpeed(propertyPosition, stormTrack, propertyName, eventId) { - console.log(`[getEstimatedWindSpeed] Called for property: ${propertyName}, event: ${eventId}`); - let minDistance = Number.MAX_VALUE; - let closestSegmentIndex = -1; + // Hurricanes + fetch('api/hurricanes.php').then(res => res.json()).then(data => { + processHurricaneData(data, layers.hurricanes); + }); - if (!stormTrack || stormTrack.length === 0) { - console.error(`[getEstimatedWindSpeed] Received empty or invalid stormTrack for event ${eventId}`); - return { windSpeed: 0, distanceKm: 0, closestTrackPoint: null }; - } + // Weather Alerts + fetch('api/weather_alerts.php').then(res => res.json()).then(data => { + processWeatherAlerts(data, layers.weatherAlerts); + }); + + // SPC Outlook + fetch('api/spc.php').then(res => res.json()).then(data => { + processSpcOutlook(data, layers.spc); + }); - console.log(`[getEstimatedWindSpeed] propertyPosition:`, propertyPosition); - console.log(`[getEstimatedWindSpeed] stormTrack length:`, stormTrack.length); + // Other data sources like SPC/Alerts can be loaded here too if needed on init + }; - for (let i = 0; i < stormTrack.length; i++) { - const lon = stormTrack[i][0]; - const lat = stormTrack[i][1]; + function processSpcOutlook(spcData, dataSource) { + dataSource.entities.removeAll(); + if (!spcData || !Array.isArray(spcData)) return; - if (isNaN(lon) || isNaN(lat)) { - console.error(`[getEstimatedWindSpeed] Invalid coordinates in stormTrack for event ${eventId} at index ${i}:`, stormTrack[i]); - continue; - } - try { - const trackPointPosition = Cesium.Cartesian3.fromDegrees(lon, lat); - const distance = Cesium.Cartesian3.distance(propertyPosition, trackPointPosition); - - if (i < 5) { // Log first 5 distances - console.log(`[getEstimatedWindSpeed] Index ${i}: lon=${lon}, lat=${lat}, distance=${distance}`); - } + spcData.forEach(feature => { + const riskLevel = feature.name; + const color = getSpcColor(riskLevel); - if (distance < minDistance) { - minDistance = distance; - closestSegmentIndex = i; - } - } catch (e) { - console.error(`[getEstimatedWindSpeed] Error processing track point ${i} for event ${eventId}: lon=${lon}, lat=${lat}`, e); - } - } - - if (closestSegmentIndex === -1) { - console.error(`[getEstimatedWindSpeed] Could not find closest track point for event ${eventId}`); - return { windSpeed: 0, distanceKm: 0, closestTrackPoint: null }; - } - - console.log(`[getEstimatedWindSpeed] Closest segment index: ${closestSegmentIndex}`); - - const distanceKm = minDistance / 1000; - const trackPoint = stormTrack[closestSegmentIndex]; - const windOnTrackMph = trackPoint[2]; - - console.log(`[getEstimatedWindSpeed] minDistance (m): ${minDistance}`); - console.log(`[getEstimatedWindSpeed] distanceKm: ${distanceKm}`); - console.log(`[getEstimatedWindSpeed] closest trackPoint:`, trackPoint); - console.log(`[getEstimatedWindSpeed] windOnTrackMph: ${windOnTrackMph}`); - - const decayConstant = 0.01386; // -ln(0.5) / 50km - const estimatedWindMph = windOnTrackMph * Math.exp(-decayConstant * distanceKm); - - console.log(`[getEstimatedWindSpeed] decayConstant: ${decayConstant}`); - console.log(`[getEstimatedWindSpeed] Math.exp term: ${Math.exp(-decayConstant * distanceKm)}`); - console.log(`[getEstimatedWindSpeed] Final estimatedWindMph: ${estimatedWindMph}`); - - if (isNaN(estimatedWindMph)) { - console.error(`[getEstimatedWindSpeed] FATAL: Resulting wind speed is NaN. Inputs: windOnTrack=${windOnTrackMph}, distanceKm=${distanceKm}`); - } - - return { - windSpeed: estimatedWindMph, - distanceKm: distanceKm, - closestTrackPoint: { - lon: trackPoint[0], - lat: trackPoint[1], - wind: windOnTrackMph - } - }; - } - - function calculateNetLoss(grossLoss, deductible, limit) { - const lossAfterDeductible = Math.max(0, grossLoss - deductible); - return Math.min(lossAfterDeductible, limit); - } - - function getDamageRatio(windSpeedMph) { - if (windSpeedMph < 74) return 0.0; - if (windSpeedMph <= 95) return 0.03; - if (windSpeedMph <= 110) return 0.08; - if (windSpeedMph <= 129) return 0.15; - if (windSpeedMph <= 156) return 0.30; - return 0.50; - } - - function renderEPCurve(epData) { - const ctx = document.getElementById('epCurveChart').getContext('2d'); - - if (epCurveChart) { - epCurveChart.destroy(); - } - - const chartData = { - datasets: [{ - label: 'Exceedance Probability Curve', - data: epData.map(p => ({ x: p.loss, y: p.exceedanceProbability })), - borderColor: 'rgba(75, 192, 192, 1)', - backgroundColor: 'rgba(75, 192, 192, 0.2)', - borderWidth: 2, - showLine: true, - pointRadius: 4, - pointBackgroundColor: 'rgba(75, 192, 192, 1)', - }] - }; - - epCurveChart = new Chart(ctx, { - type: 'scatter', - data: chartData, - options: { - responsive: true, - maintainAspectRatio: false, - title: { - display: true, - text: 'Exceedance Probability (EP) Curve' + dataSource.entities.add({ + name: `SPC Outlook: ${riskLevel}`, + polygon: { + hierarchy: new Cesium.PolygonHierarchy( + Cesium.Cartesian3.fromDegreesArray(feature.coordinates) + ), + material: color.withAlpha(0.5), + outline: true, + outlineColor: color }, - scales: { - xAxes: [{ - type: 'linear', - position: 'bottom', - scaleLabel: { - display: true, - labelString: 'Loss ($)' - }, - ticks: { - callback: function(value) { - return '$' + value.toLocaleString(); - } - } - }], - yAxes: [{ - type: 'linear', - scaleLabel: { - display: true, - labelString: 'Exceedance Probability' - }, - ticks: { - min: 0, - max: 1, - callback: function(value) { - return (value * 100).toFixed(0) + '%'; - } - } - }] - }, - tooltips: { - callbacks: { - label: function(tooltipItem, data) { - const datasetLabel = data.datasets[tooltipItem.datasetIndex].label || ''; - const loss = tooltipItem.xLabel.toLocaleString(); - const prob = (tooltipItem.yLabel * 100).toFixed(2); - return `${datasetLabel} Loss: $${loss} (EP: ${prob}%)`; - } - } - } - } + description: ` +

SPC Convective Outlook

+

Risk Level: ${riskLevel}

+ ` + }); }); } - const runProbabilisticAnalysisButton = document.getElementById('runProbabilisticAnalysis'); - runProbabilisticAnalysisButton.addEventListener('click', runProbabilisticAnalysis); - - async function runProbabilisticAnalysis() { - console.log("Starting probabilistic analysis..."); - catSimulationDataSource.entities.removeAll(); - document.getElementById('cat-simulation-results').style.display = 'none'; - const resultsDiv = document.getElementById('probabilistic-results'); - resultsDiv.style.display = 'none'; - - const activePortfolio = customPortfolioData || portfolio; - if (!activePortfolio || activePortfolio.length === 0) { - alert("Please load a portfolio to run the analysis."); - return; - } - - try { - const response = await fetch('api/stochastic_events.php'); - if (!response.ok) { - throw new Error(`Failed to fetch stochastic events: ${response.statusText}`); - } - const stochasticEvents = await response.json(); - console.log(`Fetched ${stochasticEvents.length} stochastic events.`); - console.log('Full stochastic events data:', stochasticEvents); - console.log('Stochastic Events:', JSON.stringify(stochasticEvents, null, 2)); - - let averageAnnualLoss = 0; - const eventLosses = []; - - stochasticEvents.forEach(event => { - console.log('Processing event:', event); - let eventTotalLoss = 0; - activePortfolio.forEach(property => { - try { - console.log(`Processing property: ${property.name}, lon: ${property.lon}, lat: ${property.lat}`); - if (isNaN(property.lon) || isNaN(property.lat)) { - console.error('Invalid coordinates for property:', property); - return; - } - const propertyPosition = Cesium.Cartesian3.fromDegrees(property.lon, property.lat); - - if (!event.track_data || !Array.isArray(event.track_data)) { - console.error('Invalid track_data for event:', event); - return; - } - - const windData = getEstimatedWindSpeed(propertyPosition, event.track_data, property.name, event.event_id); - - if (!windData || windData.closestTrackPoint === null || isNaN(windData.windSpeed)) { - console.error('Could not calculate valid wind data for property, skipping.', {property, event}); - return; - } - - const damageRatio = getDamageRatio(windData.windSpeed); - const grossLoss = property.value * damageRatio; - const netLoss = calculateNetLoss(grossLoss, property.deductible || 0, property.limit || Number.POSITIVE_INFINITY); - eventTotalLoss += netLoss; - } catch (e) { - console.error(`An error occurred while processing property ${property.name} for event ${event.id}:`, e); - } - }); - averageAnnualLoss += eventTotalLoss * event.probability; - eventLosses.push({ loss: eventTotalLoss, probability: event.probability }); - }); - - console.log(`Calculated Average Annual Loss (AAL): ${averageAnnualLoss}`); - - eventLosses.sort((a, b) => b.loss - a.loss); - - let cumulativeProbability = 0; - const epData = eventLosses.map(event => { - cumulativeProbability += event.probability; - return { - loss: event.loss, - exceedanceProbability: cumulativeProbability - }; - }); - epData.push({ loss: 0, exceedanceProbability: 1.0 }); - - console.log("EP Curve Data:", epData); - - document.getElementById('aal-value').textContent = `$${Math.round(averageAnnualLoss).toLocaleString()}`; - resultsDiv.style.display = 'block'; - - renderEPCurve(epData); - - activePortfolio.forEach(property => { - if (isNaN(property.lon) || isNaN(property.lat)) { - return; - } - catSimulationDataSource.entities.add({ - id: `property-${property.id}`, - position: Cesium.Cartesian3.fromDegrees(property.lon, property.lat), - point: { - pixelSize: 10, - color: Cesium.Color.BLUE, - outlineColor: Cesium.Color.WHITE, - outlineWidth: 2 - }, - label: { - text: property.name, - font: '12pt monospace', - style: Cesium.LabelStyle.FILL_AND_OUTLINE, - outlineWidth: 2, - verticalOrigin: Cesium.VerticalOrigin.BOTTOM, - pixelOffset: new Cesium.Cartesian2(0, -9) - } - }); - }); - viewer.flyTo(catSimulationDataSource.entities); - - } catch (error) { - console.error("Error during probabilistic analysis:", error); - alert(`An error occurred during analysis: ${error.message}`); + function getSpcColor(riskLevel) { + switch (riskLevel) { + case 'TSTM': return Cesium.Color.fromCssColorString('#c1e2c1'); // Light Green + case 'MRGL': return Cesium.Color.fromCssColorString('#a4d0a4'); // Green + case 'SLGT': return Cesium.Color.fromCssColorString('#fdfd96'); // Yellow + case 'ENH': return Cesium.Color.fromCssColorString('#ffb347'); // Orange + case 'MDT': return Cesium.Color.fromCssColorString('#ff6961'); // Red + case 'HIGH': return Cesium.Color.fromCssColorString('#ff00ff'); // Magenta + default: return Cesium.Color.GRAY; } } - const runCatSimulationButton = document.getElementById('runCatSimulation'); - runCatSimulationButton.addEventListener('click', runCatSimulation); + function processWeatherAlerts(alertsData, dataSource) { + dataSource.entities.removeAll(); + const alertsListContainer = document.getElementById('alerts-list-container'); + alertsListContainer.innerHTML = ''; // Clear previous list - async function runCatSimulation() { - catSimulationDataSource.entities.removeAll(); - document.getElementById('cat-simulation-results').style.display = 'none'; - let totalLoss = 0; + const alertColors = { + "warning": Cesium.Color.RED, + "watch": Cesium.Color.ORANGE, + "advisory": Cesium.Color.YELLOW, + "statement": Cesium.Color.SKYBLUE, + "default": Cesium.Color.LIGHTGRAY + }; - const hurricaneSelect = document.getElementById('hurricane-select'); - const selectedHurricaneId = hurricaneSelect.value; - - if (!selectedHurricaneId) { - alert('Please select a hurricane to run the simulation.'); + if (!alertsData || !Array.isArray(alertsData) || alertsData.length === 0) { + alertsListContainer.innerHTML = '

No active alerts in the covered area.

'; return; } - const activePortfolio = customPortfolioData || portfolio; + const ul = document.createElement('ul'); + ul.className = 'alerts-list'; - activePortfolio.forEach(property => { - catSimulationDataSource.entities.add({ - id: `property-${property.id}`, - position: Cesium.Cartesian3.fromDegrees(property.lon, property.lat), + alertsData.forEach(alert => { + // Add to list in modal + const li = document.createElement('li'); + li.innerHTML = ` +

${alert.event}

+

Sender: ${alert.sender_name}

+

${alert.description || 'No description provided.'}

+ `; + ul.appendChild(li); + + // Draw on map + if (alert.polygons && Array.isArray(alert.polygons)) { + const eventName = (alert.event || 'alert').toLowerCase(); + let color = alertColors.default; + for (const key in alertColors) { + if (eventName.includes(key)) { + color = alertColors[key]; + break; + } + } + + alert.polygons.forEach(polygonCoords => { + const coordinates = polygonCoords.flat(); + dataSource.entities.add({ + name: alert.event || 'Weather Alert', + polygon: { + hierarchy: new Cesium.PolygonHierarchy( + Cesium.Cartesian3.fromDegreesArray(coordinates) + ), + material: color.withAlpha(0.4), + outline: true, + outlineColor: color.withAlpha(0.8) + }, + description: ` +

${alert.event}

+

Sender: ${alert.sender_name}

+

${alert.description || 'No description provided.'}

+ ` + }); + }); + } + }); + + alertsListContainer.appendChild(ul); + } + + function processHurricaneData(geoJson, dataSource) { + dataSource.entities.removeAll(); + const features = geoJson.features; + + // Separate features by type + const cones = features.filter(f => f.geometry.type === 'Polygon'); + const tracks = features.filter(f => f.geometry.type === 'LineString'); + const points = features.filter(f => f.geometry.type === 'Point'); + + // Process cones first (bottom layer) + cones.forEach(feature => { + dataSource.entities.add({ + polygon: { + hierarchy: new Cesium.PolygonHierarchy( + Cesium.Cartesian3.fromDegreesArray(feature.geometry.coordinates[0].flat()) + ), + material: Cesium.Color.WHITE.withAlpha(0.15), + outline: true, + outlineColor: Cesium.Color.WHITE.withAlpha(0.3) + } + }); + }); + + // Process tracks + tracks.forEach(feature => { + dataSource.entities.add({ + polyline: { + positions: Cesium.Cartesian3.fromDegreesArray(feature.geometry.coordinates.flat()), + width: 2, + material: new Cesium.PolylineDashMaterialProperty({ + color: Cesium.Color.WHITE + }) + } + }); + }); + + // Process points (top layer) + points.forEach(feature => { + const props = feature.properties; + const windSpeed = parseInt(props.WINDSPEED, 10); + dataSource.entities.add({ + position: Cesium.Cartesian3.fromDegrees(feature.geometry.coordinates[0], feature.geometry.coordinates[1]), point: { pixelSize: 10, - color: Cesium.Color.GREEN, + color: Cesium.Color.fromCssColorString(getStormColor(windSpeed)), outlineColor: Cesium.Color.WHITE, outlineWidth: 2 }, label: { - text: property.name, - font: '12pt monospace', + text: `${props.STORMNAME} (${windSpeed} mph)`, + font: '12pt Inter, sans-serif', style: Cesium.LabelStyle.FILL_AND_OUTLINE, outlineWidth: 2, verticalOrigin: Cesium.VerticalOrigin.BOTTOM, - pixelOffset: new Cesium.Cartesian2(0, -9) + pixelOffset: new Cesium.Cartesian2(0, -12) } }); }); - - const response = await fetch(`api/hurricanes.php?id=${selectedHurricaneId}`); - const hurricaneData = await response.json(); - const trackPositions = hurricaneData.track.flatMap(p => [p[0], p[1]]); - - catSimulationDataSource.entities.add({ - name: hurricaneData.name, - polyline: { - positions: Cesium.Cartesian3.fromDegreesArray(trackPositions), - width: 3, - material: Cesium.Color.RED.withAlpha(0.8) - } - }); - - activePortfolio.forEach(property => { - const propertyPosition = Cesium.Cartesian3.fromDegrees(property.lon, property.lat); - - const windData = getEstimatedWindSpeed(propertyPosition, hurricaneData.track, property.name, hurricaneData.id); - const damageRatio = getDamageRatio(windData.windSpeed); - const grossLoss = property.value * damageRatio; - const netLoss = calculateNetLoss(grossLoss, property.deductible || 0, property.limit || Number.POSITIVE_INFINITY); - totalLoss += netLoss; - - const entity = catSimulationDataSource.entities.getById(`property-${property.id}`); - let pointColor = Cesium.Color.GREEN; - if (damageRatio > 0) { - if (damageRatio <= 0.03) pointColor = Cesium.Color.YELLOW; - else if (damageRatio <= 0.08) pointColor = Cesium.Color.ORANGE; - else if (damageRatio <= 0.15) pointColor = Cesium.Color.RED; - else pointColor = Cesium.Color.PURPLE; - } - entity.point.color = pointColor; - - entity.label.text = `${property.name}\nWind: ${windData.windSpeed.toFixed(0)} mph\nLoss: $${Math.round(netLoss).toLocaleString()}`; - }); - - document.getElementById('total-loss-value').textContent = `$${Math.round(totalLoss).toLocaleString()}`; - document.getElementById('cat-simulation-results').style.display = 'block'; - - viewer.flyTo(catSimulationDataSource.entities, { - duration: 2.0 - }); } - const displayModeSelect = document.getElementById('displayModeSelect'); - const windControls = document.getElementById('wind-controls'); - const catControls = document.getElementById('cat-controls'); + function getStormColor(windspeed) { + if (windspeed >= 157) return '#c83226'; // Cat 5 + if (windspeed >= 130) return '#ff6900'; // Cat 4 + if (windspeed >= 111) return '#ff9c00'; // Cat 3 + if (windspeed >= 96) return '#ffcf00'; // Cat 2 + if (windspeed >= 74) return '#ffff00'; // Cat 1 + return '#00c8ff'; // Tropical Storm + } - displayModeSelect.addEventListener('change', function(e) { - if (this.value === 'wind') { - windControls.style.display = 'block'; - catControls.style.display = 'none'; - windLayer.setVisible(true); - catSimulationDataSource.entities.removeAll(); - } else { - windControls.style.display = 'none'; - catControls.style.display = 'block'; - windLayer.setVisible(false); - } - }); + loadDataSources(); - // Trigger the change event to set the initial state - displayModeSelect.dispatchEvent(new Event('change')); - - // Cross-section chart logic - const modal = document.getElementById("crossSectionModal"); - const closeButton = document.getElementsByClassName("close-button")[0]; - const chartCanvas = document.getElementById("crossSectionChart"); + // --- Modal & Click Handling --- + const crossSectionModal = document.getElementById("crossSectionModal"); + const crossSectionCloseButton = crossSectionModal.querySelector(".close-button"); + const alertsModal = document.getElementById("alertsModal"); + const alertsCloseButton = alertsModal.querySelector(".close-button"); + const alertsNavButton = document.getElementById("show-alerts-btn"); let crossSectionChart; - closeButton.onclick = function () { - modal.style.display = "none"; - }; + crossSectionCloseButton.onclick = () => { crossSectionModal.style.display = "none"; }; + alertsCloseButton.onclick = () => { alertsModal.style.display = "none"; }; + alertsNavButton.addEventListener('click', (e) => { + e.preventDefault(); + alertsModal.style.display = "block"; + }); - window.onclick = function (event) { - if (event.target == modal) { - modal.style.display = "none"; - } + window.onclick = (event) => { + if (event.target == crossSectionModal) crossSectionModal.style.display = "none"; + if (event.target == alertsModal) alertsModal.style.display = "none"; }; const handler = new Cesium.ScreenSpaceEventHandler(viewer.scene.canvas); handler.setInputAction(function (click) { - console.log("Globe clicked. Position:", click.position); - const scene = viewer.scene; - const cartesian = scene.pickPosition(click.position); - console.log("Cartesian position:", cartesian); - - if (Cesium.defined(cartesian)) { - const ellipsoid = scene.globe.ellipsoid; - const cartographic = ellipsoid.cartesianToCartographic(cartesian); - const longitude = Cesium.Math.toDegrees(cartographic.longitude).toFixed(4); - const latitude = Cesium.Math.toDegrees(cartographic.latitude).toFixed(4); - - console.log(`Fetching cross-section for lat: ${latitude}, lon: ${longitude}`); - const modal = document.getElementById("crossSectionModal"); - const loadingIndicator = document.getElementById("loadingIndicator"); - loadingIndicator.style.display = 'block'; // Show loading indicator - - fetch(`/api/cross_section.php?lat=${latitude}&lon=${longitude}`) - .then(response => { - console.log("Received response from server:", response); - if (!response.ok) { - throw new Error(`Network response was not ok: ${response.statusText}`); - } - return response.json(); - }) - .then(data => { - loadingIndicator.style.display = 'none'; // Hide loading indicator - if (data.error) { - alert("Error fetching cross-section data: " + data.error); - return; - } - console.log("Cross-section data received:", data); - - const labels = data.hourly.pressure_level; - const temperature = data.hourly.temperature; - const humidity = data.hourly.relative_humidity; - const windspeed = data.hourly.wind_speed; - - if (crossSectionChart) { - crossSectionChart.destroy(); - } - - crossSectionChart = new Chart(chartCanvas, { - type: "line", - data: { - labels: labels, - datasets: [{ - label: "Temperature (°C)", - data: temperature, - borderColor: "red", - yAxisID: "y", - tension: 0.1, - }, { - label: "Relative Humidity (%)", - data: humidity, - borderColor: "blue", - yAxisID: "y1", - tension: 0.1, - }, { - label: "Wind Speed (km/h)", - data: windspeed, - borderColor: "green", - yAxisID: "y2", - tension: 0.1, - }] - }, - options: { - responsive: true, - interaction: { - mode: 'index', - intersect: false, - }, - scales: { - x: { - title: { - display: true, - text: "Pressure Level (hPa)", - }, - reverse: true - }, - y: { - type: 'linear', - display: true, - position: 'left', - title: { - display: true, - text: 'Temperature (°C)' - } - }, - y1: { - type: 'linear', - display: true, - position: 'right', - title: { - display: true, - text: 'Relative Humidity (%)' - }, - grid: { - drawOnChartArea: false, - }, - }, - y2: { - type: 'linear', - display: true, - position: 'right', - title: { - display: true, - text: 'Wind Speed (km/h)' - }, - grid: { - drawOnChartArea: false, - }, - } - }, - }, - }); - - modal.style.display = "block"; - console.log("Chart displayed."); - }) - .catch(error => { - loadingIndicator.style.display = 'none'; // Hide loading indicator - console.error("Error fetching or parsing cross-section data:", error); - alert("Failed to retrieve or display cross-section data. See console for details."); - }); - } else { - console.log("No position could be picked from the globe."); + const cartesian = viewer.scene.pickPosition(click.position); + if (!Cesium.defined(cartesian)) { + return; // Didn't pick a point on the globe } + + const cartographic = Cesium.Cartographic.fromCartesian(cartesian); + const longitude = Cesium.Math.toDegrees(cartographic.longitude); + const latitude = Cesium.Math.toDegrees(cartographic.latitude); + + + + // --- Now, handle the risk assessment modal --- + crossSectionModal.style.display = "block"; + + // Reset UI + document.getElementById('riskScoreValue').textContent = 'Calculating...'; + document.getElementById('wildfireRisk').textContent = 'Calculating...'; + document.getElementById('hurricaneRisk').textContent = 'Calculating...'; + document.getElementById('historicalHurricaneRisk').textContent = 'Calculating...'; + document.getElementById("loadingIndicator").style.display = 'block'; + if (crossSectionChart) { + crossSectionChart.destroy(); + } + + // Fetch Risk Score + fetch(`api/risk_score.php?lat=${latitude}&lon=${longitude}`) + .then(response => response.json()) + .then(data => { + if (data.error) throw new Error(data.error); + document.getElementById('riskScoreValue').textContent = data.risk_score ? `${data.risk_score} / 100` : 'N/A'; + document.getElementById('wildfireRisk').textContent = data.factors?.wildfire?.details || 'N/A'; + document.getElementById('hurricaneRisk').textContent = data.factors?.live_hurricane?.details || 'N/A'; + document.getElementById('historicalHurricaneRisk').textContent = data.factors?.historical_hurricane?.details || 'N/A'; + }) + .catch(error => { + console.error("Error fetching risk score:", error); + document.getElementById('riskScoreValue').textContent = 'Error'; + }); + + // Fetch Cross-Section Data + fetch(`/api/cross_section.php?lat=${latitude}&lon=${longitude}`) + .then(response => response.json()) + .then(data => { + document.getElementById("loadingIndicator").style.display = 'none'; + if (data.error) throw new Error(data.error); + + const { pressure_level, temperature, relative_humidity, wind_speed } = data.hourly; + crossSectionChart = new Chart(document.getElementById("crossSectionChart"), { + type: "line", + data: { + labels: pressure_level, + datasets: [ + { label: "Temperature (C)", data: temperature, borderColor: "red", yAxisID: "y" }, + { label: "Humidity (%)", data: relative_humidity, borderColor: "blue", yAxisID: "y1" }, + { label: "Wind Speed (km/h)", data: wind_speed, borderColor: "green", yAxisID: "y2" } + ] + }, + options: { + scales: { + x: { title: { display: true, text: "Pressure (hPa)" }, reverse: true }, + y: { type: 'linear', position: 'left', title: { display: true, text: 'Temp (C)' } }, + y1: { type: 'linear', position: 'right', title: { display: true, text: 'Humidity (%)' }, grid: { drawOnChartArea: false } }, + y2: { type: 'linear', position: 'right', title: { display: true, text: 'Wind (km/h)' }, grid: { drawOnChartArea: false } } + } + } + }); + }) + .catch(error => { + console.error("Error fetching cross-section:", error); + document.getElementById("loadingIndicator").style.display = 'none'; + }); + }, Cesium.ScreenSpaceEventType.LEFT_CLICK); } catch (error) { - console.error('A critical error occurred while initializing the Cesium viewer:', error); - const cesiumContainer = document.getElementById('cesiumContainer'); - cesiumContainer.innerHTML = '
Error: Could not load the 3D scene. Please check the console for details.
'; + console.error('Critical error initializing Cesium:', error); + document.getElementById('cesiumContainer').innerHTML = '

Error loading 3D scene.

'; } } -function waitForCesium() { - if (typeof Cesium !== 'undefined') { - initializeGlobe(); - } else { - console.log('Waiting for Cesium to load...'); - setTimeout(waitForCesium, 100); - } -} +document.addEventListener('DOMContentLoaded', () => { + const sidebar = document.getElementById('sidebar'); + const hamburgerMenu = document.getElementById('hamburger-menu'); + const homeLink = document.getElementById('home-link'); -document.addEventListener('DOMContentLoaded', waitForCesium); \ No newline at end of file + const mainContent = document.getElementById('main-content'); + + // Initially hide the sidebar for a cleaner look on load + sidebar.classList.add('sidebar-hidden'); + mainContent.classList.add('sidebar-hidden'); + + hamburgerMenu.addEventListener('click', () => { + sidebar.classList.toggle('sidebar-hidden'); + mainContent.classList.toggle('sidebar-hidden'); + }); + + homeLink.addEventListener('click', (e) => { + e.preventDefault(); + showPanel(cesiumContainer); + }); + + initializeGlobe(); + + // --- Dashboard --- + const cesiumContainer = document.getElementById('cesiumContainer'); + const dashboardContent = document.getElementById('dashboard-content'); + const analyticsContent = document.getElementById('analytics-content'); + const reportsContent = document.getElementById('reports-content'); + const settingsContent = document.getElementById('settings-content'); + const summaryContent = document.getElementById('summary-content'); + const helpContent = document.getElementById('help-content'); + + const contentPanels = [cesiumContainer, dashboardContent, analyticsContent, reportsContent, settingsContent, summaryContent, helpContent]; + + function showPanel(panelToShow) { + contentPanels.forEach(panel => { + if (panel === panelToShow) { + panel.classList.remove('hidden'); + } else { + panel.classList.add('hidden'); + } + }); + } + + function getRiskScoreClass(score) { + if (score > 75) return 'risk-high'; + if (score > 50) return 'risk-medium'; + if (score > 25) return 'risk-low'; + return 'risk-very-low'; + } + + async function loadDashboardData() { + const tbody = dashboardContent.querySelector('tbody'); + tbody.innerHTML = 'Loading facility data...'; + + try { + const facilitiesResponse = await fetch('api/facilities.php'); + const facilitiesData = await facilitiesResponse.json(); + + if (!facilitiesData.success || facilitiesData.facilities.length === 0) { + tbody.innerHTML = 'No facilities found.'; + return; + } + + let tableHtml = ''; + facilitiesData.facilities.forEach(facility => { + tableHtml += ` + + ${facility.name} + ${facility.city} + ${facility.state} + ${facility.risk !== null ? facility.risk : 'N/A'} + + `; + }); + tbody.innerHTML = tableHtml; + + } catch (error) { + console.error('Error loading dashboard data:', error); + tbody.innerHTML = 'Error loading data.'; + } + } + + let facilitiesByStateChart = null; + async function loadAnalyticsData() { + const ctx = document.getElementById('facilitiesByStateChart').getContext('2d'); + + try { + const facilitiesResponse = await fetch('api/facilities.php'); + const facilitiesData = await facilitiesResponse.json(); + + if (!facilitiesData.success || facilitiesData.facilities.length === 0) { + return; + } + + const facilitiesByState = facilitiesData.facilities.reduce((acc, facility) => { + const state = facility.state || 'Unknown'; + acc[state] = (acc[state] || 0) + 1; + return acc; + }, {}); + + const chartData = { + labels: Object.keys(facilitiesByState), + datasets: [{ + label: 'Number of Facilities', + data: Object.values(facilitiesByState), + backgroundColor: 'rgba(0, 170, 255, 0.5)', + borderColor: 'rgba(0, 170, 255, 1)', + borderWidth: 1 + }] + }; + + if (facilitiesByStateChart) { + facilitiesByStateChart.destroy(); + } + + facilitiesByStateChart = new Chart(ctx, { + type: 'bar', + data: chartData, + options: { + scales: { + y: { + beginAtZero: true + } + } + } + }); + + } catch (error) { + console.error('Error loading analytics data:', error); + } + } + + async function downloadReport() { + try { + const facilitiesResponse = await fetch('api/facilities.php'); + const facilitiesData = await facilitiesResponse.json(); + + if (!facilitiesData.success || facilitiesData.facilities.length === 0) { + alert('No facilities to generate a report for.'); + return; + } + + let csvContent = 'data:text/csv;charset=utf-8,'; + csvContent += 'Facility Name,City,State,Latitude,Longitude,Risk Score\n'; + + facilitiesData.facilities.forEach(facility => { + csvContent += `"${facility.name}","${facility.city}","${facility.state}",${facility.latitude},${facility.longitude},${facility.risk || 'N/A'}\n`; + }); + + const encodedUri = encodeURI(csvContent); + const link = document.createElement('a'); + link.setAttribute('href', encodedUri); + link.setAttribute('download', 'facility_risk_report.csv'); + document.body.appendChild(link); + link.click(); + document.body.removeChild(link); + + } catch (error) { + console.error('Error generating report:', error); + alert('Failed to generate report.'); + } + } + + document.getElementById('dashboard-nav-link').addEventListener('click', (e) => { + e.preventDefault(); + showPanel(dashboardContent); + loadDashboardData(); + }); + + document.getElementById('analytics-nav-link').addEventListener('click', (e) => { + e.preventDefault(); + showPanel(analyticsContent); + loadAnalyticsData(); + }); + + document.getElementById('reports-nav-link').addEventListener('click', (e) => { + e.preventDefault(); + showPanel(reportsContent); + }); + + const downloadReportBtn = document.getElementById('download-report-btn'); + downloadReportBtn.addEventListener('click', () => { + downloadReport(); + }); + + document.getElementById('settings-nav-link').addEventListener('click', (e) => { + e.preventDefault(); + showPanel(settingsContent); + }); + + document.getElementById('summary-nav-link').addEventListener('click', (e) => { + e.preventDefault(); + showPanel(summaryContent); + }); + + document.getElementById('help-nav-link').addEventListener('click', (e) => { + e.preventDefault(); + showPanel(helpContent); + }); +}); diff --git a/assets/js/wind.js b/assets/js/wind.js index 87795ce..3fda2cd 100644 --- a/assets/js/wind.js +++ b/assets/js/wind.js @@ -131,6 +131,10 @@ class WindLayer { }); } + isPlaying() { + return this.particleSystem && this.particleSystem.isPlaying; + } + setParticleDensity(density) { this.readyPromise.then(() => { if (this.particleSystem) { @@ -149,6 +153,7 @@ class ParticleSystem { // Use a polyline collection instead of a point collection this.polylines = this.scene.primitives.add(new Cesium.PolylineCollection()); this.particles = []; + this.isPlaying = true; this.createParticles(); @@ -295,10 +300,12 @@ class ParticleSystem { } pause() { + this.isPlaying = false; this.scene.preRender.removeEventListener(this.update, this); } play() { + this.isPlaying = true; this.scene.preRender.addEventListener(this.update, this); } } diff --git a/db/migrations/20251017_create_facilities_table.sql b/db/migrations/20251017_create_facilities_table.sql new file mode 100644 index 0000000..83dfe36 --- /dev/null +++ b/db/migrations/20251017_create_facilities_table.sql @@ -0,0 +1,7 @@ +CREATE TABLE IF NOT EXISTS facilities ( + id INT AUTO_INCREMENT PRIMARY KEY, + name VARCHAR(255) NOT NULL, + latitude DECIMAL(10, 8) NOT NULL, + longitude DECIMAL(11, 8) NOT NULL, + created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP +); diff --git a/index.php b/index.php index 44e055a..d0fbf81 100644 --- a/index.php +++ b/index.php @@ -4,136 +4,159 @@ - Worldsphere.ai - 3D Weather Map + Worldsphere - Healthcare Risk Management + + - -
-
-
-

Layers

- - - - - -
-
-

Display Mode

- -
-
-
-

Wind Altitude

- - 10000 m -
-
-

Wind Animation

- -