211 lines
6.6 KiB
PHP
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);
|