package main import ( "container/heap" "encoding/json" "fmt" "log" "net/http" "os" "sort" "strconv" "sync" "time" "git.tornberg.me/go-gtfs/pkg/reader" "git.tornberg.me/go-gtfs/pkg/types" ) // TripPlanner handles preprocessed transit data for efficient routing type TripPlanner struct { stops map[string]*types.Stop trips map[string]*types.Trip routes map[string]*types.Route agencies map[string]*types.Agency transfers []types.Transfer stopTimes map[string][]types.StopTime graph map[string][]Edge weightedStops map[string][]Connection } type StopWithPossibleConnections struct { *types.Stop PossibleConnections []Connection } type Connection struct { *types.Stop Distance float64 Time time.Duration } const ( transferPenalty = 90 * time.Minute maxTransfers = 4 maxWaitBetweenTrips = 1 * time.Hour //trajectoryAngleTolerance = 220.0 maxTravelDuration = 12 * time.Hour maxDetourFactor = 2 ) // NewTripPlanner creates a new trip planner instance func NewTripPlanner() *TripPlanner { return &TripPlanner{ stops: make(map[string]*types.Stop), trips: make(map[string]*types.Trip), routes: make(map[string]*types.Route), agencies: make(map[string]*types.Agency), stopTimes: make(map[string][]types.StopTime), graph: make(map[string][]Edge), weightedStops: make(map[string][]Connection), } } // LoadData loads all GTFS data func (tp *TripPlanner) LoadData(dataDir string) error { files := []string{"agency", "routes", "stops", "trips", "stop_times", "transfers"} for _, file := range files { f, err := os.Open(dataDir + "/" + file + ".txt") if err != nil { log.Fatalf("failed to open %s: %v", file, err) } switch file { case "agency": err = reader.ParseAgencies(f, func(a types.Agency) { tp.agencies[a.AgencyID] = &a }) case "routes": err = reader.ParseRoutes(f, func(r types.Route) { tp.routes[r.RouteID] = &r if ag, ok := tp.agencies[r.AgencyID]; ok { r.Agency = ag ag.AddRoute(&r) } }) case "stops": err = reader.ParseStops(f, func(s types.Stop) { tp.stops[s.StopID] = &s }) case "trips": err = reader.ParseTrips(f, func(t types.Trip) { trip := t if route, ok := tp.routes[trip.RouteID]; ok { trip.SetRoute(route) route.AddTrip(&trip) } else { log.Printf("route %s not found", trip.RouteID) } if agency, ok := tp.agencies[trip.AgencyID]; ok { trip.Agency = agency } else { log.Printf("agency %s not found", trip.AgencyID) } tp.trips[trip.TripID] = &trip }) case "stop_times": err = reader.ParseStopTimes(f, func(st types.StopTime) { stop, ok := tp.stops[st.StopID] if ok { st.SetStop(stop) } else { log.Printf("stop %s not found", st.StopID) } trp, ok := tp.trips[st.TripID] if !ok { log.Printf("trip %s not found", st.TripID) } else { stop.AddTrip(trp) trp.AddStopTime(&st) } }) case "transfers": err = reader.ParseTransfers(f, func(tr types.Transfer) { tp.transfers = append(tp.transfers, tr) stop, ok := tp.stops[tr.FromStopID] if ok { stop.AddTransfer(&tr) } else { log.Printf("stop %s not found for transfer", tr.FromStopID) } }) } if err != nil { log.Printf("failed to parse %s: %v", file, err) } f.Close() } return nil } // Preprocess builds the routing graph and precomputes routes func (tp *TripPlanner) Preprocess() error { if hosjo, ok := tp.stops["740025287"]; ok { trips := hosjo.GetTripsAfter(time.Now()) for _, trip := range trips { log.Printf("Trip %s (%s):", trip.TripShortName, trip.TripHeadsign) for stop := range trip.GetDirectPossibleDestinations(hosjo, time.Now()) { log.Printf("- Stop %s at %s", stop.Stop.StopName, stop.DepartureTime) } } } // Build graph with trip edges for tripID, sts := range tp.stopTimes { sort.Slice(sts, func(i, j int) bool { return sts[i].StopSequence < sts[j].StopSequence }) for i := 0; i < len(sts)-1; i++ { from := sts[i].StopID to := sts[i+1].StopID departure := parseTime(sts[i].DepartureTime) arrival := parseTime(sts[i+1].ArrivalTime) timeDiff := arrival - departure if timeDiff > 0 { tp.graph[from] = append(tp.graph[from], Edge{To: to, TripID: tripID, Time: timeDiff, DepartureTime: departure}) } } } // Add transfer edges for _, tr := range tp.transfers { if tr.TransferType == 2 { // minimum transfer time tp.graph[tr.FromStopID] = append(tp.graph[tr.FromStopID], Edge{ To: tr.ToStopID, TripID: "transfer", Time: float64(tr.MinTransferTime), DepartureTime: 0, }) } } tp.stopTimes = nil return nil } // FindRoute finds the best route between two stops starting at the given time func (tp *TripPlanner) FindRoute(from, to string, when time.Time) *Route { routes := tp.FindRoutes(from, to, when, 1) if len(routes) > 0 { return routes[0] } return nil } // FindRoutes finds the best routes (up to num) between two stops starting at the given time func (tp *TripPlanner) FindRoutes(from, to string, when time.Time, num int) []*Route { var allRoutes []*Route seen := make(map[string]bool) // to avoid duplicates based on departure and arrival times for i := 0; i < num*20 && len(allRoutes) < num; i++ { route := tp.findRoute(from, to, when.Add(time.Duration(i*5)*time.Minute)) if route != nil { key := fmt.Sprintf("%d-%d", route.Legs[0].DepartureTime.Unix(), route.Legs[len(route.Legs)-1].ArrivalTime.Unix()) if !seen[key] { seen[key] = true allRoutes = append(allRoutes, route) log.Printf("Found route %d: departure %v, arrival %v, duration %v", len(allRoutes), route.Legs[0].DepartureTime, route.Legs[len(route.Legs)-1].ArrivalTime, route.Duration()) } } } log.Printf("Total routes found: %d", len(allRoutes)) return allRoutes } // findRoute implements a time-aware Dijkstra algorithm for routing func (tp *TripPlanner) findRoute(start, end string, when time.Time) *Route { startStop := tp.GetStop(start) if startStop == nil { return nil } goalStop := tp.GetStop(end) if goalStop == nil { return nil } maxAllowedDistance := haversine(startStop.StopLat, startStop.StopLon, goalStop.StopLat, goalStop.StopLon) * maxDetourFactor arrival := make(map[string]time.Time) cost := make(map[string]time.Time) prev := make(map[string]PathInfo) pq := &priorityQueue{} heap.Init(pq) arrival[start] = when cost[start] = when prev[start] = PathInfo{Prev: "", TripID: "", DepartureTime: when, Transfers: 0, LastTrip: "", WaitDuration: 0} heap.Push(pq, &pqItem{Stop: start, Cost: when}) for pq.Len() > 0 { item := heap.Pop(pq).(*pqItem) current := item.Stop if storedCost, ok := cost[current]; !ok || item.Cost.After(storedCost) { continue } currentArrival, ok := arrival[current] if !ok || currentArrival.IsZero() { continue } if current == end { stopsPath := []string{} current := end visited := make(map[string]bool) for current != "" { if visited[current] { break } visited[current] = true stopsPath = append([]string{current}, stopsPath...) if current == start { break } info, ok := prev[current] if !ok || info.Prev == "" { break } current = info.Prev } legs := []Leg{} var currentLeg *Leg for i := 0; i < len(stopsPath)-1; i++ { from := stopsPath[i] to := stopsPath[i+1] tripID := prev[to].TripID if tripID != "transfer" { if currentLeg == nil || currentLeg.TripID != tripID { if currentLeg != nil { legs = append(legs, *currentLeg) } trip := tp.GetTrip(tripID) route := tp.GetRoute(trip.RouteID) currentLeg = &Leg{ TripID: tripID, From: from, FromStop: tp.GetStop(from), Trip: trip, Agency: tp.GetAgency(route.AgencyID), Route: route, Stops: []string{from}, DepartureTime: prev[to].DepartureTime, } } currentLeg.To = to currentLeg.ToStop = tp.GetStop(to) currentLeg.Stops = append(currentLeg.Stops, to) currentLeg.ArrivalTime = arrival[to] } } if currentLeg != nil { currentLeg.To = stopsPath[len(stopsPath)-1] currentLeg.ToStop = tp.GetStop(currentLeg.To) currentLeg.ArrivalTime = arrival[stopsPath[len(stopsPath)-1]] legs = append(legs, *currentLeg) } return &Route{Legs: legs} } currentStop := tp.GetStop(current) if currentStop == nil { continue } //currentBearing := bearing(currentStop.StopLat, currentStop.StopLon, goalStop.StopLat, goalStop.StopLon) for _, edge := range tp.graph[current] { if edge.To == current { continue } if info, ok := prev[current]; ok && info.Prev == edge.To && info.TripID == edge.TripID { continue } nextStop := tp.GetStop(edge.To) if nextStop == nil { continue } distanceToGoal := haversine(nextStop.StopLat, nextStop.StopLon, goalStop.StopLat, goalStop.StopLon) if distanceToGoal > maxAllowedDistance { continue } // if edge.TripID != "transfer" { // edgeBearing := bearing(currentStop.StopLat, currentStop.StopLon, nextStop.StopLat, nextStop.StopLon) // if angleDifference(currentBearing, edgeBearing) > trajectoryAngleTolerance { // continue // } // } var arrivalTime time.Time var departureTime time.Time var waitDuration time.Duration if edge.TripID == "transfer" { waitDuration = time.Duration(edge.Time) * time.Second if waitDuration > maxWaitBetweenTrips { continue } arrivalTime = currentArrival.Add(waitDuration) departureTime = arrivalTime } else { depSec := edge.DepartureTime day := currentArrival.Truncate(24 * time.Hour) departure := day.Add(time.Duration(depSec) * time.Second) if departure.Before(currentArrival) { departure = departure.Add(24 * time.Hour) } if departure.After(currentArrival) || departure.Equal(currentArrival) { arrivalTime = departure.Add(time.Duration(edge.Time) * time.Second) departureTime = departure waitDuration = departureTime.Sub(currentArrival) if waitDuration > maxWaitBetweenTrips { continue } } else { continue } } if arrivalTime.Sub(when) > maxTravelDuration { continue } currentTransfers := prev[current].Transfers lastTrip := prev[current].LastTrip newTransfers := currentTransfers var newLastTrip string if edge.TripID == "transfer" { newLastTrip = lastTrip } else { newLastTrip = edge.TripID if lastTrip != "" && lastTrip != edge.TripID { newTransfers++ } } if newTransfers > maxTransfers { continue } costTime := arrivalTime if edge.TripID != "transfer" && lastTrip != "" && lastTrip != edge.TripID { costTime = costTime.Add(transferPenalty) } existingCost, hasCost := cost[edge.To] existingArrival := arrival[edge.To] existingInfo, havePrev := prev[edge.To] shouldRelax := !hasCost || existingCost.IsZero() || costTime.Before(existingCost) if !shouldRelax && costTime.Equal(existingCost) { if existingArrival.IsZero() || arrivalTime.Before(existingArrival) { shouldRelax = true } else if havePrev && arrivalTime.Equal(existingArrival) { if newTransfers < existingInfo.Transfers { shouldRelax = true } else if waitDuration < existingInfo.WaitDuration { shouldRelax = true } } } if shouldRelax { ancestor := current createsCycle := false for ancestor != "" { if ancestor == edge.To { createsCycle = true break } info, ok := prev[ancestor] if !ok { break } ancestor = info.Prev } if createsCycle { continue } arrival[edge.To] = arrivalTime cost[edge.To] = costTime prev[edge.To] = PathInfo{Prev: current, TripID: edge.TripID, DepartureTime: departureTime, Transfers: newTransfers, LastTrip: newLastTrip, WaitDuration: waitDuration} heap.Push(pq, &pqItem{Stop: edge.To, Cost: costTime}) } } } return nil } func (tp *TripPlanner) GetRoute(routeId string) *types.Route { if routeId == "" { return nil } route, ok := tp.routes[routeId] if !ok { return nil } return route } func (tp *TripPlanner) GetAgency(agencyId string) *types.Agency { if agencyId == "" { return nil } agency, ok := tp.agencies[agencyId] if !ok { return nil } return agency } func (tp *TripPlanner) GetTrip(tripId string) *types.Trip { if tripId == "" { return nil } trip, ok := tp.trips[tripId] if !ok { return nil } return trip } func (tp *TripPlanner) GetStop(prev string) *types.Stop { if prev == "" { return nil } stop, ok := tp.stops[prev] if !ok { return nil } return stop } type Edge struct { To string TripID string Time float64 DepartureTime float64 } type TripDetail struct { RouteShortName string `json:"route_short_name"` AgencyName string `json:"agency_name"` Stops []string `json:"stops"` } type Leg struct { From string `json:"-"` FromStop *types.Stop `json:"from"` To string `json:"-"` ToStop *types.Stop `json:"to"` TripID string `json:"-"` Trip *types.Trip `json:"trip"` Stops []string `json:"stops"` Agency *types.Agency `json:"agency"` Route *types.Route `json:"route"` DepartureTime time.Time `json:"departure_time"` ArrivalTime time.Time `json:"arrival_time"` } type Route struct { Legs []Leg `json:"legs"` } func (r *Route) EndTime() time.Time { if len(r.Legs) == 0 { return time.Time{} } return r.Legs[len(r.Legs)-1].ArrivalTime } func (r *Route) StartTime() time.Time { if len(r.Legs) == 0 { return time.Time{} } return r.Legs[0].DepartureTime } func (r *Route) Duration() time.Duration { if len(r.Legs) == 0 { return 0 } return r.Legs[len(r.Legs)-1].ArrivalTime.Sub(r.Legs[0].DepartureTime) } type PathInfo struct { Prev string TripID string DepartureTime time.Time Transfers int LastTrip string WaitDuration time.Duration } func main() { tp := NewTripPlanner() wg := &sync.WaitGroup{} if err := tp.LoadData("data"); err != nil { fmt.Printf("Failed to load data: %v\n", err) os.Exit(1) } wg.Wait() if err := tp.Preprocess(); err != nil { fmt.Printf("Failed to preprocess data: %v\n", err) os.Exit(1) } http.HandleFunc("/api/stops", func(w http.ResponseWriter, r *http.Request) { w.Header().Set("Access-Control-Allow-Origin", "*") w.Header().Set("Access-Control-Allow-Methods", "GET") w.Header().Set("Access-Control-Allow-Headers", "Content-Type") stopList := []types.Stop{} for _, s := range tp.stops { if _, hasConnections := tp.graph[s.StopID]; hasConnections { stopList = append(stopList, *s) } } w.Header().Set("Content-Type", "application/json") json.NewEncoder(w).Encode(stopList) }) http.HandleFunc("/api/route", func(w http.ResponseWriter, r *http.Request) { w.Header().Set("Access-Control-Allow-Origin", "*") w.Header().Set("Access-Control-Allow-Methods", "GET") w.Header().Set("Access-Control-Allow-Headers", "Content-Type") from := r.URL.Query().Get("from") to := r.URL.Query().Get("to") whenStr := r.URL.Query().Get("when") numStr := r.URL.Query().Get("num") num := 3 if numStr != "" { if parsed, err := strconv.Atoi(numStr); err == nil && parsed > 0 { num = parsed } } when := time.Now() if whenStr != "" { if parsed, err := time.Parse(time.RFC3339, whenStr); err == nil { when = parsed } } if from == "" || to == "" { w.WriteHeader(http.StatusBadRequest) json.NewEncoder(w).Encode(map[string]string{"error": "from and to parameters required"}) return } routes := tp.FindRoutes(from, to, when, num) if len(routes) == 0 { w.WriteHeader(http.StatusNotFound) json.NewEncoder(w).Encode(map[string]string{"error": "no route found"}) return } w.Header().Set("Content-Type", "application/json") json.NewEncoder(w).Encode(routes) }) log.Printf("Listening on 8080") http.ListenAndServe(":8080", nil) }