This commit is contained in:
2025-11-15 17:53:50 +01:00
parent 1bebded484
commit 5dc296805a
15 changed files with 634 additions and 688 deletions

View File

@@ -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}
// }

View File

@@ -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
// }

View File

@@ -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)
})

View File

@@ -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
}
}
}
}

View File

@@ -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,10 +273,25 @@ 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,
},
}
}
// // findRoute implements a time-aware Dijkstra algorithm for routing

View File

@@ -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",

View File

@@ -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": {

View File

@@ -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;
}

View File

@@ -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 }) => { (
const JsonView = ({ data }) => {
return (
<pre className="json-view">{JSON.stringify(data, null, 2)}</pre>
); };
);
};
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 (
<MapContainer center={center} zoom={10} style={{ height: '400px', width: '100%' }}>
<FitBounds stops={stops} />
<TileLayer
url="https://{s}.tile.openstreetmap.org/{z}/{x}/{y}.png"
attribution='&copy; <a href="https://www.openstreetmap.org/copyright">OpenStreetMap</a> contributors'
/>
{stops.map((stop, index) => (
<CircleMarker key={stop.stop_id} center={[stop.stop_lat, stop.stop_lon]} radius={5} color="blue" fillColor="blue" fillOpacity={0.5}>
<Popup>{stop.stop_name}</Popup>
</CircleMarker>
))}
<Polyline positions={positions} color="blue" />
</MapContainer>
);
};
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,24 +121,45 @@ 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 (
<div className="App">
<h1>GTFS Route Planner</h1>
<div className="mode-switch">
<button onClick={() => setMode("plan")} className={mode === "plan" ? "active" : ""} type="button">Plan Route</button>
<button onClick={() => setMode("explore")} className={mode === "explore" ? "active" : ""} type="button">Explore Trips</button>
</div>
{mode === "plan" && (
<div className="planner">
<div className="selects">
<label htmlFor="from-stop">
@@ -137,7 +214,7 @@ function App() {
<h2 className="route-title">
Routes from {from.name} to {to.name}
</h2>
{/* {routes.length > 1 && (
{routes.length > 1 && (
<div className="route-selector">
{routes.map((_, index) => (
<button
@@ -150,57 +227,114 @@ function App() {
</button>
))}
</div>
)} */}
<JsonView data={routes} />
{/* {routes[selectedRouteIndex]?.legs &&
)}
{routes[selectedRouteIndex]?.legs &&
routes[selectedRouteIndex].legs.length > 0 && (
<div className="route">
{routes[selectedRouteIndex].legs.map((leg, index) => {
const trip = leg.trip;
console.log(leg)
return (
<div key={leg.trip?.trip_id || index} className="leg">
<h3 className="leg-title">
Leg {index + 1}:{" "}
{trip?.trip_short_name ?? trip.route_id} by{" "}
{leg.agency?.agency_name}
{trip.agency_name}
</h3>
<p className="leg-stops">
From: {leg.from?.stop_name} To: {leg.to?.stop_name}
From: {leg.start?.stop.stop_name} To: {leg.end?.stop?.stop_name}
</p>
<p className="leg-time">
Departure:{" "}
{new Date(leg.departure_time).toLocaleString()}
{asTimeString(leg.start.departure_time)}
</p>
<p className="leg-time">
Arrival: {new Date(leg.arrival_time).toLocaleString()}
Arrival: {asTimeString(leg.end.arrival_time)}
</p>
<ul className="stops-list">
{leg.stops?.map((stopId) => {
{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 (
<li key={stopId} className="stop-item">
<li key={stopId} className={`stop-item ${isCurrent ? 'current' : ''}`}>
{stop?.stop_name || stopId}
</li>
);
})}
</ul>
{/* {index < routes[selectedRouteIndex].legs.length - 1 && (
{index < routes[selectedRouteIndex].legs.length - 1 && (
<p className="transfer">
Transfer at{" "}
{stops.find((s) => s.stop_id === leg.to)?.stop_name ||
leg.to}
</p>
)} }
)}
</div>
);
})}
<MapComponent stops={routeStops} />
</div>
)}
</div>
)}
</div>
)}
{mode === "explore" && (
<div className="explorer">
<div className="selects">
<label htmlFor="explore-stop">
Start Stop: ({exploreStop.id})
<StopSelector
stops={stops}
onChange={setExploreStop}
placeholder="Search for start stop"
inputId="explore-stop"
/>
</label>
<label htmlFor="explore-time">
Time:
<input
id="explore-time"
type="datetime-local"
value={exploreTime}
onChange={(e) => setExploreTime(e.target.value)}
/>
</label>
</div>
{(tripsError || stopsError) && <p className="error">{tripsError ? "Failed to load trips: " + tripsError : "Failed to load stops: " + stopsError}</p>}
{tripsLoading && <p>Loading trips...</p>}
{trips.length > 0 && (
<div className="trips">
<h2>Trips from {exploreStop.name}</h2>
{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 (
<div key={trip.trip_id} className="trip">
<h3>{trip.headsign || trip.short_name} ({trip.route_id}) by {trip.agency_name}</h3>
<ul className="stops-list">
{trip.stops.map((stop, idx) => (
<li key={idx} className={`stop-item clickable ${idx === 0 ? 'current' : ''}`} onClick={() => {
setExploreStop({ id: stop.stop_id, name: stop.stop_name });
setExploreTime(secondsToDateTime(stop.departure_time));
}}>
{asTimeString(stop.arrival_time)} - {stop.stop_name}
</li>
))}
</ul>
<MapComponent stops={tripStops} />
</div>
);
})}
</div>
)}*/}
</div>
)}
</div>
)}
</div>
);
}

View File

@@ -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)
}

View File

@@ -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
// }

View File

@@ -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"`

View File

@@ -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
}
}
}
}

View File

@@ -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"`

View File

@@ -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) {