|
| 1 | +<?php |
| 2 | +// Usage: php horizons_radec.php <designation> <YYYY-MM-DD HH:MM> <lon> <lat> <alt_m> |
| 3 | +if ($argc < 6) { |
| 4 | + echo json_encode(['error' => 'usage: designation datetime lon lat alt_m']); |
| 5 | + exit(1); |
| 6 | +} |
| 7 | +$des = $argv[1]; |
| 8 | +$dt = $argv[2]; |
| 9 | +$lon = $argv[3]; |
| 10 | +$lat = $argv[4]; |
| 11 | +$alt_m = floatval($argv[5]); |
| 12 | +$alt_km = $alt_m / 1000.0; |
| 13 | +$start = $dt; |
| 14 | +$stop = date('Y-m-d H:i', strtotime($dt . ' +1 minute')); |
| 15 | + |
| 16 | +function doQuery($command, $site, $start, $stop) |
| 17 | +{ |
| 18 | + // Request JSON format when possible for more reliable parsing. |
| 19 | + $post = [ |
| 20 | + 'format' => 'json', |
| 21 | + 'COMMAND' => $command, |
| 22 | + 'EPHEM_TYPE' => 'OBSERVER', |
| 23 | + 'CENTER' => 'coord@399', |
| 24 | + 'SITE_COORD' => $site, |
| 25 | + 'START_TIME' => "'{$start}'", |
| 26 | + 'STOP_TIME' => "'{$stop}'", |
| 27 | + 'STEP_SIZE' => "'1 m'", |
| 28 | + 'CSV_FORMAT' => 'YES' |
| 29 | + ]; |
| 30 | + $ch = curl_init('https://ssd.jpl.nasa.gov/api/horizons.api'); |
| 31 | + curl_setopt($ch, CURLOPT_RETURNTRANSFER, true); |
| 32 | + curl_setopt($ch, CURLOPT_POST, true); |
| 33 | + curl_setopt($ch, CURLOPT_POSTFIELDS, http_build_query($post)); |
| 34 | + curl_setopt($ch, CURLOPT_USERAGENT, 'Deepsky-AstronomyLibrary/1.0'); |
| 35 | + $resp = curl_exec($ch); |
| 36 | + $err = curl_error($ch); |
| 37 | + curl_close($ch); |
| 38 | + // If the API returned JSON, try to extract a textual result block to keep |
| 39 | + // downstream parsing compatible with existing logic. |
| 40 | + $decoded = @json_decode($resp, true); |
| 41 | + if (is_array($decoded)) { |
| 42 | + // Helper: recursively search for any string that contains $$SOE..$$EOE |
| 43 | + $findBlock = function ($item) use (&$findBlock) { |
| 44 | + if (is_string($item)) { |
| 45 | + if (strpos($item, '$$SOE') !== false) return $item; |
| 46 | + return null; |
| 47 | + } |
| 48 | + if (is_array($item)) { |
| 49 | + foreach ($item as $v) { |
| 50 | + $res = $findBlock($v); |
| 51 | + if ($res !== null) return $res; |
| 52 | + } |
| 53 | + } |
| 54 | + return null; |
| 55 | + }; |
| 56 | + |
| 57 | + $block = $findBlock($decoded); |
| 58 | + if ($block !== null) { |
| 59 | + return [$block, $err]; |
| 60 | + } |
| 61 | + |
| 62 | + // Some API responses include a 'result' string or 'data' key containing |
| 63 | + // the textual output — try those as fallbacks. |
| 64 | + if (isset($decoded['result']) && is_string($decoded['result'])) return [$decoded['result'], $err]; |
| 65 | + if (isset($decoded['data']) && is_string($decoded['data'])) return [$decoded['data'], $err]; |
| 66 | + |
| 67 | + // Otherwise return the original raw response so existing fallback parsing |
| 68 | + // can still attempt to find numeric ids or text blocks. |
| 69 | + } |
| 70 | + |
| 71 | + return [$resp, $err]; |
| 72 | +} |
| 73 | + |
| 74 | +$site = "'{$lon},{$lat},{$alt_km}'"; |
| 75 | +$command = "'{$des}'"; |
| 76 | +list($resp, $err) = doQuery($command, $site, $start, $stop); |
| 77 | +if ($resp === false || empty($resp)) { |
| 78 | + echo json_encode(['error' => 'horizons empty', 'curl' => $err]); |
| 79 | + exit(1); |
| 80 | +} |
| 81 | + |
| 82 | +// Save full raw response inside the workspace for debugging |
| 83 | +@file_put_contents(__DIR__ . '/horizons_raw.txt', $resp); |
| 84 | + |
| 85 | +// If no $$SOE block, attempt to resolve an index-search result and re-query. |
| 86 | +if (!preg_match('/\$\$SOE([\s\S]*?)\$\$EOE/', $resp, $m)) { |
| 87 | + $rec = null; |
| 88 | + |
| 89 | + // 1) Common Horizons numeric IDs often start with 9 and are 6+ digits. |
| 90 | + if (preg_match_all('/\b(9\d{5,9})\b/', $resp, $mrec)) { |
| 91 | + $matches = $mrec[1]; |
| 92 | + $rec = end($matches); |
| 93 | + } |
| 94 | + |
| 95 | + // 2) If not found, try to parse index-like lines beginning with '1) ...', '2) ...' |
| 96 | + if ($rec === null) { |
| 97 | + if (preg_match_all('/^\s*\d+\)\s*(.+)$/m', $resp, $mlines)) { |
| 98 | + foreach ($mlines[1] as $ln) { |
| 99 | + if (preg_match_all('/\b(\d{4,9})\b/', $ln, $mt)) { |
| 100 | + // pick the first candidate token that doesn't look like a year |
| 101 | + foreach ($mt[1] as $tok) { |
| 102 | + $intval = intval($tok); |
| 103 | + if ($intval < 1800 || $intval > 2200) { |
| 104 | + $rec = $tok; |
| 105 | + break 2; |
| 106 | + } |
| 107 | + } |
| 108 | + } |
| 109 | + } |
| 110 | + } |
| 111 | + } |
| 112 | + |
| 113 | + // 3) Fallback: search all numeric tokens (4-9 digits) and exclude year-like values |
| 114 | + if ($rec === null) { |
| 115 | + if (preg_match_all('/\b(\d{4,9})\b/', $resp, $mall)) { |
| 116 | + foreach ($mall[1] as $tok) { |
| 117 | + $intval = intval($tok); |
| 118 | + if ($intval < 1800 || $intval > 2200) { |
| 119 | + $rec = $tok; |
| 120 | + break; |
| 121 | + } |
| 122 | + } |
| 123 | + } |
| 124 | + } |
| 125 | + |
| 126 | + if ($rec !== null) { |
| 127 | + $command = "'{$rec}'"; |
| 128 | + list($resp2, $err2) = doQuery($command, $site, $start, $stop); |
| 129 | + if ($resp2 === false || empty($resp2)) { |
| 130 | + echo json_encode(['error' => 'requery failed', 'curl' => $err2]); |
| 131 | + exit(1); |
| 132 | + } |
| 133 | + $resp = $resp2; |
| 134 | + } else { |
| 135 | + echo json_encode(['error' => 'no data block and no record id']); |
| 136 | + exit(1); |
| 137 | + } |
| 138 | +} |
| 139 | +if (!preg_match('/\$\$SOE([\s\S]*?)\$\$EOE/', $resp, $mblock)) { |
| 140 | + echo json_encode(['error' => 'no block after requery']); |
| 141 | + exit(1); |
| 142 | +} |
| 143 | +$block = $mblock[1]; |
| 144 | + |
| 145 | +// Save raw block into workspace for quick inspection |
| 146 | +@file_put_contents(__DIR__ . '/horizons_block.txt', $block); |
| 147 | + |
| 148 | +// Split block into lines and choose the best data line (first non-empty, non-header line) |
| 149 | +$lines = preg_split('/\r?\n/', trim($block)); |
| 150 | +$dataLine = null; |
| 151 | +foreach ($lines as $ln) { |
| 152 | + $ln = trim($ln); |
| 153 | + if ($ln === '') continue; |
| 154 | + if (strpos($ln, '***') === 0) continue; |
| 155 | + if (strpos($ln, ',') !== false && preg_match('/\d/', $ln)) { |
| 156 | + $dataLine = $ln; |
| 157 | + break; |
| 158 | + } |
| 159 | +} |
| 160 | +if ($dataLine === null) { |
| 161 | + echo json_encode(['error' => 'no data line in block']); |
| 162 | + exit(1); |
| 163 | +} |
| 164 | + |
| 165 | +// Split fields on commas and trim |
| 166 | +$fields = array_map('trim', preg_split('/\s*,\s*/', $dataLine)); |
| 167 | + |
| 168 | +// Helper to find index matching regex |
| 169 | +function findFieldIndex(array $fields, string $regex) |
| 170 | +{ |
| 171 | + foreach ($fields as $i => $f) { |
| 172 | + if (preg_match($regex, $f)) return $i; |
| 173 | + } |
| 174 | + return -1; |
| 175 | +} |
| 176 | + |
| 177 | +// RA formats: 'HH MM SS.ss' or 'HH:MM:SS' |
| 178 | +$raIdx = findFieldIndex($fields, '/^\s*\d{1,2}[:\s]\d{1,2}[:\s]\d{1,2}(\.\d+)?/'); |
| 179 | +// DEC formats: prefer signed degrees (leading + or -) for unambiguous detection |
| 180 | +$decIdx = findFieldIndex($fields, '/^[\+\-]\d{1,3}[:\s]\d{1,2}[:\s]\d{1,2}(\.\d+)?/'); |
| 181 | + |
| 182 | +// Fallback to legacy indices if regex search failed |
| 183 | +if ($raIdx === -1) $raIdx = isset($fields[5]) ? 5 : (isset($fields[3]) ? 3 : 0); |
| 184 | +if ($decIdx === -1) $decIdx = isset($fields[6]) ? 6 : (isset($fields[4]) ? 4 : 1); |
| 185 | + |
| 186 | +$raStr = $fields[$raIdx] ?? ''; |
| 187 | +$decStr = $fields[$decIdx] ?? ''; |
| 188 | + |
| 189 | +// Prefer the a-app (apparent) RA/Dec pair if present (usually columns 5 and 6) |
| 190 | +if (isset($fields[5]) && isset($fields[6])) { |
| 191 | + $maybeRa = $fields[5]; |
| 192 | + $maybeDec = $fields[6]; |
| 193 | + if ( |
| 194 | + preg_match('/^\s*\d{1,2}[:\s]\d{1,2}[:\s]\d{1,2}(\.\d+)?/', $maybeRa) |
| 195 | + && preg_match('/^[\+\-]\d{1,3}[:\s]\d{1,2}[:\s]\d{1,2}(\.\d+)?/', $maybeDec) |
| 196 | + ) { |
| 197 | + $raStr = $maybeRa; |
| 198 | + $decStr = $maybeDec; |
| 199 | + } |
| 200 | +} |
| 201 | + |
| 202 | +function hmsToHours($s) |
| 203 | +{ |
| 204 | + $s = trim($s); |
| 205 | + if (strpos($s, ':') !== false) { |
| 206 | + list($h, $m, $sec) = explode(':', $s) + [0, 0, 0]; |
| 207 | + return intval($h) + intval($m) / 60.0 + floatval($sec) / 3600.0; |
| 208 | + } |
| 209 | + $parts = preg_split('/\s+/', $s); |
| 210 | + if (count($parts) >= 3) return intval($parts[0]) + intval($parts[1]) / 60.0 + floatval($parts[2]) / 3600.0; |
| 211 | + return floatval($s); |
| 212 | +} |
| 213 | +function dmsToDeg($s) |
| 214 | +{ |
| 215 | + $s = trim($s); |
| 216 | + $sign = 1; |
| 217 | + if ($s[0] === '+' || $s[0] === '-') { |
| 218 | + if ($s[0] === '-') $sign = -1; |
| 219 | + $s = substr($s, 1); |
| 220 | + } |
| 221 | + if (strpos($s, ':') !== false) { |
| 222 | + list($d, $m, $sec) = explode(':', $s) + [0, 0, 0]; |
| 223 | + return $sign * (abs(intval($d)) + intval($m) / 60.0 + floatval($sec) / 3600.0); |
| 224 | + } |
| 225 | + $parts = preg_split('/\s+/', $s); |
| 226 | + if (count($parts) >= 3) return $sign * (abs(intval($parts[0])) + intval($parts[1]) / 60.0 + floatval($parts[2]) / 3600.0); |
| 227 | + return $sign * floatval($s); |
| 228 | +} |
| 229 | +$raH = hmsToHours($raStr); |
| 230 | +$decD = dmsToDeg($decStr); |
| 231 | +$out = ['ra_hours' => $raH, 'dec_deg' => $decD, 'raw_ra' => $raStr, 'raw_dec' => $decStr]; |
| 232 | +// Save structured JSON output for inspection |
| 233 | +@file_put_contents(__DIR__ . '/horizons_resp.json', json_encode($out)); |
| 234 | +echo json_encode($out); |
0 commit comments