diff --git a/cmd/planner/astar.go b/cmd/planner/astar.go deleted file mode 100644 index 770c4c1..0000000 --- a/cmd/planner/astar.go +++ /dev/null @@ -1,261 +0,0 @@ -package main - -// import ( -// "container/heap" -// "time" - -// "git.tornberg.me/go-gtfs/pkg/types" -// ) - -// // CostFactors defines the parameters for the cost function in A* search. -// type CostFactors struct { -// TransferPenalty time.Duration -// MaxTransfers int -// MaxWaitBetweenTrips time.Duration -// MaxTravelDuration time.Duration -// MaxDetourFactor float64 -// } - -// // Heuristic function estimates the cost from a node to the goal. -// // For pathfinding on a map, this is typically the straight-line distance. -// type Heuristic func(from, to *types.Stop) time.Duration - -// // AStarPlanner uses the A* algorithm to find routes. -// type AStarPlanner struct { -// *TripPlanner -// CostFactors CostFactors -// Heuristic Heuristic -// } - -// // NewAStarPlanner creates a new planner that uses the A* algorithm. -// func NewAStarPlanner(tp *TripPlanner, factors CostFactors, heuristic Heuristic) *AStarPlanner { -// return &AStarPlanner{ -// TripPlanner: tp, -// CostFactors: factors, -// Heuristic: heuristic, -// } -// } - -// // findRoute implements a time-aware A* algorithm for routing. -// func (p *AStarPlanner) findRoute(start, end string, when time.Time) *Route { -// startStop := p.GetStop(start) -// if startStop == nil { -// return nil -// } -// goalStop := p.GetStop(end) -// if goalStop == nil { -// return nil -// } -// maxAllowedDistance := haversine(startStop.StopLat, startStop.StopLon, goalStop.StopLat, goalStop.StopLon) * p.CostFactors.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 { -// // Reconstruct path -// return reconstructPath(p.TripPlanner, prev, start, end, arrival) -// } - -// currentStop := p.GetStop(current) -// if currentStop == nil { -// continue -// } - -// for _, edge := range p.graph[current] { -// if edge.To == current { -// continue -// } -// if info, ok := prev[current]; ok && info.Prev == edge.To && info.TripID == edge.TripID { -// continue -// } -// nextStop := p.GetStop(edge.To) -// if nextStop == nil { -// continue -// } - -// distanceToGoal := haversine(nextStop.StopLat, nextStop.StopLon, goalStop.StopLat, goalStop.StopLon) -// if distanceToGoal > maxAllowedDistance { -// 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 > p.CostFactors.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 > p.CostFactors.MaxWaitBetweenTrips { -// continue -// } -// } else { -// continue -// } -// } - -// if arrivalTime.Sub(when) > p.CostFactors.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 > p.CostFactors.MaxTransfers { -// continue -// } - -// // A* cost calculation: g(n) + h(n) -// // g(n) is the actual cost from the start, which is the arrival time with penalties. -// gCost := arrivalTime -// if edge.TripID != "transfer" && lastTrip != "" && lastTrip != edge.TripID { -// gCost = gCost.Add(p.CostFactors.TransferPenalty) -// } - -// // h(n) is the heuristic cost from the current node to the goal. -// hCost := p.Heuristic(nextStop, goalStop) -// fCost := gCost.Add(hCost) - -// existingCost, hasCost := cost[edge.To] -// existingArrival := arrival[edge.To] -// existingInfo, havePrev := prev[edge.To] -// shouldRelax := !hasCost || existingCost.IsZero() || fCost.Before(existingCost) -// if !shouldRelax && fCost.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] = fCost -// 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: fCost}) -// } -// } -// } -// return nil -// } - -// func reconstructPath(tp *TripPlanner, prev map[string]PathInfo, start, end string, arrival map[string]time.Time) *Route { -// 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} -// } diff --git a/cmd/planner/csa.go b/cmd/planner/csa.go deleted file mode 100644 index 8b5fac6..0000000 --- a/cmd/planner/csa.go +++ /dev/null @@ -1,217 +0,0 @@ -package main - -// import ( -// "sort" -// "time" - -// "git.tornberg.me/go-gtfs/pkg/reader" -// "git.tornberg.me/go-gtfs/pkg/types" -// ) - -// // Connection represents a single leg of a trip between two stops. -// type CSAConnection struct { -// DepartureStopID string -// ArrivalStopID string -// DepartureTime types.SecondsAfterMidnight -// ArrivalTime types.SecondsAfterMidnight -// TripID string -// } - -// // CSAPlanner uses the Connection Scan Algorithm for routing. -// type CSAPlanner struct { -// *reader.TripData -// connections []CSAConnection -// } - -// // NewCSAPlanner creates and preprocesses data for the Connection Scan Algorithm. -// func NewCSAPlanner(data *reader.TripData) *CSAPlanner { -// p := &CSAPlanner{ -// TripData: data, -// } -// p.preprocess() -// return p -// } - -// // preprocess creates a sorted list of all connections. -// func (p *CSAPlanner) preprocess() { -// p.connections = make([]CSAConnection, 0) -// for tripID, trip := range p.Trips { -// sts := trip.Stops -// 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] -// to := sts[i+1] -// if from.DepartureTime < to.ArrivalTime { -// p.connections = append(p.connections, CSAConnection{ -// DepartureStopID: from.StopId, -// ArrivalStopID: to.StopId, -// DepartureTime: from.DepartureTime, -// ArrivalTime: to.ArrivalTime, -// TripID: tripID, -// }) -// } -// } -// } -// // Sort connections by departure time, which is crucial for the algorithm. -// sort.Slice(p.connections, func(i, j int) bool { -// return p.connections[i].DepartureTime < p.connections[j].DepartureTime -// }) -// } - -// // FindRoute finds the best route using the Connection Scan Algorithm. -// func (p *CSAPlanner) FindRoute(startStopID, endStopID string, when time.Time) *Route { -// earliestArrival := make(map[string]time.Time) -// journeyPointers := make(map[string]CSAConnection) // To reconstruct the path - -// startTime := types.AsSecondsAfterMidnight(when) -// day := when.Truncate(24 * time.Hour) - -// // Initialize earliest arrival times -// for stopID := range p.Stops { -// earliestArrival[stopID] = time.Time{} // Zero time represents infinity -// } -// earliestArrival[startStopID] = when - -// // Find the starting point in the connections array -// firstConnectionIdx := sort.Search(len(p.connections), func(i int) bool { -// return p.connections[i].DepartureTime >= startTime -// }) - -// // Scan through connections -// for i := firstConnectionIdx; i < len(p.connections); i++ { -// conn := p.connections[i] - -// depStopArrival, reachable := earliestArrival[conn.DepartureStopID] -// if !reachable || depStopArrival.IsZero() { -// continue // Cannot reach the departure stop of this connection yet -// } - -// connDepartureTime := day.Add(time.Duration(conn.DepartureTime) * time.Second) -// if connDepartureTime.Before(depStopArrival) { -// connDepartureTime = connDepartureTime.Add(24 * time.Hour) // Next day -// } - -// if !depStopArrival.IsZero() && connDepartureTime.After(depStopArrival) { -// // We can catch this connection -// connArrivalTime := day.Add(time.Duration(conn.ArrivalTime) * time.Second) -// if connArrivalTime.Before(connDepartureTime) { -// connArrivalTime = connArrivalTime.Add(24 * time.Hour) -// } - -// // Check if this connection offers a better arrival time at the destination stop -// currentBestArrival, hasArrival := earliestArrival[conn.ArrivalStopID] -// if !hasArrival || currentBestArrival.IsZero() || connArrivalTime.Before(currentBestArrival) { -// earliestArrival[conn.ArrivalStopID] = connArrivalTime -// journeyPointers[conn.ArrivalStopID] = conn -// } -// } -// } - -// // Reconstruct the path if the destination was reached -// if _, ok := journeyPointers[endStopID]; !ok { -// return nil // No path found -// } - -// return p.reconstructCSAPath(startStopID, endStopID, journeyPointers) -// } - -// // reconstructCSAPath builds the route from the journey pointers. -// func (p *CSAPlanner) reconstructCSAPath(startStopID, endStopID string, pointers map[string]CSAConnection) *Route { -// var path []CSAConnection -// currentStopID := endStopID -// for currentStopID != startStopID { -// conn, ok := pointers[currentStopID] -// if !ok { -// break // Should not happen if a path was found -// } -// path = append([]CSAConnection{conn}, path...) -// currentStopID = conn.DepartureStopID -// } - -// if len(path) == 0 { -// return nil -// } - -// // Group connections into legs -// var legs []Leg -// if len(path) > 0 { -// currentLeg := p.connectionToLeg(path[0]) -// for i := 1; i < len(path); i++ { -// if path[i].TripID == currentLeg.TripID { -// // Continue the current leg -// currentLeg.To = path[i].ArrivalStopID -// currentLeg.ToStop = p.GetStop(currentLeg.To) -// currentLeg.Stops = append(currentLeg.Stops, currentLeg.To) -// } else { -// // New leg -// legs = append(legs, *currentLeg) -// currentLeg = p.connectionToLeg(path[i]) -// } -// } -// legs = append(legs, *currentLeg) -// } - -// return &Route{Legs: legs} -// } - -// func (p *CSAPlanner) connectionToLeg(conn CSAConnection) *Leg { -// trip := p.GetTrip(conn.TripID) -// route := p.GetRoute(trip.RouteId) -// return &Leg{ -// TripID: conn.TripID, -// From: conn.DepartureStopID, -// To: conn.ArrivalStopID, -// FromStop: p.GetStop(conn.DepartureStopID), -// ToStop: p.GetStop(conn.ArrivalStopID), -// Trip: trip, -// Agency: p.GetAgency(route.AgencyID), -// Route: route, -// Stops: []string{conn.DepartureStopID, conn.ArrivalStopID}, -// } -// } - -// func (p *CSAPlanner) GetRoute(routeId string) *types.Route { -// if routeId == "" { -// return nil -// } -// route, ok := p.Routes[routeId] -// if !ok { -// return nil -// } -// return route -// } - -// func (p *CSAPlanner) GetAgency(agencyId string) *types.Agency { -// if agencyId == "" { -// return nil -// } -// agency, ok := p.Agencies[agencyId] -// if !ok { -// return nil -// } -// return agency -// } - -// func (p *CSAPlanner) GetTrip(tripId string) *types.Trip { -// if tripId == "" { -// return nil -// } -// trip, ok := p.Trips[tripId] -// if !ok { -// return nil -// } -// return trip -// } - -// func (p *CSAPlanner) GetStop(stopID string) *types.Stop { -// if stopID == "" { -// return nil -// } -// stop, ok := p.Stops[stopID] -// if !ok { -// return nil -// } -// return stop -// } diff --git a/cmd/planner/main.go b/cmd/planner/main.go index 1e830a7..8f6f2a5 100644 --- a/cmd/planner/main.go +++ b/cmd/planner/main.go @@ -26,8 +26,17 @@ type TripDetail struct { Stops []string `json:"stops"` } +type JSONTrip struct { + TripId string `json:"trip_id"` + RouteId string `json:"route_id"` + AgencyName string `json:"agency_name"` + TripHeadsign string `json:"trip_headsign"` + TripShortName string `json:"trip_short_name"` +} + type Leg struct { From *types.StopTime `json:"start"` + Trip *JSONTrip `json:"trip"` To *types.StopTime `json:"end"` } @@ -101,6 +110,64 @@ func main() { json.NewEncoder(w).Encode(stopList) }) + http.HandleFunc("/api/trips", func(w http.ResponseWriter, r *http.Request) { + from := r.URL.Query().Get("from") + whenStr := r.URL.Query().Get("when") + if from == "" { + w.WriteHeader(http.StatusBadRequest) + json.NewEncoder(w).Encode(map[string]string{"error": "from parameter required"}) + return + } + stop, ok := tp.Stops[from] + if !ok { + w.WriteHeader(http.StatusNotFound) + json.NewEncoder(w).Encode(map[string]string{"error": "stop not found"}) + return + } + when := time.Now() + if whenStr != "" { + var err error + when, err = time.Parse(time.RFC3339, whenStr) + if err != nil { + w.WriteHeader(http.StatusBadRequest) + json.NewEncoder(w).Encode(map[string]string{"error": "invalid when parameter"}) + return + } + } + trips := []map[string]interface{}{} + for trip := range stop.GetTripsAfter(when) { + // Find the index of the stop in the trip + startIdx := 0 + for i, st := range trip.Stops { + if st.StopId == from { + startIdx = i + break + } + } + tripData := map[string]interface{}{ + "trip_id": trip.TripId, + "headsign": trip.TripHeadsign, + "short_name": trip.TripShortName, + "route_id": trip.RouteID, + "agency_id": trip.AgencyID, + "agency_name": trip.Route.Agency.AgencyName, + "stops": []map[string]interface{}{}, + } + for _, st := range trip.Stops[startIdx:] { + tripData["stops"] = append(tripData["stops"].([]map[string]interface{}), map[string]interface{}{ + "stop_id": st.StopId, + "stop_name": st.Stop.StopName, + "location": []float64{st.Stop.StopLat, st.Stop.StopLon}, + "arrival_time": st.ArrivalTime, + "departure_time": st.DepartureTime, + }) + } + trips = append(trips, tripData) + } + w.Header().Set("Content-Type", "application/json") + json.NewEncoder(w).Encode(trips) + }) + http.HandleFunc("/api/route", func(w http.ResponseWriter, r *http.Request) { from := r.URL.Query().Get("from") @@ -125,7 +192,7 @@ func main() { return } log.Printf("using num %v", num) - w.WriteHeader(http.StatusOK) + log.Printf("start time %v", when) route, err := tp.FindRoute(from, to, when) if err != nil { @@ -133,6 +200,7 @@ func main() { json.NewEncoder(w).Encode(map[string]string{"error": "no route found"}) return } + w.WriteHeader(http.StatusOK) w.Header().Set("Content-Type", "application/json") json.NewEncoder(w).Encode(route) }) diff --git a/cmd/planner/main_test.go b/cmd/planner/main_test.go index 128bacf..05e2d8a 100644 --- a/cmd/planner/main_test.go +++ b/cmd/planner/main_test.go @@ -5,6 +5,7 @@ import ( "time" "git.tornberg.me/go-gtfs/pkg/reader" + "git.tornberg.me/go-gtfs/pkg/types" ) var tripData *reader.TripData @@ -23,35 +24,68 @@ func TestFindRouteToStockholm(t *testing.T) { //tp.Preprocess() - route, err := tp.FindRoute("740000030", "740000001", time.Now().Add(time.Hour*-16)) + routes, err := tp.FindRoute("740000030", "740000001", time.Now()) if err != nil { t.Fatalf("Error finding route: %v", err) } - if route == nil { + if len(routes) == 0 { t.Fatal("No route found from Falun Centralstation to Stockholm Centralstation") } - if len(route.Legs) < 1 { + if len(routes[0].Legs) < 1 { t.Fatal("Route has no legs") } } +func TestFindRouteWithRequiredChanges(t *testing.T) { + tp := NewTripPlanner(tripData) + //tp.Preprocess() + routes, err := tp.FindRoute("740000030", "740010947", time.Now().Add(time.Hour*-4)) + if err != nil { + t.Fatalf("Error finding route: %v", err) + } + if len(routes) == 0 { + t.Fatal("No route found from Falun Centralstation to Uppsala Centralstation") + } + if len(routes[0].Legs) < 2 { + t.Fatal("Route has less than 2 legs, expected at least one transfer") + } +} + func TestFindRouteToMalmo(t *testing.T) { tp := NewTripPlanner(tripData) //tp.Preprocess() - route, err := tp.FindRoute("740000030", "740000003", time.Now().Add(time.Hour*-16)) + routes, err := tp.FindRoute("740000030", "740000003", time.Now().Add(time.Hour*-8)) if err != nil { t.Fatalf("Error finding route: %v", err) } - if route == nil { + if len(routes) == 0 { t.Fatal("No route found from Falun Centralstation to Malmö Centralstation") } - if len(route.Legs) < 1 { + if len(routes[0].Legs) < 1 { t.Fatal("Route has no legs") } } + +func TestDepartuesAfter(t *testing.T) { + if hosjo, ok := tripData.Stops["740025287"]; ok { + trips := hosjo.GetTripsAfter(time.Now()) + p := 3 + for trip := range trips { + t.Logf("Trip %s (%s):", trip.TripShortName, trip.TripHeadsign) + for stop := range trip.GetDirectPossibleDestinations(hosjo, time.Now()) { + t.Logf("- Stop %s at %s", stop.Stop.StopName, types.AsTime(stop.DepartureTime)) + } + p-- + if p == 0 { + break + } + } + + } +} diff --git a/cmd/planner/planner.go b/cmd/planner/planner.go index af897c6..297d46d 100644 --- a/cmd/planner/planner.go +++ b/cmd/planner/planner.go @@ -4,7 +4,6 @@ import ( "fmt" "log" "slices" - "sort" "time" "git.tornberg.me/go-gtfs/pkg/reader" @@ -41,41 +40,41 @@ const ( func NewTripPlanner(data *reader.TripData) *TripPlanner { return &TripPlanner{ TripData: data, - graph: make(map[string][]Edge), + //graph: make(map[string][]Edge), } } // 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, types.AsTime(stop.DepartureTime)) - } - } + // 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, types.AsTime(stop.DepartureTime)) + // } + // } - } + // } - // Build graph with trip edges - for tripID, trip := range tp.Trips { - sts := trip.Stops - 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 := sts[i].DepartureTime - arrival := sts[i+1].DepartureTime - timeDiff := arrival - departure - if timeDiff > 0 { - tp.graph[from] = append(tp.graph[from], Edge{To: to, TripID: tripID, Time: timeDiff, DepartureTime: departure}) - } - } - } + // // Build graph with trip edges + // for tripID, trip := range tp.Trips { + // sts := trip.Stops + // 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 := sts[i].DepartureTime + // arrival := sts[i+1].DepartureTime + // 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 { @@ -100,8 +99,8 @@ type History struct { TravelTime types.SecondsAfterMidnight } -func NewHistory(st *types.StopTime, distanceToEnd float64, travelTime types.SecondsAfterMidnight) *History { - return &History{ +func NewHistory(st *types.StopTime, distanceToEnd float64, travelTime types.SecondsAfterMidnight) History { + return History{ StopTime: st, DistanceToEnd: distanceToEnd, TravelTime: travelTime, @@ -109,7 +108,7 @@ func NewHistory(st *types.StopTime, distanceToEnd float64, travelTime types.Seco } // FindRoutes finds the best routes (up to num) between two stops starting at the given time -func (tp *TripPlanner) FindRoute(from, to string, when time.Time) (*Route, error) { +func (tp *TripPlanner) FindRoute(from, to string, when time.Time) ([]*Route, error) { fromStop := tp.GetStop(from) toStop := tp.GetStop(to) @@ -117,28 +116,61 @@ func (tp *TripPlanner) FindRoute(from, to string, when time.Time) (*Route, error if fromStop == nil || toStop == nil { return nil, fmt.Errorf("invalid from or to stop") } + routes := make([]*Route, 0) + usedRouteIds := make(map[string]struct{}) + + for trip := range fromStop.GetTripsAfter(when) { + if _, used := usedRouteIds[trip.RouteId]; used { + continue + } + for i := len(trip.Stops) - 1; i >= 0; i-- { + stop := trip.Stops[i] + if stop.StopId == toStop.StopId { + usedRouteIds[trip.RouteId] = struct{}{} + routes = append(routes, &Route{ + Legs: []Leg{NewLeg(trip.Stops[0], trip.Stops[i])}, + }) + break + } else if stop.StopId == fromStop.StopId { + break + } else if stop.PickupType == 0 { + distance := stop.Stop.HaversineDistance(toStop) + + } + } + + } - possibleNextStops := make([]*types.StopTime, 0) - var startTime *types.StopTime for start, stop := range fromStop.GetStopsAfter(when) { if stop.StopId == toStop.StopId { - return &Route{ + routes = append(routes, &Route{ Legs: []Leg{NewLeg(start, stop)}, - }, nil + }) } else if from != stop.StopId { - startTime = start - possibleNextStops = append(possibleNextStops, stop) - } - } - slices.SortFunc(possibleNextStops, byDistanceTo(*toStop)) - for _, nextStop := range possibleNextStops { - route, err := tp.findRoute(*nextStop, toStop, *NewHistory(startTime, startTime.Stop.HaversineDistance(toStop), types.AsSecondsAfterMidnight(when)), *NewHistory(nextStop, nextStop.Stop.HaversineDistance(toStop), nextStop.ArrivalTime-startTime.ArrivalTime)) - if err == nil && route != nil { - return route, nil - } - } + // startTime = start + route, err := tp.findRoute(*start, toStop, NewHistory(start, start.Stop.HaversineDistance(toStop), 0), NewHistory(stop, stop.Stop.HaversineDistance(toStop), stop.ArrivalTime-start.DepartureTime)) - return nil, fmt.Errorf("no route found") + if err == nil && route != nil { + routes = append(routes, route) + } + } + } + // slices.SortFunc(possibleNextStops, byDistanceTo(*toStop)) + // for _, nextStop := range possibleNextStops { + // route, err := tp.findRoute(*nextStop, toStop, *NewHistory(startTime, startTime.Stop.HaversineDistance(toStop), types.AsSecondsAfterMidnight(when)), *NewHistory(nextStop, nextStop.Stop.HaversineDistance(toStop), nextStop.ArrivalTime-startTime.ArrivalTime)) + // if err == nil && route != nil { + // return route, nil + // } + // } + slices.SortFunc(routes, func(a, b *Route) int { + transfersA := len(a.Legs) - 1 + transfersB := len(b.Legs) - 1 + if transfersA != transfersB { + return transfersA - transfersB + } + return a.Duration() - b.Duration() - (transfersA-transfersB)*int(transferPenalty.Seconds()) + }) + return routes[:min(len(routes), 10)], nil } func byDistanceTo(end types.Stop) func(a, b *types.StopTime) int { @@ -149,10 +181,38 @@ func byDistanceTo(end types.Stop) func(a, b *types.StopTime) int { } } +func isInCorrectDirection(from, possible, end *types.Stop) bool { + if from.StopId == end.StopId || possible.StopId == end.StopId { + return true + } + if from.StopId == possible.StopId { + return false + } + startToEndLat := end.StopLat - from.StopLat + startToEndLon := end.StopLon - from.StopLon + startToPossibleLat := possible.StopLat - from.StopLat + startToPossibleLon := possible.StopLon - from.StopLon + dotProduct := startToEndLat*startToPossibleLat + startToEndLon*startToPossibleLon + return dotProduct > -0.4 && dotProduct < 0.4 +} + func shouldTryStop(end *types.Stop, visited ...History) func(possible *types.StopTime) bool { lastDistance := visited[len(visited)-1].Stop.HaversineDistance(end) return func(possible *types.StopTime) bool { + if end.StopId == possible.StopId { + return true + } + if possible.DepartureTime > visited[len(visited)-1].DepartureTime+types.SecondsAfterMidnight(maxWaitBetweenTrips.Seconds()) { + return false + } + if possible.DropOffType == 1 { + return false + } + // if !isInCorrectDirection(visited[len(visited)-1].Stop, possible.Stop, end) { + // return false + // } distance := possible.Stop.HaversineDistance(end) + for _, v := range visited { if v.DistanceToEnd <= distance*1.2 { return false @@ -187,7 +247,7 @@ func (tp *TripPlanner) findRoute(start types.StopTime, end *types.Stop, changes tries := 15 for _, nextStop := range possibleNextStops { - route, err := tp.findRoute(*nextStop, end, append(changes, *NewHistory(nextStop, nextStop.Stop.HaversineDistance(end), nextStop.ArrivalTime-start.ArrivalTime))...) + route, err := tp.findRoute(*nextStop, end, append(changes, NewHistory(nextStop, nextStop.Stop.HaversineDistance(end), nextStop.ArrivalTime-start.ArrivalTime))...) if err == nil && route != nil { return route, nil } @@ -213,9 +273,24 @@ func CreateLegs(stops []History, finalStop *types.StopTime) []Leg { } func NewLeg(fromStop, toStop *types.StopTime) Leg { + trip, ok := toStop.Stop.Trips[toStop.TripId] + if !ok { + log.Printf("trip %s not found for stop %s", toStop.TripId, toStop.Stop.StopName) + return Leg{ + From: fromStop, + To: toStop, + } + } return Leg{ From: fromStop, To: toStop, + Trip: &JSONTrip{ + TripId: trip.TripId, + RouteId: trip.RouteID, + AgencyName: trip.Agency.AgencyName, + TripHeadsign: trip.TripHeadsign, + TripShortName: trip.TripShortName, + }, } } diff --git a/frontend/package-lock.json b/frontend/package-lock.json index 49d9e2c..b86d2b4 100644 --- a/frontend/package-lock.json +++ b/frontend/package-lock.json @@ -8,8 +8,10 @@ "name": "frontend", "version": "0.0.0", "dependencies": { + "leaflet": "^1.9.4", "react": "^19.2.0", "react-dom": "^19.2.0", + "react-leaflet": "^5.0.0", "swr": "^2.3.6" }, "devDependencies": { @@ -1007,6 +1009,17 @@ "@jridgewell/sourcemap-codec": "^1.4.14" } }, + "node_modules/@react-leaflet/core": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/@react-leaflet/core/-/core-3.0.0.tgz", + "integrity": "sha512-3EWmekh4Nz+pGcr+xjf0KNyYfC3U2JjnkWsh0zcqaexYqmmB5ZhH37kz41JXGmKzpaMZCnPofBBm64i+YrEvGQ==", + "license": "Hippocratic-2.1", + "peerDependencies": { + "leaflet": "^1.9.0", + "react": "^19.0.0", + "react-dom": "^19.0.0" + } + }, "node_modules/@rolldown/pluginutils": { "version": "1.0.0-beta.47", "resolved": "https://registry.npmjs.org/@rolldown/pluginutils/-/pluginutils-1.0.0-beta.47.tgz", @@ -2253,6 +2266,12 @@ "json-buffer": "3.0.1" } }, + "node_modules/leaflet": { + "version": "1.9.4", + "resolved": "https://registry.npmjs.org/leaflet/-/leaflet-1.9.4.tgz", + "integrity": "sha512-nxS1ynzJOmOlHp+iL3FyWqK89GtNL8U8rvlMOsQdTTssxZwCXh8N2NB3GDQOL+YR3XnWyZAxwQixURb+FA74PA==", + "license": "BSD-2-Clause" + }, "node_modules/levn": { "version": "0.4.1", "resolved": "https://registry.npmjs.org/levn/-/levn-0.4.1.tgz", @@ -2526,6 +2545,20 @@ "react": "^19.2.0" } }, + "node_modules/react-leaflet": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/react-leaflet/-/react-leaflet-5.0.0.tgz", + "integrity": "sha512-CWbTpr5vcHw5bt9i4zSlPEVQdTVcML390TjeDG0cK59z1ylexpqC6M1PJFjV8jD7CF+ACBFsLIDs6DRMoLEofw==", + "license": "Hippocratic-2.1", + "dependencies": { + "@react-leaflet/core": "^3.0.0" + }, + "peerDependencies": { + "leaflet": "^1.9.0", + "react": "^19.0.0", + "react-dom": "^19.0.0" + } + }, "node_modules/react-refresh": { "version": "0.18.0", "resolved": "https://registry.npmjs.org/react-refresh/-/react-refresh-0.18.0.tgz", diff --git a/frontend/package.json b/frontend/package.json index 0db66fc..64776a7 100644 --- a/frontend/package.json +++ b/frontend/package.json @@ -10,8 +10,10 @@ "preview": "vite preview" }, "dependencies": { + "leaflet": "^1.9.4", "react": "^19.2.0", "react-dom": "^19.2.0", + "react-leaflet": "^5.0.0", "swr": "^2.3.6" }, "devDependencies": { diff --git a/frontend/src/App.css b/frontend/src/App.css index 2b2c389..3cce2dd 100644 --- a/frontend/src/App.css +++ b/frontend/src/App.css @@ -4,6 +4,8 @@ margin: 0 auto; padding: 2rem; font-family: Arial, sans-serif; + background-color: white; + color: black; } /* Form Layout */ @@ -119,7 +121,7 @@ button:disabled { padding: 1rem; border: 1px solid #ddd; border-radius: 8px; - background-color: #f9f9f9; + background-color: white; } .leg h3 { @@ -180,7 +182,7 @@ button:disabled { /* Stops List */ .stops-list { - background-color: #ecf0f1; + background-color: white; border-radius: 4px; padding: 0.5rem; margin: 0.5rem 0; @@ -198,6 +200,15 @@ button:disabled { border-bottom: none; } +.stop-item.clickable { + cursor: pointer; +} + +.stop-item.past { + color: #999; + cursor: not-allowed; +} + /* Time Input */ input[type="datetime-local"] { padding: 0.5rem; @@ -216,20 +227,42 @@ select { box-sizing: border-box; } -/* Route Selector */ -.route-selector { +/* Mode Switch */ +.mode-switch { display: flex; gap: 0.5rem; - margin-bottom: 1rem; + margin-bottom: 2rem; justify-content: center; } -.route-selector button { - padding: 0.5rem 1rem; - font-size: 0.9rem; -} - -.route-selector button.active { +.mode-switch button.active { background-color: #535bf2; font-weight: bold; } + +/* Explorer */ +.explorer { + display: flex; + flex-direction: column; + gap: 1rem; + align-items: center; +} + +.trips { + width: 100%; + max-width: 800px; +} + +.trip { + margin-bottom: 2rem; + padding: 1rem; + border: 1px solid #ddd; + border-radius: 8px; + background-color: white; +} + +.trip h3 { + margin-top: 0; + color: #333; + font-size: 1.1rem; +} diff --git a/frontend/src/App.jsx b/frontend/src/App.jsx index c2c4573..d79c84b 100644 --- a/frontend/src/App.jsx +++ b/frontend/src/App.jsx @@ -1,6 +1,8 @@ -import { useState, useMemo } from "react"; +import { useState, useMemo, useEffect } from "react"; import useSWR from "swr"; import "./App.css"; +import { MapContainer, TileLayer, Marker, Popup, Polyline, CircleMarker, useMap } from 'react-leaflet'; +import 'leaflet/dist/leaflet.css'; function StopSelector({ stops, onChange, placeholder, inputId }) { const [inputValue, setInputValue] = useState(""); @@ -49,12 +51,66 @@ function StopSelector({ stops, onChange, placeholder, inputId }) { const fetcher = (url) => fetch(url).then(res => res.ok ? res.json() : Promise.reject(res.statusText)); -const JsonView = ({ data }) => { ( -
{JSON.stringify(data, null, 2)}
-); }; +const JsonView = ({ data }) => { + return ( +
{JSON.stringify(data, null, 2)}
+ ); +}; + +const MapComponent = ({ stops }) => { + if (!stops || stops.length === 0) return null; + const center = [stops[0].stop_lat, stops[0].stop_lon]; + const positions = stops.map(stop => [stop.stop_lat, stop.stop_lon]); + return ( + + + + {stops.map((stop, index) => ( + + {stop.stop_name} + + ))} + + + ); +}; + +const FitBounds = ({ stops }) => { + const map = useMap(); + useEffect(() => { + if (stops && stops.length > 1) { + const bounds = stops.reduce((acc, stop) => { + return [ + [Math.min(acc[0][0], stop.stop_lat), Math.min(acc[0][1], stop.stop_lon)], + [Math.max(acc[1][0], stop.stop_lat), Math.max(acc[1][1], stop.stop_lon)] + ]; + }, [[stops[0].stop_lat, stops[0].stop_lon], [stops[0].stop_lat, stops[0].stop_lon]]); + map.fitBounds(bounds, { padding: [20, 20] }); + } + }, [map, stops]); + return null; +}; + +const asTimeString = (secondsAfterMidnight) => { + + const hours = Math.floor(secondsAfterMidnight / 3600) % 24; + const minutes = Math.floor((secondsAfterMidnight % 3600) / 60); + return `${hours.toString().padStart(2, '0')}:${minutes.toString().padStart(2, '0')}`; +} + +const secondsToDateTime = (secondsAfterMidnight) => { + const now = new Date(); + const date = new Date(now.getFullYear(), now.getMonth(), now.getDate()); + date.setSeconds(secondsAfterMidnight); + return date.toISOString(); +} function App() { const { data: stops = [], error: stopsError } = useSWR('/api/stops', fetcher); + const [mode, setMode] = useState("plan"); const [from, setFrom] = useState({ id: "740000030", name: "Falun Centralstation", @@ -65,142 +121,220 @@ function App() { }); const [time, setTime] = useState(new Date().toISOString()); const [num, setNum] = useState(3); - const [routeKey, setRouteKey] = useState(null); - const { data: routes = [], error: routesError, isLoading } = useSWR(routeKey, fetcher); + + const { data: routes = [], error: routesError, isLoading } = useSWR(from.id && to.id ? `/api/route?from=${from.id}&to=${to.id}&when=${encodeURIComponent(time)}&num=${num}` : null, (url => fetcher(url).then(data => data.map((item, idx) => ({ ...item, key: idx })))), { revalidateOnFocus: false }); const [selectedRouteIndex, setSelectedRouteIndex] = useState(0); const [error, setError] = useState(""); + const routeStops = useMemo(() => { + if (!routes[selectedRouteIndex]?.legs) return []; + const stopIds = new Set(); + routes[selectedRouteIndex].legs.forEach(leg => { + leg.trip?.stops?.forEach(stopId => stopIds.add(stopId)); + }); + return Array.from(stopIds).map(id => stops.find(s => s.stop_id === id)).filter(Boolean); + }, [routes, selectedRouteIndex, stops]); + + const [exploreStop, setExploreStop] = useState({ + id: "740000030", + name: "Falun Centralstation", + }); + const [exploreTime, setExploreTime] = useState(new Date().toISOString()); + const { data: trips = [], error: tripsError, isLoading: tripsLoading } = useSWR(mode === "explore" && exploreStop.id ? `/api/trips?from=${exploreStop.id}&when=${encodeURIComponent(exploreTime)}` : null, fetcher, { revalidateOnFocus: false }); + const findRoute = () => { if (!from.id || !to.id) { setError("Please select both from and to stops"); return; } setError(""); - setRouteKey(`/api/route?from=${from.id}&to=${to.id}&when=${encodeURIComponent(time)}&num=${num}`); + // setRouteKey(`/api/route?from=${from.id}&to=${to.id}&when=${encodeURIComponent(time)}&num=${num}`); setSelectedRouteIndex(0); }; return (

GTFS Route Planner

-
-
- - - - - -
- - {(error || stopsError || routesError) &&

{error || (stopsError ? "Failed to load stops: " + stopsError.message : "Failed to find route: " + routesError)}

} - {routes.length > 0 && ( -
-

- Routes from {from.name} to {to.name} -

- {/* {routes.length > 1 && ( -
- {routes.map((_, index) => ( - - ))} -
- )} */} - - {/* {routes[selectedRouteIndex]?.legs && - routes[selectedRouteIndex].legs.length > 0 && ( -
- {routes[selectedRouteIndex].legs.map((leg, index) => { - const trip = leg.trip; - return ( -
-

- Leg {index + 1}:{" "} - {trip?.trip_short_name ?? trip.route_id} by{" "} - {leg.agency?.agency_name} -

-

- From: {leg.from?.stop_name} To: {leg.to?.stop_name} -

-

- Departure:{" "} - {new Date(leg.departure_time).toLocaleString()} -

-

- Arrival: {new Date(leg.arrival_time).toLocaleString()} -

-
    - {leg.stops?.map((stopId) => { - const stop = stops.find( - (s) => s.stop_id === stopId, - ); - return ( -
  • - {stop?.stop_name || stopId} -
  • - ); - })} -
- {/* {index < routes[selectedRouteIndex].legs.length - 1 && ( -

- Transfer at{" "} - {stops.find((s) => s.stop_id === leg.to)?.stop_name || - leg.to} -

- )} } -
- ); - })} -
- )}*/} -
- )} +
+ +
+ {mode === "plan" && ( +
+
+ + + + + +
+ + {(error || stopsError || routesError) &&

{error || (stopsError ? "Failed to load stops: " + stopsError.message : "Failed to find route: " + routesError)}

} + {routes.length > 0 && ( +
+

+ Routes from {from.name} to {to.name} +

+ {routes.length > 1 && ( +
+ {routes.map((_, index) => ( + + ))} +
+ )} + + {routes[selectedRouteIndex]?.legs && + routes[selectedRouteIndex].legs.length > 0 && ( +
+ {routes[selectedRouteIndex].legs.map((leg, index) => { + + const trip = leg.trip; + console.log(leg) + return ( +
+

+ Leg {index + 1}:{" "} + {trip?.trip_short_name ?? trip.route_id} by{" "} + {trip.agency_name} +

+

+ From: {leg.start?.stop.stop_name} To: {leg.end?.stop?.stop_name} +

+

+ Departure:{" "} + {asTimeString(leg.start.departure_time)} +

+

+ Arrival: {asTimeString(leg.end.arrival_time)} +

+
    + {trip.stops?.map((stopId, idx) => { + const stop = stops.find( + (s) => s.stop_id === stopId, + ); + const isStart = index === 0 && idx === 0; + const isEnd = index === routes[selectedRouteIndex].legs.length - 1 && idx === trip.stops.length - 1; + const isCurrent = isStart || isEnd; + return ( +
  • + {stop?.stop_name || stopId} +
  • + ); + })} +
+ {index < routes[selectedRouteIndex].legs.length - 1 && ( +

+ Transfer at{" "} + {stops.find((s) => s.stop_id === leg.to)?.stop_name || + leg.to} +

+ )} +
+ ); + })} + +
+ )} +
+ )} +
+ )} + {mode === "explore" && ( +
+
+ + +
+ {(tripsError || stopsError) &&

{tripsError ? "Failed to load trips: " + tripsError : "Failed to load stops: " + stopsError}

} + {tripsLoading &&

Loading trips...

} + {trips.length > 0 && ( +
+

Trips from {exploreStop.name}

+ {trips.slice(0, 10).map((trip, index) => { + const tripStops = trip.stops.map(s => stops.find(st => st.stop_id === s.stop_id)).filter(Boolean); + return ( +
+

{trip.headsign || trip.short_name} ({trip.route_id}) by {trip.agency_name}

+
    + {trip.stops.map((stop, idx) => ( +
  • { + setExploreStop({ id: stop.stop_id, name: stop.stop_name }); + setExploreTime(secondsToDateTime(stop.departure_time)); + }}> + {asTimeString(stop.arrival_time)} - {stop.stop_name} +
  • + ))} +
+ +
+ ); + })} +
+ )} +
+ )}
); } diff --git a/pkg/reader/loader.go b/pkg/reader/loader.go index 0ccd749..0cf3fa8 100644 --- a/pkg/reader/loader.go +++ b/pkg/reader/loader.go @@ -41,8 +41,8 @@ func LoadTripData(path string) (*TripData, error) { err = 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) + r.SetAgency(ag) + // ag.AddRoute(&r) } }) case "stops": @@ -88,7 +88,7 @@ func LoadTripData(path string) (*TripData, error) { //transfers = append(transfers, tr) stop, ok := tp.Stops[tr.FromStopId] if ok { - stop.AddTransfer(&tr) + stop.AddTransfer(&tr, tp.Stops[tr.ToStopId]) } else { log.Printf("stop %s not found for transfer", tr.FromStopId) } diff --git a/pkg/types/agency.go b/pkg/types/agency.go index 3ba64f3..994b386 100644 --- a/pkg/types/agency.go +++ b/pkg/types/agency.go @@ -6,12 +6,12 @@ type Agency struct { AgencyURL string `json:"agency_url" csv:"agency_url"` AgencyTimezone string `json:"agency_timezone" csv:"agency_timezone"` AgencyLang string `json:"agency_lang" csv:"agency_lang"` - Routes map[string]*Route + //Routes map[string]*Route } -func (a *Agency) AddRoute(route *Route) { - if a.Routes == nil { - a.Routes = make(map[string]*Route) - } - a.Routes[route.RouteID] = route -} +// func (a *Agency) AddRoute(route *Route) { +// if a.Routes == nil { +// a.Routes = make(map[string]*Route) +// } +// a.Routes[route.RouteID] = route +// } diff --git a/pkg/types/route.go b/pkg/types/route.go index 3183e53..e1af08f 100644 --- a/pkg/types/route.go +++ b/pkg/types/route.go @@ -1,10 +1,10 @@ package types type Route struct { - Agency *Agency `json:"agency" csv:"agency"` + Agency *Agency `json:"agency" csv:"-"` Trips []*Trip `json:"trips" csv:"trips"` - RouteID string `json:"route_id" csv:"route_id"` - AgencyID string `json:"agency_id" csv:"agency_id"` + RouteId string `json:"route_id" csv:"route_id"` + AgencyId string `json:"agency_id" csv:"agency_id"` RouteShortName string `json:"route_short_name" csv:"route_short_name"` RouteLongName string `json:"route_long_name" csv:"route_long_name"` RouteType int `json:"route_type" csv:"route_type"` diff --git a/pkg/types/stop.go b/pkg/types/stop.go index bcfd5d7..acef80e 100644 --- a/pkg/types/stop.go +++ b/pkg/types/stop.go @@ -16,7 +16,8 @@ type Stop struct { Transfers []*Transfer `json:"-" csv:"transfers"` } -func (s *Stop) AddTransfer(transfer *Transfer) { +func (s *Stop) AddTransfer(transfer *Transfer, toStop *Stop) { + transfer.ToStop = toStop if s.Transfers == nil { s.Transfers = make([]*Transfer, 0) } @@ -24,7 +25,6 @@ func (s *Stop) AddTransfer(transfer *Transfer) { } func (s *Stop) AddTrip(trip *Trip) { - s.Trips[trip.TripId] = trip } @@ -39,7 +39,7 @@ func (s *Stop) GetTripsAfter(when time.Time) iter.Seq[*TripWithDepartureTime] { for _, trip := range s.Trips { for _, stop := range trip.Stops { - if stop.StopId == s.StopId && stop.ArrivalTime >= startAfterMidnight { + if stop.StopId == s.StopId && stop.ArrivalTime >= startAfterMidnight && stop.PickupType == 0 { if !yield(&TripWithDepartureTime{Trip: trip, DepartureTime: stop.DepartureTime}) { return @@ -86,21 +86,44 @@ func (s *Stop) GetUpcomingStops(start *StopTime) iter.Seq[*StopTime] { func (s *Stop) GetStopsAfter(when time.Time) iter.Seq2[*StopTime, *StopTime] { startAfterMidnight := AsSecondsAfterMidnight(when) + var first *StopTime return func(yield func(start, stop *StopTime) bool) { for _, trip := range s.Trips { - found := false + found := -1 var start *StopTime for _, stop := range trip.Stops { if stop.StopId == s.StopId && stop.ArrivalTime >= startAfterMidnight { - found = true + found = stop.StopSequence start = stop + if first == nil || start.ArrivalTime < first.ArrivalTime { + first = start + } } - if found { + if found != -1 && stop.StopSequence > found && stop.PickupType == 0 { if !yield(start, stop) { return } } } + + } + if first == nil { + return + } + for _, transfer := range s.Transfers { + if transfer.FromStopId == s.StopId { + + if !yield(first, &StopTime{ + Stop: transfer.ToStop, + TripId: "transfer", + ArrivalTime: startAfterMidnight + SecondsAfterMidnight(transfer.MinTransferTime), + DepartureTime: startAfterMidnight + SecondsAfterMidnight(transfer.MinTransferTime), + StopId: transfer.ToStopId, + }) { + return + } + + } } } diff --git a/pkg/types/transfer.go b/pkg/types/transfer.go index 6f600b2..d60ca74 100644 --- a/pkg/types/transfer.go +++ b/pkg/types/transfer.go @@ -1,6 +1,7 @@ package types type Transfer struct { + ToStop *Stop `json:"-" csv:"-"` FromStopId string `json:"from_stop_id" csv:"from_stop_id"` ToStopId string `json:"to_stop_id" csv:"to_stop_id"` TransferType int `json:"transfer_type" csv:"transfer_type"` diff --git a/pkg/types/trip.go b/pkg/types/trip.go index 6648174..1d4b6e7 100644 --- a/pkg/types/trip.go +++ b/pkg/types/trip.go @@ -42,7 +42,28 @@ func (t *Trip) AddStopTime(stopTime *StopTime) { if t.Stops == nil { t.Stops = make([]*StopTime, 0) } - t.Stops = append(t.Stops, stopTime) + // Find the index to insert based on StopSequence + idx := 0 + for i, st := range t.Stops { + if stopTime.StopSequence < st.StopSequence { + idx = i + break + } + idx = i + 1 + } + // Insert at the correct position + t.Stops = append(t.Stops[:idx], append([]*StopTime{stopTime}, t.Stops[idx:]...)...) + // Adjust times if necessary for next-day continuation + for i := idx; i < len(t.Stops)-1; i++ { + curr := t.Stops[i] + next := t.Stops[i+1] + if next.ArrivalTime < curr.ArrivalTime { + next.ArrivalTime += 86400 + } + if next.DepartureTime < curr.DepartureTime { + next.DepartureTime += 86400 + } + } } func (t *Trip) Has(stop *Stop) (*StopTime, bool) {