34935-vm/api/risk_score.php
2025-10-17 01:31:35 +00:00

211 lines
6.6 KiB
PHP

<?php
// api/risk_score.php
/**
* Calculates a risk score for a given latitude and longitude.
*
* The risk score is based on:
* 1. Proximity to active wildfires.
* 2. Location within an active hurricane's forecast cone.
* 3. Historical hurricane activity in the area.
*/
header('Content-Type: application/json');
ini_set('display_errors', 0); // Don't show errors to the user
require_once __DIR__ . '/../db/config.php';
// --- Utility Functions ---
/**
* Point in Polygon check.
* Ray-casting algorithm.
* @param array $point The point to check: [$lon, $lat]
* @param array $polygon The polygon vertices: [[$lon, $lat], [$lon, $lat], ...]
* @return bool True if the point is in the polygon.
*/
function pointInPolygon($point, $polygon) {
$inside = false;
$x = $point[0];
$y = $point[1];
for ($i = 0, $j = count($polygon) - 1; $i < count($polygon); $j = $i++) {
$xi = $polygon[$i][0];
$yi = $polygon[$i][1];
$xj = $polygon[$j][0];
$yj = $polygon[$j][1];
$intersect = (($yi > $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);