diff --git a/.vscode/launch.json b/.vscode/launch.json new file mode 100644 index 0000000..735a974 --- /dev/null +++ b/.vscode/launch.json @@ -0,0 +1,16 @@ +{ + // Use IntelliSense to learn about possible attributes. + // Hover to view descriptions of existing attributes. + // For more information, visit: https://go.microsoft.com/fwlink/?linkid=830387 + "version": "0.2.0", + "configurations": [ + { + "name": "Launch Package", + "type": "go", + "request": "launch", + "mode": "auto", + "program": "${workspaceFolder}/cmd/planner", + "cwd": "${workspaceFolder}", + }, + ] +} \ No newline at end of file diff --git a/cmd/planner/astar.go b/cmd/planner/astar.go new file mode 100644 index 0000000..770c4c1 --- /dev/null +++ b/cmd/planner/astar.go @@ -0,0 +1,261 @@ +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 new file mode 100644 index 0000000..8b5fac6 --- /dev/null +++ b/cmd/planner/csa.go @@ -0,0 +1,217 @@ +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 e604bbf..1e830a7 100644 --- a/cmd/planner/main.go +++ b/cmd/planner/main.go @@ -1,497 +1,23 @@ 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 + Time types.SecondsAfterMidnight + DepartureTime types.SecondsAfterMidnight } type TripDetail struct { @@ -501,42 +27,33 @@ type TripDetail struct { } 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"` + From *types.StopTime `json:"start"` + To *types.StopTime `json:"end"` } 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 { +func (r *Route) EndTime() types.SecondsAfterMidnight { if len(r.Legs) == 0 { return 0 } - return r.Legs[len(r.Legs)-1].ArrivalTime.Sub(r.Legs[0].DepartureTime) + return r.Legs[len(r.Legs)-1].To.ArrivalTime +} + +func (r *Route) StartTime() types.SecondsAfterMidnight { + if len(r.Legs) == 0 { + return 0 + } + return r.Legs[0].From.DepartureTime +} + +func (r *Route) Duration() int { + if len(r.Legs) == 0 { + return 0 + } + return int(r.Legs[len(r.Legs)-1].To.ArrivalTime - r.Legs[0].From.DepartureTime) } type PathInfo struct { @@ -549,36 +66,43 @@ type PathInfo struct { } 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) + tripData, err := reader.LoadTripData("data") + if err != nil { + log.Fatalf("unable to load data %v", err) } - wg.Wait() + tp := NewTripPlanner(tripData) + if err := tp.Preprocess(); err != nil { fmt.Printf("Failed to preprocess data: %v\n", err) os.Exit(1) } + 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.ArrivalTime)) + } + } + + } + 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 { + for _, s := range tp.Stops { + if len(s.Trips) > 0 { stopList = append(stopList, *s) } } + w.WriteHeader(http.StatusOK) 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") @@ -591,7 +115,7 @@ func main() { } when := time.Now() if whenStr != "" { - if parsed, err := time.Parse(time.RFC3339, whenStr); err == nil { + if parsed, err := time.Parse(time.DateTime, whenStr); err == nil { when = parsed } } @@ -600,14 +124,17 @@ func main() { 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 { + 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 { 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) + json.NewEncoder(w).Encode(route) }) log.Printf("Listening on 8080") http.ListenAndServe(":8080", nil) diff --git a/cmd/planner/main_test.go b/cmd/planner/main_test.go index dba651b..128bacf 100644 --- a/cmd/planner/main_test.go +++ b/cmd/planner/main_test.go @@ -1,26 +1,32 @@ package main import ( - "os" "testing" "time" + + "git.tornberg.me/go-gtfs/pkg/reader" ) -func TestFindRoute(t *testing.T) { - os.Chdir("../..") - tp := NewTripPlanner() +var tripData *reader.TripData - err := tp.LoadData("data") +func init() { + var err error + tripData, err = reader.LoadTripData("../../data") if err != nil { - t.Fatalf("Failed to load data: %v", err) + panic("Failed to load trip data: " + err.Error()) } +} - err = tp.Preprocess() +func TestFindRouteToStockholm(t *testing.T) { + + tp := NewTripPlanner(tripData) + + //tp.Preprocess() + + route, err := tp.FindRoute("740000030", "740000001", time.Now().Add(time.Hour*-16)) if err != nil { - t.Fatalf("Failed to preprocess: %v", err) + t.Fatalf("Error finding route: %v", err) } - - route := tp.FindRoute("740000030", "740000001", time.Now()) if route == nil { t.Fatal("No route found from Falun Centralstation to Stockholm Centralstation") } @@ -29,50 +35,17 @@ func TestFindRoute(t *testing.T) { t.Fatal("Route has no legs") } - stops := []string{} - if len(route.Legs) > 0 { - stops = append(stops, route.Legs[0].From) - for _, leg := range route.Legs { - stops = append(stops, leg.To) - } - } - - if len(stops) < 2 { - t.Fatal("Route path is too short") - } - - if stops[0] != "740000030" { - t.Errorf("Route does not start at Falun Centralstation (740000030), starts at %s", stops[0]) - } - - if stops[len(stops)-1] != "740000001" { - t.Errorf("Route does not end at Stockholm Centralstation (740000001), ends at %s", stops[len(stops)-1]) - } - - // Additional check: ensure all stops in path exist - for _, stopID := range stops { - if s, exists := tp.stops[stopID]; !exists { - t.Errorf("Stop %s in path does not exist", stopID) - } else { - t.Logf("stop: %s", s.StopName) - } - } } func TestFindRouteToMalmo(t *testing.T) { - tp := NewTripPlanner() + tp := NewTripPlanner(tripData) - err := tp.LoadData("data") + //tp.Preprocess() + + route, err := tp.FindRoute("740000030", "740000003", time.Now().Add(time.Hour*-16)) if err != nil { - t.Fatalf("Failed to load data: %v", err) + t.Fatalf("Error finding route: %v", err) } - - err = tp.Preprocess() - if err != nil { - t.Fatalf("Failed to preprocess: %v", err) - } - - route := tp.FindRoute("740000030", "740000003", time.Now()) if route == nil { t.Fatal("No route found from Falun Centralstation to Malmö Centralstation") } @@ -81,30 +54,4 @@ func TestFindRouteToMalmo(t *testing.T) { t.Fatal("Route has no legs") } - stops := []string{} - if len(route.Legs) > 0 { - stops = append(stops, route.Legs[0].From) - for _, leg := range route.Legs { - stops = append(stops, leg.To) - } - } - - if len(stops) < 2 { - t.Fatal("Route path is too short") - } - - if stops[0] != "740000030" { - t.Errorf("Route does not start at Falun Centralstation (740000030), starts at %s", stops[0]) - } - - if stops[len(stops)-1] != "740000003" { - t.Errorf("Route does not end at Malmö Centralstation (740000003), ends at %s", stops[len(stops)-1]) - } - - // Additional check: ensure all stops in path exist - for _, stopID := range stops { - if _, exists := tp.stops[stopID]; !exists { - t.Errorf("Stop %s in path does not exist", stopID) - } - } } diff --git a/cmd/planner/planner.go b/cmd/planner/planner.go new file mode 100644 index 0000000..d9a3efa --- /dev/null +++ b/cmd/planner/planner.go @@ -0,0 +1,236 @@ +package main + +import ( + "fmt" + "log" + "slices" + "sort" + "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 { + *reader.TripData + graph map[string][]Edge +} + +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(data *reader.TripData) *TripPlanner { + return &TripPlanner{ + TripData: data, + 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)) + } + } + + } + + // 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 { + // 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 +} + +// 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) { + + fromStop := tp.GetStop(from) + toStop := tp.GetStop(to) + + if fromStop == nil || toStop == nil { + return nil, fmt.Errorf("invalid from or to stop") + } + + possibleNextStops := make([]*types.StopTime, 0) + + for start, stop := range fromStop.GetStopsAfter(when) { + if stop.StopId == toStop.StopId { + return &Route{ + Legs: []Leg{NewLeg(start, stop)}, + }, nil + } else { + possibleNextStops = append(possibleNextStops, start) + } + } + slices.SortFunc(possibleNextStops, byArrivalTime(*toStop)) + for _, nextStop := range possibleNextStops { + route, err := tp.findRoute(*nextStop, toStop, nextStop) + if err == nil && route != nil { + return route, nil + } + } + + return nil, fmt.Errorf("no route found") +} + +func byArrivalTime(end types.Stop) func(a, b *types.StopTime) int { + return func(a, b *types.StopTime) int { + distanceA := haversine(a.Stop.StopLat, a.Stop.StopLon, end.StopLat, end.StopLon) * 1000 + distanceB := haversine(b.Stop.StopLat, b.Stop.StopLon, end.StopLat, end.StopLon) * 1000 + return (int(distanceA) - int(distanceB)) + (int(b.ArrivalTime - a.ArrivalTime)) + } +} +func (tp *TripPlanner) findRoute(start types.StopTime, end *types.Stop, changes ...*types.StopTime) (*Route, error) { + if len(changes) >= maxTransfers { + return nil, fmt.Errorf("max transfers reached") + } + possibleNextStops := make([]*types.StopTime, 0) + for stop := range start.Stop.GetUpcomingStops(&start) { + if stop.StopId == end.StopId { + return &Route{ + Legs: CreateLegs(changes, stop), + }, nil + } else { + if !slices.ContainsFunc(changes, func(c *types.StopTime) bool { return c.StopId == stop.StopId }) { + possibleNextStops = append(possibleNextStops, stop) + } + } + } + slices.SortFunc(possibleNextStops, byArrivalTime(*end)) + + tries := 15 + for _, nextStop := range possibleNextStops { + route, err := tp.findRoute(*nextStop, end, append(changes, nextStop)...) + if err == nil && route != nil { + return route, nil + } + tries-- + if tries <= 0 { + break + } + } + return nil, fmt.Errorf("no route found") +} + +func CreateLegs(stops []*types.StopTime, finalStop *types.StopTime) []Leg { + legs := make([]Leg, 0, len(stops)+1) + var previousStop *types.StopTime + for _, stop := range stops { + if previousStop != nil { + legs = append(legs, NewLeg(previousStop, stop)) + } + previousStop = stop + } + legs = append(legs, NewLeg(previousStop, finalStop)) + return legs +} + +func NewLeg(fromStop, toStop *types.StopTime) Leg { + return Leg{ + From: fromStop, + To: toStop, + } +} + +// // findRoute implements a time-aware Dijkstra algorithm for routing +// func (tp *TripPlanner) findRoute(start, end string, when time.Time) *Route { +// csaPlanner := NewCSAPlanner(tp.TripData) +// return csaPlanner.FindRoute(start, end, when) +// } + +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 +} diff --git a/frontend/package-lock.json b/frontend/package-lock.json index 9267e02..49d9e2c 100644 --- a/frontend/package-lock.json +++ b/frontend/package-lock.json @@ -9,7 +9,8 @@ "version": "0.0.0", "dependencies": { "react": "^19.2.0", - "react-dom": "^19.2.0" + "react-dom": "^19.2.0", + "swr": "^2.3.6" }, "devDependencies": { "@eslint/js": "^9.39.1", @@ -1675,6 +1676,15 @@ "dev": true, "license": "MIT" }, + "node_modules/dequal": { + "version": "2.0.3", + "resolved": "https://registry.npmjs.org/dequal/-/dequal-2.0.3.tgz", + "integrity": "sha512-0je+qPKHEMohvfRTCEo3CrPG6cAzAYgmzKyxRiYSSDkS6eGJdyVJm7WaYA5ECaAD9wLB2T4EEeymA5aFVcYXCA==", + "license": "MIT", + "engines": { + "node": ">=6" + } + }, "node_modules/electron-to-chromium": { "version": "1.5.252", "resolved": "https://registry.npmjs.org/electron-to-chromium/-/electron-to-chromium-1.5.252.tgz", @@ -2653,6 +2663,19 @@ "node": ">=8" } }, + "node_modules/swr": { + "version": "2.3.6", + "resolved": "https://registry.npmjs.org/swr/-/swr-2.3.6.tgz", + "integrity": "sha512-wfHRmHWk/isGNMwlLGlZX5Gzz/uTgo0o2IRuTMcf4CPuPFJZlq0rDaKUx+ozB5nBOReNV1kiOyzMfj+MBMikLw==", + "license": "MIT", + "dependencies": { + "dequal": "^2.0.3", + "use-sync-external-store": "^1.4.0" + }, + "peerDependencies": { + "react": "^16.11.0 || ^17.0.0 || ^18.0.0 || ^19.0.0" + } + }, "node_modules/tinyglobby": { "version": "0.2.15", "resolved": "https://registry.npmjs.org/tinyglobby/-/tinyglobby-0.2.15.tgz", @@ -2724,6 +2747,15 @@ "punycode": "^2.1.0" } }, + "node_modules/use-sync-external-store": { + "version": "1.6.0", + "resolved": "https://registry.npmjs.org/use-sync-external-store/-/use-sync-external-store-1.6.0.tgz", + "integrity": "sha512-Pp6GSwGP/NrPIrxVFAIkOQeyw8lFenOHijQWkUTrDvrF4ALqylP2C/KCkeS9dpUM3KvYRQhna5vt7IL95+ZQ9w==", + "license": "MIT", + "peerDependencies": { + "react": "^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0" + } + }, "node_modules/vite": { "version": "7.2.2", "resolved": "https://registry.npmjs.org/vite/-/vite-7.2.2.tgz", diff --git a/frontend/package.json b/frontend/package.json index 585ac84..0db66fc 100644 --- a/frontend/package.json +++ b/frontend/package.json @@ -11,7 +11,8 @@ }, "dependencies": { "react": "^19.2.0", - "react-dom": "^19.2.0" + "react-dom": "^19.2.0", + "swr": "^2.3.6" }, "devDependencies": { "@eslint/js": "^9.39.1", diff --git a/frontend/src/App.jsx b/frontend/src/App.jsx index 2fccfc6..de4b082 100644 --- a/frontend/src/App.jsx +++ b/frontend/src/App.jsx @@ -1,30 +1,27 @@ -import { useState, useEffect } from "react"; +import { useState, useMemo } from "react"; +import useSWR from "swr"; import "./App.css"; function StopSelector({ stops, onChange, placeholder, inputId }) { const [inputValue, setInputValue] = useState(""); - const [filteredStops, setFilteredStops] = useState([]); - const [showList, setShowList] = useState(false); - useEffect(() => { + const filteredStops = useMemo(() => { if (inputValue.trim()) { - const filtered = stops + return stops .filter((stop) => stop.stop_name.toLowerCase().includes(inputValue.toLowerCase()), ) .slice(0, 10); - setFilteredStops(filtered); - setShowList(true); } else { - setFilteredStops([]); - setShowList(false); + return []; } }, [inputValue, stops]); + const showList = useMemo(() => filteredStops.length > 0, [filteredStops]); + const handleSelect = (stop) => { setInputValue(stop.stop_name); onChange({ id: stop.stop_id, name: stop.stop_name }); - setShowList(false); }; return ( @@ -50,8 +47,10 @@ function StopSelector({ stops, onChange, placeholder, inputId }) { ); } +const fetcher = (url) => fetch(url).then(res => res.ok ? res.json() : Promise.reject(res.statusText)); + function App() { - const [stops, setStops] = useState([]); + const { data: stops = [], error: stopsError } = useSWR('/api/stops', fetcher); const [from, setFrom] = useState({ id: "740000030", name: "Falun Centralstation", @@ -60,40 +59,21 @@ function App() { id: "740000001", name: "Stockholm Centralstation", }); - const [time, setTime] = useState(new Date().toISOString().slice(0, 16)); + const [time, setTime] = useState(new Date().toISOString()); const [num, setNum] = useState(3); - const [routes, setRoutes] = useState([]); + const [routeKey, setRouteKey] = useState(null); + const { data: routes = [], error: routesError, isLoading } = useSWR(routeKey, fetcher); const [selectedRouteIndex, setSelectedRouteIndex] = useState(0); - const [loading, setLoading] = useState(false); const [error, setError] = useState(""); - useEffect(() => { - fetch("/api/stops") - .then((res) => res.json()) - .then(setStops) - .catch((err) => setError("Failed to load stops: " + err.message)); - }, []); - const findRoute = () => { if (!from.id || !to.id) { setError("Please select both from and to stops"); return; } - setLoading(true); setError(""); - fetch( - `/api/route?from=${from.id}&to=${to.id}&when=${encodeURIComponent(time)}&num=${num}`, - ) - .then((res) => (res.ok ? res.json() : Promise.reject(res.statusText))) - .then((data) => { - setRoutes(data); - setSelectedRouteIndex(0); - setLoading(false); - }) - .catch((err) => { - setError("Failed to find route: " + err); - setLoading(false); - }); + setRouteKey(`/api/route?from=${from.id}&to=${to.id}&when=${encodeURIComponent(time)}&num=${num}`); + setSelectedRouteIndex(0); }; return ( @@ -142,12 +122,12 @@ function App() { - - {error &&

{error}

} + {(error || stopsError || routesError) &&

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

} {routes.length > 0 && (

diff --git a/frontend/vite.config.js b/frontend/vite.config.js index 4bc1597..ebfedde 100644 --- a/frontend/vite.config.js +++ b/frontend/vite.config.js @@ -8,8 +8,6 @@ export default defineConfig({ proxy: { "/api": { target: "http://localhost:8080", - changeOrigin: true, - secure: false, }, }, }, diff --git a/go.mod b/go.mod index 9bd4585..2eca20d 100644 --- a/go.mod +++ b/go.mod @@ -3,36 +3,8 @@ module git.tornberg.me/go-gtfs go 1.25.1 require ( - github.com/bytedance/sonic v1.14.0 // indirect - github.com/bytedance/sonic/loader v0.3.0 // indirect - github.com/cloudwego/base64x v0.1.6 // indirect - github.com/gabriel-vasile/mimetype v1.4.8 // indirect - github.com/gin-contrib/sse v1.1.0 // indirect - github.com/gin-gonic/gin v1.11.0 // indirect - github.com/go-playground/locales v0.14.1 // indirect - github.com/go-playground/universal-translator v0.18.1 // indirect - github.com/go-playground/validator/v10 v10.27.0 // indirect - github.com/goccy/go-json v0.10.2 // indirect - github.com/goccy/go-yaml v1.18.0 // indirect - github.com/json-iterator/go v1.1.12 // indirect - github.com/klauspost/cpuid/v2 v2.3.0 // indirect - github.com/leodido/go-urn v1.4.0 // indirect - github.com/mattn/go-isatty v0.0.20 // indirect - github.com/modern-go/concurrent v0.0.0-20180228061459-e0a39a4cb421 // indirect - github.com/modern-go/reflect2 v1.0.2 // indirect - github.com/pelletier/go-toml/v2 v2.2.4 // indirect - github.com/quic-go/qpack v0.5.1 // indirect - github.com/quic-go/quic-go v0.54.0 // indirect - github.com/twitchyliquid64/golang-asm v0.15.1 // indirect - github.com/ugorji/go/codec v1.3.0 // indirect - go.uber.org/mock v0.5.0 // indirect - golang.org/x/arch v0.20.0 // indirect - golang.org/x/crypto v0.40.0 // indirect - golang.org/x/mod v0.25.0 // indirect - golang.org/x/net v0.42.0 // indirect - golang.org/x/sync v0.16.0 // indirect - golang.org/x/sys v0.35.0 // indirect - golang.org/x/text v0.27.0 // indirect - golang.org/x/tools v0.34.0 // indirect - google.golang.org/protobuf v1.36.9 // indirect + github.com/davecgh/go-spew v1.1.1 // indirect + github.com/pmezard/go-difflib v1.0.0 // indirect + github.com/stretchr/testify v1.11.1 // indirect + gopkg.in/yaml.v3 v3.0.1 // indirect ) diff --git a/go.sum b/go.sum index 4bf36a7..cc8b3f4 100644 --- a/go.sum +++ b/go.sum @@ -1,79 +1,9 @@ -github.com/bytedance/sonic v1.14.0 h1:/OfKt8HFw0kh2rj8N0F6C/qPGRESq0BbaNZgcNXXzQQ= -github.com/bytedance/sonic v1.14.0/go.mod h1:WoEbx8WTcFJfzCe0hbmyTGrfjt8PzNEBdxlNUO24NhA= -github.com/bytedance/sonic/loader v0.3.0 h1:dskwH8edlzNMctoruo8FPTJDF3vLtDT0sXZwvZJyqeA= -github.com/bytedance/sonic/loader v0.3.0/go.mod h1:N8A3vUdtUebEY2/VQC0MyhYeKUFosQU6FxH2JmUe6VI= -github.com/cloudwego/base64x v0.1.6 h1:t11wG9AECkCDk5fMSoxmufanudBtJ+/HemLstXDLI2M= -github.com/cloudwego/base64x v0.1.6/go.mod h1:OFcloc187FXDaYHvrNIjxSe8ncn0OOM8gEHfghB2IPU= -github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= +github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c= github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= -github.com/gabriel-vasile/mimetype v1.4.8 h1:FfZ3gj38NjllZIeJAmMhr+qKL8Wu+nOoI3GqacKw1NM= -github.com/gabriel-vasile/mimetype v1.4.8/go.mod h1:ByKUIKGjh1ODkGM1asKUbQZOLGrPjydw3hYPU2YU9t8= -github.com/gin-contrib/sse v1.1.0 h1:n0w2GMuUpWDVp7qSpvze6fAu9iRxJY4Hmj6AmBOU05w= -github.com/gin-contrib/sse v1.1.0/go.mod h1:hxRZ5gVpWMT7Z0B0gSNYqqsSCNIJMjzvm6fqCz9vjwM= -github.com/gin-gonic/gin v1.11.0 h1:OW/6PLjyusp2PPXtyxKHU0RbX6I/l28FTdDlae5ueWk= -github.com/gin-gonic/gin v1.11.0/go.mod h1:+iq/FyxlGzII0KHiBGjuNn4UNENUlKbGlNmc+W50Dls= -github.com/go-playground/locales v0.14.1 h1:EWaQ/wswjilfKLTECiXz7Rh+3BjFhfDFKv/oXslEjJA= -github.com/go-playground/locales v0.14.1/go.mod h1:hxrqLVvrK65+Rwrd5Fc6F2O76J/NuW9t0sjnWqG1slY= -github.com/go-playground/universal-translator v0.18.1 h1:Bcnm0ZwsGyWbCzImXv+pAJnYK9S473LQFuzCbDbfSFY= -github.com/go-playground/universal-translator v0.18.1/go.mod h1:xekY+UJKNuX9WP91TpwSH2VMlDf28Uj24BCp08ZFTUY= -github.com/go-playground/validator/v10 v10.27.0 h1:w8+XrWVMhGkxOaaowyKH35gFydVHOvC0/uWoy2Fzwn4= -github.com/go-playground/validator/v10 v10.27.0/go.mod h1:I5QpIEbmr8On7W0TktmJAumgzX4CA1XNl4ZmDuVHKKo= -github.com/goccy/go-json v0.10.2 h1:CrxCmQqYDkv1z7lO7Wbh2HN93uovUHgrECaO5ZrCXAU= -github.com/goccy/go-json v0.10.2/go.mod h1:6MelG93GURQebXPDq3khkgXZkazVtN9CRI+MGFi0w8I= -github.com/goccy/go-yaml v1.18.0 h1:8W7wMFS12Pcas7KU+VVkaiCng+kG8QiFeFwzFb+rwuw= -github.com/goccy/go-yaml v1.18.0/go.mod h1:XBurs7gK8ATbW4ZPGKgcbrY1Br56PdM69F7LkFRi1kA= -github.com/google/gofuzz v1.0.0/go.mod h1:dBl0BpW6vV/+mYPU4Po3pmUjxk6FQPldtuIdl/M65Eg= -github.com/json-iterator/go v1.1.12 h1:PV8peI4a0ysnczrg+LtxykD8LfKY9ML6u2jnxaEnrnM= -github.com/json-iterator/go v1.1.12/go.mod h1:e30LSqwooZae/UwlEbR2852Gd8hjQvJoHmT4TnhNGBo= -github.com/klauspost/cpuid/v2 v2.3.0 h1:S4CRMLnYUhGeDFDqkGriYKdfoFlDnMtqTiI/sFzhA9Y= -github.com/klauspost/cpuid/v2 v2.3.0/go.mod h1:hqwkgyIinND0mEev00jJYCxPNVRVXFQeu1XKlok6oO0= -github.com/leodido/go-urn v1.4.0 h1:WT9HwE9SGECu3lg4d/dIA+jxlljEa1/ffXKmRjqdmIQ= -github.com/leodido/go-urn v1.4.0/go.mod h1:bvxc+MVxLKB4z00jd1z+Dvzr47oO32F/QSNjSBOlFxI= -github.com/mattn/go-isatty v0.0.20 h1:xfD0iDuEKnDkl03q4limB+vH+GxLEtL/jb4xVJSWWEY= -github.com/mattn/go-isatty v0.0.20/go.mod h1:W+V8PltTTMOvKvAeJH7IuucS94S2C6jfK/D7dTCTo3Y= -github.com/modern-go/concurrent v0.0.0-20180228061459-e0a39a4cb421 h1:ZqeYNhU3OHLH3mGKHDcjJRFFRrJa6eAM5H+CtDdOsPc= -github.com/modern-go/concurrent v0.0.0-20180228061459-e0a39a4cb421/go.mod h1:6dJC0mAP4ikYIbvyc7fijjWJddQyLn8Ig3JB5CqoB9Q= -github.com/modern-go/reflect2 v1.0.2 h1:xBagoLtFs94CBntxluKeaWgTMpvLxC4ur3nMaC9Gz0M= -github.com/modern-go/reflect2 v1.0.2/go.mod h1:yWuevngMOJpCy52FWWMvUC8ws7m/LJsjYzDa0/r8luk= -github.com/pelletier/go-toml/v2 v2.2.4 h1:mye9XuhQ6gvn5h28+VilKrrPoQVanw5PMw/TB0t5Ec4= -github.com/pelletier/go-toml/v2 v2.2.4/go.mod h1:2gIqNv+qfxSVS7cM2xJQKtLSTLUE9V8t9Stt+h56mCY= +github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM= github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4= -github.com/quic-go/qpack v0.5.1 h1:giqksBPnT/HDtZ6VhtFKgoLOWmlyo9Ei6u9PqzIMbhI= -github.com/quic-go/qpack v0.5.1/go.mod h1:+PC4XFrEskIVkcLzpEkbLqq1uCoxPhQuvK5rH1ZgaEg= -github.com/quic-go/quic-go v0.54.0 h1:6s1YB9QotYI6Ospeiguknbp2Znb/jZYjZLRXn9kMQBg= -github.com/quic-go/quic-go v0.54.0/go.mod h1:e68ZEaCdyviluZmy44P6Iey98v/Wfz6HCjQEm+l8zTY= -github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME= -github.com/stretchr/objx v0.4.0/go.mod h1:YvHI0jy2hoMjB+UWwv71VJQ9isScKT/TqJzVSSt89Yw= -github.com/stretchr/objx v0.5.0/go.mod h1:Yh+to48EsGEfYuaHDzXPcE3xhTkx73EhmCGUpEOglKo= -github.com/stretchr/testify v1.3.0/go.mod h1:M5WIy9Dh21IEIfnGCwXGc5bZfKNJtfHm1UVUgZn+9EI= -github.com/stretchr/testify v1.7.1/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg= -github.com/stretchr/testify v1.8.0/go.mod h1:yNjHg4UonilssWZ8iaSj1OCr/vHnekPRkoO+kdMU+MU= -github.com/stretchr/testify v1.8.1/go.mod h1:w2LPCIKwWwSfY2zedu0+kehJoqGctiVI29o6fzry7u4= -github.com/twitchyliquid64/golang-asm v0.15.1 h1:SU5vSMR7hnwNxj24w34ZyCi/FmDZTkS4MhqMhdFk5YI= -github.com/twitchyliquid64/golang-asm v0.15.1/go.mod h1:a1lVb/DtPvCB8fslRZhAngC2+aY1QWCk3Cedj/Gdt08= -github.com/ugorji/go/codec v1.3.0 h1:Qd2W2sQawAfG8XSvzwhBeoGq71zXOC/Q1E9y/wUcsUA= -github.com/ugorji/go/codec v1.3.0/go.mod h1:pRBVtBSKl77K30Bv8R2P+cLSGaTtex6fsA2Wjqmfxj4= -go.uber.org/mock v0.5.0 h1:KAMbZvZPyBPWgD14IrIQ38QCyjwpvVVV6K/bHl1IwQU= -go.uber.org/mock v0.5.0/go.mod h1:ge71pBPLYDk7QIi1LupWxdAykm7KIEFchiOqd6z7qMM= -golang.org/x/arch v0.20.0 h1:dx1zTU0MAE98U+TQ8BLl7XsJbgze2WnNKF/8tGp/Q6c= -golang.org/x/arch v0.20.0/go.mod h1:bdwinDaKcfZUGpH09BB7ZmOfhalA8lQdzl62l8gGWsk= -golang.org/x/crypto v0.40.0 h1:r4x+VvoG5Fm+eJcxMaY8CQM7Lb0l1lsmjGBQ6s8BfKM= -golang.org/x/crypto v0.40.0/go.mod h1:Qr1vMER5WyS2dfPHAlsOj01wgLbsyWtFn/aY+5+ZdxY= -golang.org/x/mod v0.25.0 h1:n7a+ZbQKQA/Ysbyb0/6IbB1H/X41mKgbhfv7AfG/44w= -golang.org/x/mod v0.25.0/go.mod h1:IXM97Txy2VM4PJ3gI61r1YEk/gAj6zAHN3AdZt6S9Ww= -golang.org/x/net v0.42.0 h1:jzkYrhi3YQWD6MLBJcsklgQsoAcw89EcZbJw8Z614hs= -golang.org/x/net v0.42.0/go.mod h1:FF1RA5d3u7nAYA4z2TkclSCKh68eSXtiFwcWQpPXdt8= -golang.org/x/sync v0.16.0 h1:ycBJEhp9p4vXvUZNszeOq0kGTPghopOL8q0fq3vstxw= -golang.org/x/sync v0.16.0/go.mod h1:1dzgHSNfp02xaA81J2MS99Qcpr2w7fw1gpm99rleRqA= -golang.org/x/sys v0.6.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= -golang.org/x/sys v0.35.0 h1:vz1N37gP5bs89s7He8XuIYXpyY0+QlsKmzipCbUtyxI= -golang.org/x/sys v0.35.0/go.mod h1:BJP2sWEmIv4KK5OTEluFJCKSidICx8ciO85XgH3Ak8k= -golang.org/x/text v0.27.0 h1:4fGWRpyh641NLlecmyl4LOe6yDdfaYNrGb2zdfo4JV4= -golang.org/x/text v0.27.0/go.mod h1:1D28KMCvyooCX9hBiosv5Tz/+YLxj0j7XhWjpSUF7CU= -golang.org/x/tools v0.34.0 h1:qIpSLOxeCYGg9TrcJokLBG4KFA6d795g0xkBkiESGlo= -golang.org/x/tools v0.34.0/go.mod h1:pAP9OwEaY1CAW3HOmg3hLZC5Z0CCmzjAF2UQMSqNARg= -google.golang.org/protobuf v1.36.9 h1:w2gp2mA27hUeUzj9Ex9FBjsBm40zfaDtEWow293U7Iw= -google.golang.org/protobuf v1.36.9/go.mod h1:fuxRtAxBytpl4zzqUh6/eyUujkJdNiuEkXntxiD/uRU= +github.com/stretchr/testify v1.11.1 h1:7s2iGBzp5EwR7/aIZr8ao5+dra3wiQyKjjFuvgVKu7U= +github.com/stretchr/testify v1.11.1/go.mod h1:wZwfW3scLgRK+23gO65QZefKpKQRnfz6sD981Nm4B6U= gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= -gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= +gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA= gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= diff --git a/pkg/reader/csvreader.go b/pkg/reader/csvreader.go index eee63e2..7241e4a 100644 --- a/pkg/reader/csvreader.go +++ b/pkg/reader/csvreader.go @@ -95,7 +95,7 @@ func ParseCalendarDates(r io.Reader, callback func(types.CalendarDate)) error { return nil } -func ParseFeedInfos(r io.Reader, callback func(types.FeedInfo)) error { +func ParseFeedInfos(r io.Reader, callback func(id string, data types.FeedInfo)) error { reader := csv.NewReader(r) if _, err := reader.Read(); err != nil { return err @@ -109,13 +109,13 @@ func ParseFeedInfos(r io.Reader, callback func(types.FeedInfo)) error { return err } feedInfo := types.FeedInfo{ - FeedID: record[0], - FeedPublisherName: record[1], - FeedPublisherURL: record[2], - FeedLang: record[3], - FeedVersion: record[4], + //FeedID: record[0], + PublisherName: record[1], + PublisherURL: record[2], + Language: record[3], + Version: record[4], } - callback(feedInfo) + callback(record[0], feedInfo) } return nil } @@ -135,6 +135,7 @@ func ParseRoutes(r io.Reader, callback func(types.Route)) error { } routeType, _ := strconv.Atoi(record[4]) route := types.Route{ + Trips: make([]*types.Trip, 0), RouteID: record[0], AgencyID: record[1], RouteShortName: record[2], @@ -147,7 +148,7 @@ func ParseRoutes(r io.Reader, callback func(types.Route)) error { return nil } -func ParseStopTimes(r io.Reader, callback func(types.StopTime)) error { +func ParseStopTimes(r io.Reader, callback func(data types.StopTime)) error { reader := csv.NewReader(r) if _, err := reader.Read(); err != nil { return err @@ -164,10 +165,10 @@ func ParseStopTimes(r io.Reader, callback func(types.StopTime)) error { pickupType, _ := strconv.Atoi(record[5]) dropOffType, _ := strconv.Atoi(record[6]) stopTime := types.StopTime{ - TripID: record[0], - ArrivalTime: record[1], - DepartureTime: record[2], - StopID: record[3], + TripId: record[0], + ArrivalTime: types.ParseTimeString(record[1]), + DepartureTime: types.ParseTimeString(record[2]), + StopId: record[3], StopSequence: stopSequence, PickupType: pickupType, DropOffType: dropOffType, @@ -194,7 +195,9 @@ func ParseStops(r io.Reader, callback func(types.Stop)) error { stopLon, _ := strconv.ParseFloat(record[3], 64) locationType, _ := strconv.Atoi(record[4]) stop := types.Stop{ - StopID: record[0], + Trips: make(map[string]*types.Trip), + Transfers: make([]*types.Transfer, 0), + StopId: record[0], StopName: record[1], StopLat: stopLat, StopLon: stopLon, @@ -221,12 +224,12 @@ func ParseTransfers(r io.Reader, callback func(types.Transfer)) error { transferType, _ := strconv.Atoi(record[2]) minTransferTime, _ := strconv.Atoi(record[3]) transfer := types.Transfer{ - FromStopID: record[0], - ToStopID: record[1], + FromStopId: record[0], + ToStopId: record[1], TransferType: transferType, MinTransferTime: minTransferTime, - FromTripID: record[4], - ToTripID: record[5], + FromTripId: record[4], + ToTripId: record[5], } callback(transfer) } @@ -247,9 +250,9 @@ func ParseTrips(r io.Reader, callback func(types.Trip)) error { return err } trip := types.Trip{ - RouteID: record[0], - ServiceID: record[1], - TripID: record[2], + RouteId: record[0], + ServiceId: record[1], + TripId: record[2], TripHeadsign: record[3], TripShortName: record[4], } diff --git a/pkg/reader/loader.go b/pkg/reader/loader.go new file mode 100644 index 0000000..0ccd749 --- /dev/null +++ b/pkg/reader/loader.go @@ -0,0 +1,103 @@ +package reader + +import ( + "log" + "os" + "path/filepath" + + "git.tornberg.me/go-gtfs/pkg/types" +) + +type TripData struct { + Stops map[string]*types.Stop + Trips map[string]*types.Trip + Routes map[string]*types.Route + Agencies map[string]*types.Agency +} + +func LoadTripData(path string) (*TripData, error) { + tp := &TripData{ + Stops: make(map[string]*types.Stop), + Trips: make(map[string]*types.Trip), + Routes: make(map[string]*types.Route), + Agencies: make(map[string]*types.Agency), + } + + files := []string{"agency", "routes", "stops", "trips", "stop_times", "transfers"} + for _, file := range files { + + f, err := os.Open(filepath.Join(path, file+".txt")) + if err != nil { + log.Fatalf("failed to open %s: %v", file, err) + } + defer f.Close() + + switch file { + case "agency": + err = ParseAgencies(f, func(a types.Agency) { + tp.Agencies[a.AgencyID] = &a + }) + case "routes": + 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) + } + }) + case "stops": + err = ParseStops(f, func(s types.Stop) { + tp.Stops[s.StopId] = &s + }) + case "trips": + err = 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 = 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 = ParseTransfers(f, func(tr types.Transfer) { + //transfers = append(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 { + return tp, err + } + } + return tp, nil +} diff --git a/pkg/types/feedinfo.go b/pkg/types/feedinfo.go index dd3bf4e..fa80a4c 100644 --- a/pkg/types/feedinfo.go +++ b/pkg/types/feedinfo.go @@ -1,9 +1,9 @@ package types type FeedInfo struct { - FeedID string `json:"feed_id" csv:"feed_id"` - FeedPublisherName string `json:"feed_publisher_name" csv:"feed_publisher_name"` - FeedPublisherURL string `json:"feed_publisher_url" csv:"feed_publisher_url"` - FeedLang string `json:"feed_lang" csv:"feed_lang"` - FeedVersion string `json:"feed_version" csv:"feed_version"` + // FeedID string `json:"feed_id" csv:"feed_id"` + PublisherName string `json:"feed_publisher_name" csv:"feed_publisher_name"` + PublisherURL string `json:"feed_publisher_url" csv:"feed_publisher_url"` + Language string `json:"feed_lang" csv:"feed_lang"` + Version string `json:"feed_version" csv:"feed_version"` } diff --git a/pkg/types/stop.go b/pkg/types/stop.go index fdadd3c..321f78e 100644 --- a/pkg/types/stop.go +++ b/pkg/types/stop.go @@ -1,17 +1,18 @@ package types import ( + "iter" "time" ) type Stop struct { - trips map[string]*Trip - StopID string `json:"stop_id" csv:"stop_id"` - StopName string `json:"stop_name" csv:"stop_name"` - StopLat float64 `json:"stop_lat" csv:"stop_lat"` - StopLon float64 `json:"stop_lon" csv:"stop_lon"` - LocationType int `json:"location_type" csv:"location_type"` - Transfers []*Transfer `json:"transfers" csv:"transfers"` + Trips map[string]*Trip `json:"-" csv:"-"` + StopId string `json:"stop_id" csv:"stop_id"` + StopName string `json:"stop_name" csv:"stop_name"` + StopLat float64 `json:"stop_lat" csv:"stop_lat"` + StopLon float64 `json:"stop_lon" csv:"stop_lon"` + LocationType int `json:"location_type" csv:"location_type"` + Transfers []*Transfer `json:"-" csv:"transfers"` } func (s *Stop) AddTransfer(transfer *Transfer) { @@ -22,21 +23,71 @@ func (s *Stop) AddTransfer(transfer *Transfer) { } func (s *Stop) AddTrip(trip *Trip) { - if s.trips == nil { - s.trips = make(map[string]*Trip) - } - s.trips[trip.TripID] = trip + + s.Trips[trip.TripId] = trip } -func (s *Stop) GetTripsAfter(time time.Time) []*Trip { - var trips []*Trip - for _, trip := range s.trips { - for _, stop := range trip.Stops { - if stop.StopID == s.StopID && stop.DepartsAfter(time) { - trips = append(trips, trip) - break +type TripWithDepartureTime struct { + *Trip + DepartureTime SecondsAfterMidnight +} + +func (s *Stop) GetTripsAfter(when time.Time) iter.Seq[*TripWithDepartureTime] { + startAfterMidnight := AsSecondsAfterMidnight(when) + return func(yield func(*TripWithDepartureTime) bool) { + for _, trip := range s.Trips { + + for _, stop := range trip.Stops { + if stop.StopId == s.StopId && stop.ArrivalTime >= startAfterMidnight { + + if !yield(&TripWithDepartureTime{Trip: trip, DepartureTime: stop.DepartureTime}) { + return + } + break + } + } + + } + } +} + +func (s *Stop) GetUpcomingStops(start *StopTime) iter.Seq[*StopTime] { + return func(yield func(*StopTime) bool) { + found := false + for _, trip := range s.Trips { + for _, stop := range trip.Stops { + if !found { + if stop.StopId == start.StopId && stop.DepartureTime >= start.ArrivalTime { + found = true + } + } else { + if !yield(stop) { + return + } + } } } } - return trips +} + +func (s *Stop) GetStopsAfter(when time.Time) iter.Seq2[*StopTime, *StopTime] { + startAfterMidnight := AsSecondsAfterMidnight(when) + return func(yield func(start, stop *StopTime) bool) { + for _, trip := range s.Trips { + found := false + var start *StopTime + for _, stop := range trip.Stops { + if stop.StopId == s.StopId && stop.ArrivalTime >= startAfterMidnight { + found = true + start = stop + } + if found { + if !yield(start, stop) { + return + } + } + } + } + + } } diff --git a/pkg/types/stoptime.go b/pkg/types/stoptime.go index 8f79c5a..72554cf 100644 --- a/pkg/types/stoptime.go +++ b/pkg/types/stoptime.go @@ -1,24 +1,34 @@ package types import ( + "fmt" + "iter" "strconv" "strings" "time" ) -type StopTime struct { - departureTime int - Stop *Stop `json:"stop"` - TripID string `json:"trip_id" csv:"trip_id"` - ArrivalTime string `json:"arrival_time" csv:"arrival_time"` - DepartureTime string `json:"departure_time" csv:"departure_time"` - StopID string `json:"stop_id" csv:"stop_id"` - StopSequence int `json:"stop_sequence" csv:"stop_sequence"` - PickupType int `json:"pickup_type" csv:"pickup_type"` - DropOffType int `json:"drop_off_type" csv:"drop_off_type"` +type SecondsAfterMidnight int + +func AsTime(s SecondsAfterMidnight) string { + h := int(s) / 3600 + m := (int(s) % 3600) / 60 + sec := int(s) % 60 + return fmt.Sprintf("%02d:%02d:%02d", h, m, sec) } -func parseTime(s string) int { +type StopTime struct { + Stop *Stop `json:"stop"` + TripId string `json:"trip_id" csv:"trip_id"` + ArrivalTime SecondsAfterMidnight `json:"arrival_time" csv:"arrival_time"` + DepartureTime SecondsAfterMidnight `json:"departure_time" csv:"departure_time"` + StopId string `json:"stop_id" csv:"stop_id"` + StopSequence int `json:"stop_sequence" csv:"stop_sequence"` + PickupType int `json:"pickup_type" csv:"pickup_type"` + DropOffType int `json:"drop_off_type" csv:"drop_off_type"` +} + +func ParseTimeString(s string) SecondsAfterMidnight { parts := strings.Split(s, ":") if len(parts) != 3 { return 0 @@ -26,37 +36,29 @@ func parseTime(s string) int { h, _ := strconv.Atoi(parts[0]) m, _ := strconv.Atoi(parts[1]) sec, _ := strconv.Atoi(parts[2]) - return h*3600 + m*60 + sec + return SecondsAfterMidnight(h*3600 + m*60 + sec) } -func (st *StopTime) DepartTimeAsSeconds() int { - if st.departureTime > 0 { - return st.departureTime - } - if st.DepartureTime == "" { - return 0 - } - - st.departureTime = parseTime(st.DepartureTime) - return st.departureTime +func AsSecondsAfterMidnight(when time.Time) SecondsAfterMidnight { + return SecondsAfterMidnight(when.Hour()*3600 + when.Minute()*60 + when.Second()) } func (st *StopTime) DepartsAfter(when time.Time) bool { - secondsAfterMidnight := st.DepartTimeAsSeconds() - return secondsAfterMidnight >= when.Hour()*3600+when.Minute()*60+when.Second() + + return st.DepartureTime >= AsSecondsAfterMidnight(when) } -// func (st *StopTime) GetPossibleTrips() iter.Seq[*Trip] { -// return func(yield func(*Trip) bool) { -// for _, trip := range st.Stop.trips { -// if trip.TripID != st.TripID { -// if !yield(trip) { -// return -// } -// } -// } -// } -// } +func (st *StopTime) GetPossibleTrips() iter.Seq[*Trip] { + return func(yield func(*Trip) bool) { + for _, trip := range st.Stop.Trips { + if trip.TripId != st.TripId { + if !yield(trip) { + return + } + } + } + } +} func (st *StopTime) SetStop(stop *Stop) { st.Stop = stop diff --git a/pkg/types/transfer.go b/pkg/types/transfer.go index 2eb720c..6f600b2 100644 --- a/pkg/types/transfer.go +++ b/pkg/types/transfer.go @@ -1,10 +1,10 @@ package types type Transfer struct { - FromStopID string `json:"from_stop_id" csv:"from_stop_id"` - ToStopID string `json:"to_stop_id" csv:"to_stop_id"` + 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"` MinTransferTime int `json:"min_transfer_time" csv:"min_transfer_time"` - FromTripID string `json:"from_trip_id" csv:"from_trip_id"` - ToTripID string `json:"to_trip_id" csv:"to_trip_id"` + FromTripId string `json:"from_trip_id" csv:"from_trip_id"` + ToTripId string `json:"to_trip_id" csv:"to_trip_id"` } diff --git a/pkg/types/trip.go b/pkg/types/trip.go index 3758992..6648174 100644 --- a/pkg/types/trip.go +++ b/pkg/types/trip.go @@ -8,9 +8,9 @@ import ( type Trip struct { *Route Stops []*StopTime `json:"stops" csv:"stops"` - RouteID string `json:"route_id" csv:"route_id"` - ServiceID string `json:"service_id" csv:"service_id"` - TripID string `json:"trip_id" csv:"trip_id"` + RouteId string `json:"route_id" csv:"route_id"` + ServiceId string `json:"service_id" csv:"service_id"` + TripId string `json:"trip_id" csv:"trip_id"` TripHeadsign string `json:"trip_headsign" csv:"trip_headsign"` TripShortName string `json:"trip_short_name" csv:"trip_short_name"` } @@ -20,7 +20,7 @@ func (t *Trip) GetDirectPossibleDestinations(stop *Stop, when time.Time) iter.Se started := false for _, st := range t.Stops { if !started { - if st.StopID == stop.StopID && st.PickupType == 0 { + if st.StopId == stop.StopId && st.PickupType == 0 { started = true } continue @@ -44,3 +44,12 @@ func (t *Trip) AddStopTime(stopTime *StopTime) { } t.Stops = append(t.Stops, stopTime) } + +func (t *Trip) Has(stop *Stop) (*StopTime, bool) { + for _, st := range t.Stops { + if st.StopId == stop.StopId { + return st, true + } + } + return nil, false +}