[sid, arrMin, depMin, depTod, dayOff] $cur = null; $tid = -1; $flat = []; // When verified-only is on, the graph contains ONLY trains with a run_days mask; // unverified trains (untried or no_schedule) are excluded from routing entirely. $verifiedOnly = defined('RT_VERIFIED_ONLY') ? RT_VERIFIED_ONLY : true; try { if ($verifiedOnly) { $stmt = $db->query( 'SELECT ts.train_no, ts.station_code, ts.arr_min, ts.dep_min, ts.departure, ts.day_offset FROM train_stops ts JOIN trains t ON t.train_no = ts.train_no WHERE t.run_days IS NOT NULL ORDER BY ts.train_no, ts.seq' ); } else { $stmt = $db->query( 'SELECT train_no, station_code, arr_min, dep_min, departure, day_offset FROM train_stops ORDER BY train_no, seq' ); } } catch (Throwable $e) { // older schema without run_days -> fall back to all trains $stmt = $db->query( 'SELECT train_no, station_code, arr_min, dep_min, departure, day_offset FROM train_stops ORDER BY train_no, seq' ); } $flush = function() use (&$trips, &$tid, &$flat) { if ($tid >= 0) $trips[$tid] = $flat; }; foreach ($stmt as $row) { if ($row['train_no'] !== $cur) { $flush(); $cur = $row['train_no']; $tid = count($trainNo); $trainNo[] = $cur; $flat = []; } $code = $row['station_code']; if (!isset($stId[$code])) { $stId[$code] = count($stRev); $stRev[] = $code; } $sid = $stId[$code]; $arrMin = $row['arr_min'] === null ? -1 : (int)$row['arr_min']; $depMin = $row['dep_min'] === null ? -1 : (int)$row['dep_min']; $depTod = $row['departure'] === null ? -1 : rt_tod($row['departure']); $dayOff = (int)$row['day_offset']; $idx = intdiv(count($flat), 5); $flat[] = $sid; $flat[] = $arrMin; $flat[] = $depMin; $flat[] = $depTod; $flat[] = $dayOff; if ($depTod >= 0) { $dep[$sid][] = $tid; $dep[$sid][] = $idx; } } $flush(); // Per-train run-day mask + freshness, keyed to tid via train number. $noToTid = array_flip($trainNo); // train_no => tid $runmask = array_fill(0, count($trainNo), null); // '1111111' (Mon..Sun) or null = unharvested $fresh = array_fill(0, count($trainNo), 0); // unix ts of refreshed_at, 0 = never // run_days / refreshed_at live on trains; tolerate older schemas without them. try { $q = $db->query('SELECT train_no, run_days, refreshed_at FROM trains'); foreach ($q as $r) { $tn = $r['train_no']; if (!isset($noToTid[$tn])) continue; $i = $noToTid[$tn]; $m = $r['run_days'] ?? null; if (is_string($m) && preg_match('/^[01]{7}$/', $m)) $runmask[$i] = $m; $fresh[$i] = !empty($r['refreshed_at']) ? (int)strtotime($r['refreshed_at']) : 0; } } catch (Throwable $e) { /* columns not present yet -> everything stays daily */ } return ['stId'=>$stId, 'stRev'=>$stRev, 'trainNo'=>$trainNo, 'trips'=>$trips, 'dep'=>$dep, 'runmask'=>$runmask, 'fresh'=>$fresh]; } function rt_tod(?string $time): int { if ($time === null || $time === '') return -1; $p = explode(':', $time); return count($p) >= 2 ? ((int)$p[0]) * 60 + (int)$p[1] : -1; } /* next absolute minute >= E whose time-of-day == tod (daily instances) */ function rt_next(int $tod, int $E): int { $d = (($tod - $E) % 1440 + 1440) % 1440; return $E + $d; } /* Does this train run for a boarding at $depAbs? mask null => assume daily (true). * baseDow = ISO weekday (1=Mon..7=Sun) of the SEARCH date (midnight = abs minute 0). * dayOff = boarding stop's day_offset (calendar days after the train's origin dep). * Origin departure weekday = searchDOW + floor(depAbs/1440) - dayOff (mod 7). */ function rt_runs(?string $mask, int $baseDow, int $depAbs, int $dayOff): bool { if ($mask === null) return true; $boardDayIdx = intdiv($depAbs, 1440); $idx = (($baseDow - 1) + $boardDayIdx - $dayOff) % 7; $idx = ($idx + 7) % 7; // Mon=0 .. Sun=6 return $mask[$idx] === '1'; } /* First daily instance >= earliest, within window, on a day the train ACTUALLY runs. -1 if none. */ function rt_first_run(?string $mask, int $baseDow, int $dayOff, int $depTod, int $earliest, int $window): int { $start = rt_next($depTod, $earliest); for ($cand = $start; $cand <= $earliest + $window; $cand += 1440) { if (rt_runs($mask, $baseDow, $cand, $dayOff)) return $cand; } return -1; } function rt_plan(array $tt, string $fromCode, string $toCode, int $queryDep, array $opt = []): array { $minT = $opt['min_transfer'] ?? RT_MIN_TRANSFER; $maxL = $opt['max_layover'] ?? RT_MAX_LAYOVER; $hor = $opt['horizon'] ?? RT_HORIZON; $ymd = $opt['date'] ?? date('Y-m-d'); $baseDow = (int)date('N', strtotime($ymd)); // 1=Mon..7=Sun if (!isset($tt['stId'][$fromCode], $tt['stId'][$toCode])) return ['direct'=>[], 'hops'=>[]]; $from = $tt['stId'][$fromCode]; $to = $tt['stId'][$toCode]; $trips = $tt['trips']; $dep = $tt['dep']; $runmask = $tt['runmask'] ?? []; $direct = rt_direct($tt, $from, $to, $queryDep, $hor, $baseDow); if (!isset($dep[$from])) return ['direct'=>rt_label_journey($tt, $direct), 'hops'=>[]]; $arr = [0 => [$from => $queryDep]]; $pred = [0 => []]; $marked = [$from => $queryDep]; for ($r = 1; $r <= RT_MAX_ROUNDS; $r++) { $arr[$r] = $arr[$r-1]; $pred[$r] = $pred[$r-1]; $ridden = []; $newMarked = []; foreach ($marked as $s => $a) { if (!isset($dep[$s])) continue; $earliest = ($r === 1) ? $a : $a + $minT; $window = ($r === 1) ? $hor : $maxL; $arrivedOn = ($r > 1 && isset($pred[$r-1][$s])) ? $pred[$r-1][$s]['train'] : -1; $d = $dep[$s]; $dn = count($d); for ($e = 0; $e < $dn; $e += 2) { $tid = $d[$e]; $idx = $d[$e+1]; if ($tid === $arrivedOn) continue; $trip = $trips[$tid]; $off = $idx * 5; $depTod = $trip[$off+3]; $depMin = $trip[$off+2]; $dayOff = $trip[$off+4]; $mask = $runmask[$tid] ?? null; $depAbs = rt_first_run($mask, $baseDow, $dayOff, $depTod, $earliest, $window); if ($depAbs < 0) continue; // doesn't run within the window if (isset($ridden[$tid]) && $depAbs >= $ridden[$tid]) continue; $ridden[$tid] = $depAbs; $bestDest = $arr[$r][$to] ?? RT_INF; $m = count($trip); for ($j = $off + 5; $j < $m; $j += 5) { $arrMin = $trip[$j+1]; if ($arrMin < 0) continue; $arrAbs = $depAbs + ($arrMin - $depMin); if ($arrAbs >= $bestDest) continue; $code2 = $trip[$j]; if ($arrAbs < ($arr[$r][$code2] ?? RT_INF)) { $arr[$r][$code2] = $arrAbs; $pred[$r][$code2] = ['from'=>$s, 'train'=>$tid, 'board'=>$depAbs, 'arr'=>$arrAbs]; $newMarked[$code2] = $arrAbs; } } } } $marked = $newMarked; if (!$marked) break; } $hops = []; for ($r = 1; $r <= RT_MAX_ROUNDS; $r++) { if (!isset($arr[$r][$to])) continue; if ($r > 1 && isset($arr[$r-1][$to]) && $arr[$r][$to] >= $arr[$r-1][$to]) continue; $journey = rt_reconstruct($pred, $r, $from, $to); $changes = count($journey) - 1; if ($changes >= 1) $hops[$changes] = rt_label_journey($tt, $journey); } return ['direct'=>rt_label_journey($tt, $direct), 'hops'=>$hops]; } function rt_reconstruct(array $pred, int $R, int $from, int $to): array { $legs = []; $code = $to; $r = $R; $guard = 0; while ($code !== $from && $r >= 1 && $guard++ < 12) { if (!isset($pred[$r][$code])) break; $leg = $pred[$r][$code]; $legs[] = ['from'=>$leg['from'], 'to'=>$code, 'train'=>$leg['train'], 'board'=>$leg['board'], 'arr'=>$leg['arr']]; $code = $leg['from']; $r--; } return array_reverse($legs); } function rt_direct(array $tt, int $from, int $to, int $queryDep, int $hor, int $baseDow): array { $out = []; if (!isset($tt['dep'][$from])) return $out; $runmask = $tt['runmask'] ?? []; $d = $tt['dep'][$from]; $dn = count($d); for ($e = 0; $e < $dn; $e += 2) { $tid = $d[$e]; $idx = $d[$e+1]; $trip = $tt['trips'][$tid]; $off = $idx*5; $m = count($trip); $depTod = $trip[$off+3]; $depMin = $trip[$off+2]; $dayOff = $trip[$off+4]; $mask = $runmask[$tid] ?? null; for ($j = $off+5; $j < $m; $j += 5) { if ($trip[$j] !== $to || $trip[$j+1] < 0) continue; $depAbs = rt_first_run($mask, $baseDow, $dayOff, $depTod, $queryDep, $hor); if ($depAbs < 0) break; // doesn't run in window $arrAbs = $depAbs + ($trip[$j+1] - $depMin); $out[] = ['from'=>$from, 'to'=>$to, 'train'=>$tid, 'board'=>$depAbs, 'arr'=>$arrAbs]; break; } } usort($out, fn($a,$b) => $a['arr'] <=> $b['arr']); return $out; } /* map ids back to codes / train numbers, and attach run-day label + freshness */ function rt_label_journey(array $tt, array $legs): array { $now = time(); foreach ($legs as &$l) { $tid = $l['train']; $mask = $tt['runmask'][$tid] ?? null; $ts = $tt['fresh'][$tid] ?? 0; $l['from'] = $tt['stRev'][$l['from']]; $l['to'] = $tt['stRev'][$l['to']]; $l['train'] = $tt['trainNo'][$tid]; $l['days'] = $mask === null ? null : (function_exists('rd_label') ? rd_label($mask) : $mask); $l['harvested'] = $mask !== null; $l['stale'] = $mask !== null && ($ts === 0 || $ts < $now - RT_STALE_DAYS*86400); } return $legs; }