'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."); } ?>