Files
test/stormdata.php
2025-11-27 22:25:36 +00:00

572 lines
25 KiB
PHP

<?php
// --- Error Reporting (Recommended for Development) ---
ini_set('display_errors', 1);
ini_set('display_startup_errors', 1);
error_reporting(E_ALL);
// In production, turn display_errors off and configure log_errors.
// ini_set('display_errors', 0);
// ini_set('log_errors', 1);
// ini_set('error_log', '/path/to/your/php_error.log');
// --- Database Connection ---
$dbconn = pg_connect("host=localhost dbname=nws user=nws password=nws");
if (!$dbconn) {
error_log('Database connection failed: ' . pg_last_error());
http_response_code(503);
header('Content-Type: application/json; charset=utf-8');
echo json_encode(['error' => 'Service temporarily unavailable due to database connection issue.']);
exit;
}
// --- Helper Functions ---
/**
* Sends a JSON error response and terminates the script.
* @param int $http_code The HTTP status code.
* @param string $message The error message for the client.
* @param ?string $log_message Optional detailed message for the server error log.
*/
function send_error(int $http_code, string $message, ?string $log_message = null): void {
if ($log_message) { error_log($log_message); }
elseif ($http_code >= 500) { error_log("Server Error (" . $http_code . "): " . $message); }
http_response_code($http_code);
header('Content-Type: application/json; charset=utf-8');
echo json_encode(['error' => $message]);
exit;
}
/**
* Sends a GeoJSON FeatureCollection response and terminates the script.
* @param array $features An array of GeoJSON Feature objects.
*/
function send_geojson(array $features): void {
$geojson_output = ['type' => 'FeatureCollection', 'features' => $features];
header('Content-Type: application/geo+json; charset=utf-8');
echo json_encode($geojson_output);
exit;
}
// --- Main Request Handling ---
if ($_SERVER['REQUEST_METHOD'] === 'POST') {
error_log("--- New POST Request Received ---");
$input_data = null;
$request_type = null;
$contentType = trim(strtolower($_SERVER['HTTP_CONTENT_TYPE'] ?? $_SERVER['CONTENT_TYPE'] ?? ''));
error_log("Received Content-Type: " . $contentType);
// ***********************************************************
// ***** START: CRUCIAL JSON INPUT HANDLING BLOCK *****
// ***********************************************************
if (strpos($contentType, 'application/json') === 0) {
error_log("Content-Type identified as JSON.");
$raw_post_data = file_get_contents('php://input');
error_log("Raw php://input length: " . strlen($raw_post_data));
if ($raw_post_data === false || $raw_post_data === '') {
send_error(400, 'Received empty request body or could not read input.', "Error: Could not read php://input or it was empty.");
}
// Decode JSON into an associative array
$input_data = json_decode($raw_post_data, true); // Use 'true' for array
if (json_last_error() !== JSON_ERROR_NONE) {
send_error(400, 'Invalid JSON payload received.', 'JSON Decode Error: ' . json_last_error_msg() . " | Raw data snippet: " . substr($raw_post_data, 0, 100));
} elseif (!is_array($input_data)) {
send_error(400, 'Invalid JSON payload: Expected a JSON object.', "JSON Decode Warning: Result is not an array. Data: " . print_r($input_data, true));
} else {
error_log("JSON Decode Successful.");
// ** GET request_type FROM THE DECODED ARRAY **
$request_type = $input_data['request_type'] ?? null;
error_log("Extracted request_type from JSON: " . ($request_type ?? 'null'));
}
} else {
// If JSON is strictly required, reject other types
send_error(415, 'Unsupported Media Type. This endpoint requires application/json.', "Unsupported Media Type Received: " . $contentType);
}
// ***********************************************************
// ***** END: CRUCIAL JSON INPUT HANDLING BLOCK *****
// ***********************************************************
// --- Final Check and Routing ---
if ($request_type === null) {
if (is_array($input_data) && !isset($input_data['request_type'])) {
send_error(400, 'Missing "request_type" field within the request payload.');
} else {
error_log("Routing check reached but request_type is null without prior exit.");
send_error(400, 'Missing required parameter: request_type (or processing error).');
}
}
error_log("Routing request for type: " . $request_type);
switch ($request_type) {
case 'ohgo':
// ** Pass the $input_data array **
handle_ohgo_request($dbconn, $input_data);
break;
case 'ohgonopoly':
// ** Pass the $input_data array **
handle_ohgo_request_no_poly($dbconn, $input_data);
break;
case 'power':
// ** Pass the $input_data array **
handle_power_request($dbconn, $input_data);
break;
case 'powernopoly':
// ** Pass the $input_data array **
handle_power_request_no_poly($dbconn, $input_data);
break;
case 'wupoly':
// ** Pass the $input_data array **
handle_wu_request_poly($dbconn, $input_data);
break;
case 'campoly':
// ** Pass the $input_data array **
handle_cam_request($dbconn, $input_data);
break;
default:
send_error(400, 'Invalid request_type specified: ' . htmlspecialchars($request_type));
break;
}
} else {
http_response_code(405);
header('Allow: POST');
header('Content-Type: application/json; charset=utf-8');
echo json_encode(['error' => 'Invalid request method. Only POST is allowed.']);
exit;
}
// --- Request Handler Functions ---
function handle_cam_request($dbconn, array $data): void {
error_log("Handling 'camera image' request.");
// --- 1. Get Data from the $data array ---
$start_time_str = $data['start_time'] ?? null;
$end_time_str = $data['end_time'] ?? null;
$geojson_str = $data['area_geojson'] ?? null;
// --- 2. Validation ---
if ($start_time_str === null || $end_time_str === null || $geojson_str === null) {
send_error(400, 'Missing required parameters for camera request: start_time, end_time, area_geojson');
}
// Validate Timestamps (basic check, can be more robust)
// Consider using DateTime objects for more rigorous validation if needed
if (strtotime($start_time_str) === false) {
send_error(400, 'Invalid start_time format.');
}
if (strtotime($end_time_str) === false) {
send_error(400, 'Invalid end_time format.');
}
// Ensure start is before end? Optional, depends on requirements.
// if (strtotime($start_time_str) >= strtotime($end_time_str)) {
// send_error(400, 'start_time must be before end_time.');
// }
// Validate GeoJSON
$geojson_obj = json_decode($geojson_str);
if (json_last_error() !== JSON_ERROR_NONE) {
send_error(400, 'Invalid area_geojson provided: Contains invalid JSON string.', 'GeoJSON Decode Error: ' . json_last_error_msg());
}
if (!is_object($geojson_obj) || !isset($geojson_obj->type) || !in_array($geojson_obj->type, ['Polygon', 'MultiPolygon'])) {
send_error(400, 'Invalid area_geojson provided: Decoded JSON must be a Polygon or MultiPolygon object.');
}
// --- 3. Prepare and Execute Query ---
// This query finds active cameras within the GeoJSON area,
// then LEFT JOINs aggregated image data from camdb within the time range.
// We use jsonb_agg for efficiency and COALESCE to return an empty array []
// for cameras with no images in the range, instead of NULL.
// NOTE: Selecting c.* assumes 'geom' is not excessively large or problematic
// when fetched directly. If it is, list all columns except 'geom'.
// We explicitly fetch ST_AsGeoJSON for the geometry representation.
$query = "
SELECT
c.*, -- Select all columns from cams
ST_AsGeoJSON(c.geom) as geometry_geojson, -- Get geometry as GeoJSON string
COALESCE(img_agg.images, '[]'::jsonb) AS images -- Get aggregated images or empty JSON array
FROM
cams c
LEFT JOIN (
SELECT
camid,
jsonb_agg(
jsonb_build_object(
'timestamp', dateutc,
'url', filepath -- Assuming filepath is the relative URL path
) ORDER BY dateutc ASC -- Order images chronologically
) AS images
FROM
camdb
WHERE
dateutc >= $1::timestamp -- start_time
AND dateutc <= $2::timestamp -- end_time
GROUP BY
camid
) AS img_agg ON c.camid = img_agg.camid
WHERE
c.active = TRUE -- Only active cameras
AND ST_Within(c.geom, ST_GeomFromGeoJSON($3)) -- Camera location within area
ORDER BY
c.camid; -- Optional: Order cameras by ID
";
$params = array(
$start_time_str, // $1: start_time
$end_time_str, // $2: end_time
$geojson_str // $3: area_geojson string
);
$result = pg_query_params($dbconn, $query, $params);
if (!$result) {
send_error(500, 'Database query failed for camera data.', 'Camera Query Failed: ' . pg_last_error($dbconn) . " | Query: " . $query . " | Params: " . print_r($params, true));
}
// --- 4. Process Results ---
$cameras_output = [];
while ($row = pg_fetch_assoc($result)) {
// Decode the geometry GeoJSON string into a PHP object/array
$geometry = json_decode($row['geometry_geojson']);
if (json_last_error() !== JSON_ERROR_NONE) {
error_log('Failed to decode geometry for camid ' . ($row['camid'] ?? 'N/A') . ': ' . json_last_error_msg());
// Decide how to handle: skip camera, set geometry to null, etc.
$geometry = null; // Example: Set to null on error
}
// Decode the images JSON string (from jsonb_agg) into a PHP array
$images = json_decode($row['images']);
if (json_last_error() !== JSON_ERROR_NONE) {
error_log('Failed to decode images JSON for camid ' . ($row['camid'] ?? 'N/A') . ': ' . json_last_error_msg());
// Decide how to handle: skip camera, set images to empty array, etc.
$images = []; // Example: Set to empty array on error
}
// Prepare the output structure for this camera
$camera_data = $row; // Start with all columns fetched via c.*
// Replace/remove raw JSON strings and potentially the original binary geom
unset($camera_data['geometry_geojson']); // Remove the raw GeoJSON string
unset($camera_data['geom']); // Remove the raw binary geometry if it was fetched by c.*
$camera_data['geometry'] = $geometry; // Add the decoded geometry object/array
$camera_data['images'] = $images; // Add the decoded images array
$cameras_output[] = $camera_data;
}
pg_free_result($result);
error_log("Found " . count($cameras_output) . " cameras matching criteria.");
// --- 5. Send Response ---
// Use a function like send_json defined above, or inline the logic:
header('Content-Type: application/json');
echo json_encode($cameras_output, JSON_PRETTY_PRINT | JSON_UNESCAPED_SLASHES);
exit; // Important to stop script execution here
// Alternatively, if you have the helper:
// send_json($cameras_output);
}
function handle_wu_request_poly($dbconn, array $data): void { // Takes $data array
$polygons = $data['polygons'] ?? []; // Array of WKT polygons, e.g., ['POLYGON((...))', 'POLYGON((...))']
$start_time = $data['start_time'] ?? '2025-01-01 00:00:00'; // e.g., '2025-01-01 00:00:00'
$end_time = $data['end_time'] ?? '2025-01-02 00:00:00'; // e.g., '2025-01-02 00:00:00'
if (empty($polygons)) {
http_response_code(500);
echo json_encode(['error' => 'No polygons provided']);
pg_close($dbconn);
exit;
}
$polygon_placeholders = [];
$params = [];
$param_index = 1;
foreach ($polygons as $polygon) {
$polygon_placeholders[] = "ST_GeomFromText(\$$param_index, 4326)";
$params[] = $polygon;
$param_index++;
}
$params[] = $start_time;
$params[] = $end_time;
$start_time_placeholder = "\$$param_index";
$param_index++;
$end_time_placeholder = "\$$param_index";
$polygon_sql = implode(', ', $polygon_placeholders);
$sql = "
SELECT wo.*
FROM wuobs wo
JOIN wusites ws ON wo.stationid = ws.stationid
WHERE ws.geom && ST_Union(ARRAY[$polygon_sql])::geometry
AND ST_Within(ws.geom, ST_Union(ARRAY[$polygon_sql])::geometry)
AND wo.observation_time BETWEEN $start_time_placeholder AND $end_time_placeholder
";
$result = pg_query_params($dbconn, $sql, $params);
if ($result === false) {
http_response_code(500);
echo json_encode(['error' => pg_last_error($dbconn)]);
pg_close($dbconn);
exit;
}
// Fetch results
$results = [];
while ($row = pg_fetch_assoc($result)) {
$results[] = $row;
}
// Free result and close connection
pg_free_result($result);
// Output results as JSON
header('Content-Type: application/json');
echo json_encode($results);
}
/**
* Handles the 'ohgo' data request.
* @param resource $dbconn The active database connection resource.
* @param array $data The associative array of input parameters (from JSON).
*/
function handle_ohgo_request($dbconn, array $data): void { // Takes $data array
error_log("Handling 'ohgo' request.");
// --- 1. Get Data from the $data array ---
$start = $data['start_time'] ?? null; // Use $data, use correct key
$geojson_str = $data['area_geojson'] ?? null; // Use $data, not $_POST
$end = $data['end_time'] ?? null; // Use $data, use correct key
// --- 2. Validation ---
if ($start === null || $geojson_str === null || $end === null) { send_error(400, 'Missing required parameters for ohgo request: start, geojson, end'); }
$geojson_obj = json_decode($geojson_str);
if (json_last_error() !== JSON_ERROR_NONE) { send_error(400, 'Invalid GeoJSON provided: Not valid JSON.', 'GeoJSON Decode Error: ' . json_last_error_msg()); }
if (!isset($geojson_obj->type) || !in_array($geojson_obj->type, ['Polygon', 'MultiPolygon'])) { send_error(400, 'Invalid GeoJSON provided: Type must be Polygon or MultiPolygon.'); }
// --- 3. Prepare and Execute Query ---
$query = "SELECT ST_AsGeoJSON(geom) AS geometry, category, roadstatus, county, state, location, routename, description, start AS start_timestamp, endtime AS end_timestamp, lastupdate FROM ohgo WHERE start > $1::timestamp AND start < $3::timestamp AND ST_Within(geom, ST_GeomFromGeoJSON($2)) ORDER BY start ASC";
$params = array($start, $geojson_str, $end);
$result = pg_query_params($dbconn, $query, $params);
if (!$result) { send_error(500, 'Database query failed for ohgo data.', 'OHGO Query Failed: ' . pg_last_error($dbconn)); }
// --- 4. Process Results ---
$features = [];
while ($line = pg_fetch_assoc($result)) {
$geometry = json_decode($line['geometry']);
if (json_last_error() !== JSON_ERROR_NONE) { error_log('Failed to decode geometry for ohgo row: ' . json_last_error_msg()); continue; }
$properties = $line; unset($properties['geometry']);
$features[] = ['type' => 'Feature', 'geometry' => $geometry, 'properties' => $properties];
}
pg_free_result($result);
error_log("Found " . count($features) . " features for ohgo request.");
// --- 5. Send Response ---
send_geojson($features);
}
/**
* Handles the 'power' data request.
* @param resource $dbconn The active database connection resource.
* @param array $data The associative array of input parameters (from JSON).
*/
function handle_power_request($dbconn, array $data): void { // Takes $data array
error_log("Handling 'power' request.");
// --- 1. Get Data from the $data array ---
// ** Match keys from your fetch request body: start_time, area_geojson, etc. **
$start = $data['start_time'] ?? null; // Use $data, use correct key
$geojson_str = $data['area_geojson'] ?? null; // Use $data, use correct key
$end = $data['end_time'] ?? null; // Use $data, use correct key
$buffer_hours = $data['buffer'] ?? 0;// Use $data, use correct key
// --- 2. Validation ---
if ($start === null || $geojson_str === null || $end === null || $buffer_hours === null) {
// Update error message to reflect the actual keys expected from JSON
send_error(400, 'Missing required parameters for power request: start_time, area_geojson, end_time, buffer_hours');
}
if (!is_numeric($buffer_hours) || ($buffer_hours_float = floatval($buffer_hours)) < 0) { send_error(400, 'Invalid buffer_hours provided: Must be a non-negative number.'); }
$buffer_hours_int = (int)$buffer_hours_float;
$geojson_obj = json_decode($geojson_str); // Decode the *string* value from the JSON input
if (json_last_error() !== JSON_ERROR_NONE) { send_error(400, 'Invalid area_geojson provided: Contains invalid JSON string.', 'GeoJSON Decode Error: ' . json_last_error_msg()); }
if (!is_object($geojson_obj) || !isset($geojson_obj->type) || !in_array($geojson_obj->type, ['Polygon', 'MultiPolygon'])) { send_error(400, 'Invalid area_geojson provided: Decoded JSON must be a Polygon or MultiPolygon object.'); }
// ** Crucial Fix: Use the decoded $geojson_str for the query parameter, not $geojson_obj **
// --- 3. Prepare and Execute Query ---
// ** VERIFY TABLE/COLUMN NAMES FOR POWER TABLE **
$query = "SELECT ST_AsGeoJSON(realgeom) AS geometry, derivedstart AS start_timestamp, cause, peakoutage, lastchange AS end_timestamp FROM power WHERE derivedstart >= $1::timestamp AND derivedstart < ($3::timestamp + make_interval(hours => $4::integer)) AND ST_Within(realgeom, ST_GeomFromGeoJSON($2)) ORDER BY derivedstart ASC";
$params = array(
$start, // $1: start_time from JSON
$geojson_str, // $2: area_geojson STRING from JSON
$end, // $3: end_time from JSON
$buffer_hours_int // $4: buffer_hours from JSON (as integer)
);
$result = pg_query_params($dbconn, $query, $params);
if (!$result) { send_error(500, 'Database query failed for power data.', 'Power Query Failed: ' . pg_last_error($dbconn) . " | Query: " . $query . " | Params: " . print_r($params, true)); }
// --- 4. Process Results ---
$features = [];
while ($line = pg_fetch_assoc($result)) {
$geometry = json_decode($line['geometry']);
if (json_last_error() !== JSON_ERROR_NONE) { error_log('Failed to decode geometry for power row: ' . json_last_error_msg()); continue; }
$properties = $line; unset($properties['geometry']);
$features[] = ['type' => 'Feature', 'geometry' => $geometry, 'properties' => $properties];
}
pg_free_result($result);
error_log("Found " . count($features) . " features for power request.");
// --- 5. Send Response ---
send_geojson($features);
}
/**
* Handles the 'ohgo' data request.
* @param resource $dbconn The active database connection resource.
* @param array $data The associative array of input parameters (from JSON).
*/
function handle_ohgo_request_no_poly($dbconn, array $data): void { // Takes $data array
error_log("Handling 'ohgo' request.");
// --- 1. Get Data from the $data array ---
$start = $data['start_time'] ?? null; // Use $data, use correct key
$end = $data['end_time'] ?? null; // Use $data, use correct key
// --- 2. Validation ---
if ($start === null || $end === null) { send_error(400, 'Missing required parameters for ohgo request: start, geojson, end'); }
// --- 3. Prepare and Execute Query ---
$query = "SELECT ST_AsGeoJSON(geom) AS geometry, county, state AS st, location, routename AS city, upper(cwa) AS wfo, 'FLOOD' AS typetext, 'Department of Highways' AS source, description AS remark,
TO_CHAR(start, 'YYYY-MM-DD\"T\"HH24:MI:SS\"Z\"') AS valid
FROM ohgo
WHERE start > $1::timestamp
AND start < $2::timestamp
AND cwa = 'RLX'
ORDER BY start ASC";
$params = array($start, $end);
$result = pg_query_params($dbconn, $query, $params);
if (!$result) { send_error(500, 'Database query failed for ohgo data.', 'OHGO Query Failed: ' . pg_last_error($dbconn)); }
// --- 4. Process Results ---
$features = [];
while ($line = pg_fetch_assoc($result)) {
$geometry = json_decode($line['geometry']);
if (json_last_error() !== JSON_ERROR_NONE) { error_log('Failed to decode geometry for ohgo row: ' . json_last_error_msg()); continue; }
$properties = $line; unset($properties['geometry']);
$features[] = ['type' => 'Feature', 'geometry' => $geometry, 'properties' => $properties];
}
pg_free_result($result);
error_log("Found " . count($features) . " features for ohgo request.");
// --- 5. Send Response ---
send_geojson($features);
}
/**
* Handles the 'power' data request.
* @param resource $dbconn The active database connection resource.
* @param array $data The associative array of input parameters (from JSON).
*/
function handle_power_request_no_poly($dbconn, array $data): void { // Takes $data array
error_log("Handling 'power' request.");
// --- 1. Get Data from the $data array ---
// ** Match keys from your fetch request body: start_time, area_geojson, etc. **
$start = $data['start_time'] ?? null; // Use $data, use correct key
$end = $data['end_time'] ?? null; // Use $data, use correct key
$outage_threshold = $data['outage_threshold'] ?? 9;
$buffer_hours = $data['buffer'] ?? 0;// Use $data, use correct key
// --- 2. Validation ---
if ($start === null || $end === null || $buffer_hours === null) {
// Update error message to reflect the actual keys expected from JSON
send_error(400, 'Missing required parameters for power request: start_time, area_geojson, end_time, buffer_hours');
}
if (!is_numeric($buffer_hours) || ($buffer_hours_float = floatval($buffer_hours)) < 0) { send_error(400, 'Invalid buffer_hours provided: Must be a non-negative number.'); }
$buffer_hours_int = (int)$buffer_hours_float;
$outage_thresh = (float)$outage_threshold;
// --- 3. Prepare and Execute Query ---
// ** VERIFY TABLE/COLUMN NAMES FOR POWER TABLE **
$query = "SELECT ST_AsGeoJSON(realgeom) AS geometry,
TO_CHAR(derivedstart, 'YYYY-MM-DD\"T\"HH24:MI:SS\"Z\"') AS valid,
('Power Outage affecting ' || peakoutage || ' customers caused by ' || COALESCE(cause, 'unknown')) AS remark,
'Utility Company' as source,
'POWER OUTAGE' as typetext,
'U' as type,
(ROUND(ST_Y(realgeom)::numeric, 3) || ', ' || ROUND(ST_X(realgeom)::numeric, 3)) AS city,
county as county,
state as state,
state as st
FROM power
WHERE derivedstart >= $1::timestamp
AND derivedstart < ($2::timestamp + make_interval(hours => $3::integer))
and peakoutage > $4
AND ST_Within(realgeom, (SELECT geom FROM public.cwa WHERE cwa = 'RLX'))
ORDER BY derivedstart ASC";
$params = array(
$start, // $1: start_time from JSON
$end, // $2: end_time from JSON
$buffer_hours_int, // $3: buffer_hours from JSON (as integer)
$outage_thresh // $4
);
$result = pg_query_params($dbconn, $query, $params);
if (!$result) { send_error(500, 'Database query failed for power data.', 'Power Query Failed: ' . pg_last_error($dbconn) . " | Query: " . $query . " | Params: " . print_r($params, true)); }
// --- 4. Process Results ---
$features = [];
while ($line = pg_fetch_assoc($result)) {
$geometry = json_decode($line['geometry']);
if (json_last_error() !== JSON_ERROR_NONE) { error_log('Failed to decode geometry for power row: ' . json_last_error_msg()); continue; }
$properties = $line; unset($properties['geometry']);
$features[] = ['type' => 'Feature', 'geometry' => $geometry, 'properties' => $properties];
}
pg_free_result($result);
error_log("Found " . count($features) . " features for power request.");
// --- 5. Send Response ---
send_geojson($features);
}
// --- Close Database Connection ---
if ($dbconn) {
pg_close($dbconn);
error_log("Database connection closed.");
}
?>