some custom stuff
This commit is contained in:
16
.vscode/launch.json
vendored
Normal file
16
.vscode/launch.json
vendored
Normal file
@@ -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}",
|
||||||
|
},
|
||||||
|
]
|
||||||
|
}
|
||||||
261
cmd/planner/astar.go
Normal file
261
cmd/planner/astar.go
Normal file
@@ -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}
|
||||||
|
// }
|
||||||
217
cmd/planner/csa.go
Normal file
217
cmd/planner/csa.go
Normal file
@@ -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
|
||||||
|
// }
|
||||||
@@ -1,497 +1,23 @@
|
|||||||
package main
|
package main
|
||||||
|
|
||||||
import (
|
import (
|
||||||
"container/heap"
|
|
||||||
"encoding/json"
|
"encoding/json"
|
||||||
"fmt"
|
"fmt"
|
||||||
"log"
|
"log"
|
||||||
"net/http"
|
"net/http"
|
||||||
"os"
|
"os"
|
||||||
"sort"
|
|
||||||
"strconv"
|
"strconv"
|
||||||
"sync"
|
|
||||||
"time"
|
"time"
|
||||||
|
|
||||||
"git.tornberg.me/go-gtfs/pkg/reader"
|
"git.tornberg.me/go-gtfs/pkg/reader"
|
||||||
"git.tornberg.me/go-gtfs/pkg/types"
|
"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 {
|
type Edge struct {
|
||||||
To string
|
To string
|
||||||
TripID string
|
TripID string
|
||||||
Time float64
|
Time types.SecondsAfterMidnight
|
||||||
DepartureTime float64
|
DepartureTime types.SecondsAfterMidnight
|
||||||
}
|
}
|
||||||
|
|
||||||
type TripDetail struct {
|
type TripDetail struct {
|
||||||
@@ -501,42 +27,33 @@ type TripDetail struct {
|
|||||||
}
|
}
|
||||||
|
|
||||||
type Leg struct {
|
type Leg struct {
|
||||||
From string `json:"-"`
|
From *types.StopTime `json:"start"`
|
||||||
FromStop *types.Stop `json:"from"`
|
To *types.StopTime `json:"end"`
|
||||||
To string `json:"-"`
|
|
||||||
ToStop *types.Stop `json:"to"`
|
|
||||||
TripID string `json:"-"`
|
|
||||||
Trip *types.Trip `json:"trip"`
|
|
||||||
Stops []string `json:"stops"`
|
|
||||||
Agency *types.Agency `json:"agency"`
|
|
||||||
Route *types.Route `json:"route"`
|
|
||||||
DepartureTime time.Time `json:"departure_time"`
|
|
||||||
ArrivalTime time.Time `json:"arrival_time"`
|
|
||||||
}
|
}
|
||||||
|
|
||||||
type Route struct {
|
type Route struct {
|
||||||
Legs []Leg `json:"legs"`
|
Legs []Leg `json:"legs"`
|
||||||
}
|
}
|
||||||
|
|
||||||
func (r *Route) EndTime() time.Time {
|
func (r *Route) EndTime() types.SecondsAfterMidnight {
|
||||||
if len(r.Legs) == 0 {
|
|
||||||
return time.Time{}
|
|
||||||
}
|
|
||||||
return r.Legs[len(r.Legs)-1].ArrivalTime
|
|
||||||
}
|
|
||||||
|
|
||||||
func (r *Route) StartTime() time.Time {
|
|
||||||
if len(r.Legs) == 0 {
|
|
||||||
return time.Time{}
|
|
||||||
}
|
|
||||||
return r.Legs[0].DepartureTime
|
|
||||||
}
|
|
||||||
|
|
||||||
func (r *Route) Duration() time.Duration {
|
|
||||||
if len(r.Legs) == 0 {
|
if len(r.Legs) == 0 {
|
||||||
return 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 {
|
type PathInfo struct {
|
||||||
@@ -549,36 +66,43 @@ type PathInfo struct {
|
|||||||
}
|
}
|
||||||
|
|
||||||
func main() {
|
func main() {
|
||||||
tp := NewTripPlanner()
|
tripData, err := reader.LoadTripData("data")
|
||||||
wg := &sync.WaitGroup{}
|
if err != nil {
|
||||||
if err := tp.LoadData("data"); err != nil {
|
log.Fatalf("unable to load data %v", err)
|
||||||
fmt.Printf("Failed to load data: %v\n", err)
|
|
||||||
os.Exit(1)
|
|
||||||
}
|
}
|
||||||
wg.Wait()
|
tp := NewTripPlanner(tripData)
|
||||||
|
|
||||||
if err := tp.Preprocess(); err != nil {
|
if err := tp.Preprocess(); err != nil {
|
||||||
fmt.Printf("Failed to preprocess data: %v\n", err)
|
fmt.Printf("Failed to preprocess data: %v\n", err)
|
||||||
os.Exit(1)
|
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) {
|
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{}
|
stopList := []types.Stop{}
|
||||||
for _, s := range tp.stops {
|
for _, s := range tp.Stops {
|
||||||
if _, hasConnections := tp.graph[s.StopID]; hasConnections {
|
if len(s.Trips) > 0 {
|
||||||
stopList = append(stopList, *s)
|
stopList = append(stopList, *s)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
w.WriteHeader(http.StatusOK)
|
||||||
w.Header().Set("Content-Type", "application/json")
|
w.Header().Set("Content-Type", "application/json")
|
||||||
json.NewEncoder(w).Encode(stopList)
|
json.NewEncoder(w).Encode(stopList)
|
||||||
})
|
})
|
||||||
|
|
||||||
http.HandleFunc("/api/route", func(w http.ResponseWriter, r *http.Request) {
|
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")
|
from := r.URL.Query().Get("from")
|
||||||
to := r.URL.Query().Get("to")
|
to := r.URL.Query().Get("to")
|
||||||
whenStr := r.URL.Query().Get("when")
|
whenStr := r.URL.Query().Get("when")
|
||||||
@@ -591,7 +115,7 @@ func main() {
|
|||||||
}
|
}
|
||||||
when := time.Now()
|
when := time.Now()
|
||||||
if whenStr != "" {
|
if whenStr != "" {
|
||||||
if parsed, err := time.Parse(time.RFC3339, whenStr); err == nil {
|
if parsed, err := time.Parse(time.DateTime, whenStr); err == nil {
|
||||||
when = parsed
|
when = parsed
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -600,14 +124,17 @@ func main() {
|
|||||||
json.NewEncoder(w).Encode(map[string]string{"error": "from and to parameters required"})
|
json.NewEncoder(w).Encode(map[string]string{"error": "from and to parameters required"})
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
routes := tp.FindRoutes(from, to, when, num)
|
log.Printf("using num %v", num)
|
||||||
if len(routes) == 0 {
|
w.WriteHeader(http.StatusOK)
|
||||||
|
log.Printf("start time %v", when)
|
||||||
|
route, err := tp.FindRoute(from, to, when)
|
||||||
|
if err != nil {
|
||||||
w.WriteHeader(http.StatusNotFound)
|
w.WriteHeader(http.StatusNotFound)
|
||||||
json.NewEncoder(w).Encode(map[string]string{"error": "no route found"})
|
json.NewEncoder(w).Encode(map[string]string{"error": "no route found"})
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
w.Header().Set("Content-Type", "application/json")
|
w.Header().Set("Content-Type", "application/json")
|
||||||
json.NewEncoder(w).Encode(routes)
|
json.NewEncoder(w).Encode(route)
|
||||||
})
|
})
|
||||||
log.Printf("Listening on 8080")
|
log.Printf("Listening on 8080")
|
||||||
http.ListenAndServe(":8080", nil)
|
http.ListenAndServe(":8080", nil)
|
||||||
|
|||||||
@@ -1,26 +1,32 @@
|
|||||||
package main
|
package main
|
||||||
|
|
||||||
import (
|
import (
|
||||||
"os"
|
|
||||||
"testing"
|
"testing"
|
||||||
"time"
|
"time"
|
||||||
|
|
||||||
|
"git.tornberg.me/go-gtfs/pkg/reader"
|
||||||
)
|
)
|
||||||
|
|
||||||
func TestFindRoute(t *testing.T) {
|
var tripData *reader.TripData
|
||||||
os.Chdir("../..")
|
|
||||||
tp := NewTripPlanner()
|
|
||||||
|
|
||||||
err := tp.LoadData("data")
|
func init() {
|
||||||
|
var err error
|
||||||
|
tripData, err = reader.LoadTripData("../../data")
|
||||||
if err != nil {
|
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) {
|
||||||
if err != nil {
|
|
||||||
t.Fatalf("Failed to preprocess: %v", err)
|
|
||||||
}
|
|
||||||
|
|
||||||
route := tp.FindRoute("740000030", "740000001", time.Now())
|
tp := NewTripPlanner(tripData)
|
||||||
|
|
||||||
|
//tp.Preprocess()
|
||||||
|
|
||||||
|
route, err := tp.FindRoute("740000030", "740000001", time.Now().Add(time.Hour*-16))
|
||||||
|
if err != nil {
|
||||||
|
t.Fatalf("Error finding route: %v", err)
|
||||||
|
}
|
||||||
if route == nil {
|
if route == nil {
|
||||||
t.Fatal("No route found from Falun Centralstation to Stockholm Centralstation")
|
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")
|
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) {
|
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 {
|
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 {
|
if route == nil {
|
||||||
t.Fatal("No route found from Falun Centralstation to Malmö Centralstation")
|
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")
|
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)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|||||||
236
cmd/planner/planner.go
Normal file
236
cmd/planner/planner.go
Normal file
@@ -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
|
||||||
|
}
|
||||||
34
frontend/package-lock.json
generated
34
frontend/package-lock.json
generated
@@ -9,7 +9,8 @@
|
|||||||
"version": "0.0.0",
|
"version": "0.0.0",
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"react": "^19.2.0",
|
"react": "^19.2.0",
|
||||||
"react-dom": "^19.2.0"
|
"react-dom": "^19.2.0",
|
||||||
|
"swr": "^2.3.6"
|
||||||
},
|
},
|
||||||
"devDependencies": {
|
"devDependencies": {
|
||||||
"@eslint/js": "^9.39.1",
|
"@eslint/js": "^9.39.1",
|
||||||
@@ -1675,6 +1676,15 @@
|
|||||||
"dev": true,
|
"dev": true,
|
||||||
"license": "MIT"
|
"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": {
|
"node_modules/electron-to-chromium": {
|
||||||
"version": "1.5.252",
|
"version": "1.5.252",
|
||||||
"resolved": "https://registry.npmjs.org/electron-to-chromium/-/electron-to-chromium-1.5.252.tgz",
|
"resolved": "https://registry.npmjs.org/electron-to-chromium/-/electron-to-chromium-1.5.252.tgz",
|
||||||
@@ -2653,6 +2663,19 @@
|
|||||||
"node": ">=8"
|
"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": {
|
"node_modules/tinyglobby": {
|
||||||
"version": "0.2.15",
|
"version": "0.2.15",
|
||||||
"resolved": "https://registry.npmjs.org/tinyglobby/-/tinyglobby-0.2.15.tgz",
|
"resolved": "https://registry.npmjs.org/tinyglobby/-/tinyglobby-0.2.15.tgz",
|
||||||
@@ -2724,6 +2747,15 @@
|
|||||||
"punycode": "^2.1.0"
|
"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": {
|
"node_modules/vite": {
|
||||||
"version": "7.2.2",
|
"version": "7.2.2",
|
||||||
"resolved": "https://registry.npmjs.org/vite/-/vite-7.2.2.tgz",
|
"resolved": "https://registry.npmjs.org/vite/-/vite-7.2.2.tgz",
|
||||||
|
|||||||
@@ -11,7 +11,8 @@
|
|||||||
},
|
},
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"react": "^19.2.0",
|
"react": "^19.2.0",
|
||||||
"react-dom": "^19.2.0"
|
"react-dom": "^19.2.0",
|
||||||
|
"swr": "^2.3.6"
|
||||||
},
|
},
|
||||||
"devDependencies": {
|
"devDependencies": {
|
||||||
"@eslint/js": "^9.39.1",
|
"@eslint/js": "^9.39.1",
|
||||||
|
|||||||
@@ -1,30 +1,27 @@
|
|||||||
import { useState, useEffect } from "react";
|
import { useState, useMemo } from "react";
|
||||||
|
import useSWR from "swr";
|
||||||
import "./App.css";
|
import "./App.css";
|
||||||
|
|
||||||
function StopSelector({ stops, onChange, placeholder, inputId }) {
|
function StopSelector({ stops, onChange, placeholder, inputId }) {
|
||||||
const [inputValue, setInputValue] = useState("");
|
const [inputValue, setInputValue] = useState("");
|
||||||
const [filteredStops, setFilteredStops] = useState([]);
|
|
||||||
const [showList, setShowList] = useState(false);
|
|
||||||
|
|
||||||
useEffect(() => {
|
const filteredStops = useMemo(() => {
|
||||||
if (inputValue.trim()) {
|
if (inputValue.trim()) {
|
||||||
const filtered = stops
|
return stops
|
||||||
.filter((stop) =>
|
.filter((stop) =>
|
||||||
stop.stop_name.toLowerCase().includes(inputValue.toLowerCase()),
|
stop.stop_name.toLowerCase().includes(inputValue.toLowerCase()),
|
||||||
)
|
)
|
||||||
.slice(0, 10);
|
.slice(0, 10);
|
||||||
setFilteredStops(filtered);
|
|
||||||
setShowList(true);
|
|
||||||
} else {
|
} else {
|
||||||
setFilteredStops([]);
|
return [];
|
||||||
setShowList(false);
|
|
||||||
}
|
}
|
||||||
}, [inputValue, stops]);
|
}, [inputValue, stops]);
|
||||||
|
|
||||||
|
const showList = useMemo(() => filteredStops.length > 0, [filteredStops]);
|
||||||
|
|
||||||
const handleSelect = (stop) => {
|
const handleSelect = (stop) => {
|
||||||
setInputValue(stop.stop_name);
|
setInputValue(stop.stop_name);
|
||||||
onChange({ id: stop.stop_id, name: stop.stop_name });
|
onChange({ id: stop.stop_id, name: stop.stop_name });
|
||||||
setShowList(false);
|
|
||||||
};
|
};
|
||||||
|
|
||||||
return (
|
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() {
|
function App() {
|
||||||
const [stops, setStops] = useState([]);
|
const { data: stops = [], error: stopsError } = useSWR('/api/stops', fetcher);
|
||||||
const [from, setFrom] = useState({
|
const [from, setFrom] = useState({
|
||||||
id: "740000030",
|
id: "740000030",
|
||||||
name: "Falun Centralstation",
|
name: "Falun Centralstation",
|
||||||
@@ -60,40 +59,21 @@ function App() {
|
|||||||
id: "740000001",
|
id: "740000001",
|
||||||
name: "Stockholm Centralstation",
|
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 [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 [selectedRouteIndex, setSelectedRouteIndex] = useState(0);
|
||||||
const [loading, setLoading] = useState(false);
|
|
||||||
const [error, setError] = useState("");
|
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 = () => {
|
const findRoute = () => {
|
||||||
if (!from.id || !to.id) {
|
if (!from.id || !to.id) {
|
||||||
setError("Please select both from and to stops");
|
setError("Please select both from and to stops");
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
setLoading(true);
|
|
||||||
setError("");
|
setError("");
|
||||||
fetch(
|
setRouteKey(`/api/route?from=${from.id}&to=${to.id}&when=${encodeURIComponent(time)}&num=${num}`);
|
||||||
`/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);
|
setSelectedRouteIndex(0);
|
||||||
setLoading(false);
|
|
||||||
})
|
|
||||||
.catch((err) => {
|
|
||||||
setError("Failed to find route: " + err);
|
|
||||||
setLoading(false);
|
|
||||||
});
|
|
||||||
};
|
};
|
||||||
|
|
||||||
return (
|
return (
|
||||||
@@ -142,12 +122,12 @@ function App() {
|
|||||||
<option value={10}>10</option>
|
<option value={10}>10</option>
|
||||||
</select>
|
</select>
|
||||||
</label>
|
</label>
|
||||||
<button onClick={findRoute} disabled={loading} type="button">
|
<button onClick={findRoute} disabled={isLoading} type="button">
|
||||||
{loading ? "Finding route..." : "Find Route"}
|
{isLoading ? "Finding route..." : "Find Route"}
|
||||||
</button>
|
</button>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
{error && <p className="error">{error}</p>}
|
{(error || stopsError || routesError) && <p className="error">{error || (stopsError ? "Failed to load stops: " + stopsError.message : "Failed to find route: " + routesError)}</p>}
|
||||||
{routes.length > 0 && (
|
{routes.length > 0 && (
|
||||||
<div className="routes">
|
<div className="routes">
|
||||||
<h2 className="route-title">
|
<h2 className="route-title">
|
||||||
|
|||||||
@@ -8,8 +8,6 @@ export default defineConfig({
|
|||||||
proxy: {
|
proxy: {
|
||||||
"/api": {
|
"/api": {
|
||||||
target: "http://localhost:8080",
|
target: "http://localhost:8080",
|
||||||
changeOrigin: true,
|
|
||||||
secure: false,
|
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
|
|||||||
36
go.mod
36
go.mod
@@ -3,36 +3,8 @@ module git.tornberg.me/go-gtfs
|
|||||||
go 1.25.1
|
go 1.25.1
|
||||||
|
|
||||||
require (
|
require (
|
||||||
github.com/bytedance/sonic v1.14.0 // indirect
|
github.com/davecgh/go-spew v1.1.1 // indirect
|
||||||
github.com/bytedance/sonic/loader v0.3.0 // indirect
|
github.com/pmezard/go-difflib v1.0.0 // indirect
|
||||||
github.com/cloudwego/base64x v0.1.6 // indirect
|
github.com/stretchr/testify v1.11.1 // indirect
|
||||||
github.com/gabriel-vasile/mimetype v1.4.8 // indirect
|
gopkg.in/yaml.v3 v3.0.1 // 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
|
|
||||||
)
|
)
|
||||||
|
|||||||
80
go.sum
80
go.sum
@@ -1,79 +1,9 @@
|
|||||||
github.com/bytedance/sonic v1.14.0 h1:/OfKt8HFw0kh2rj8N0F6C/qPGRESq0BbaNZgcNXXzQQ=
|
github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c=
|
||||||
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/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
|
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/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM=
|
||||||
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/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4=
|
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/stretchr/testify v1.11.1 h1:7s2iGBzp5EwR7/aIZr8ao5+dra3wiQyKjjFuvgVKu7U=
|
||||||
github.com/quic-go/qpack v0.5.1/go.mod h1:+PC4XFrEskIVkcLzpEkbLqq1uCoxPhQuvK5rH1ZgaEg=
|
github.com/stretchr/testify v1.11.1/go.mod h1:wZwfW3scLgRK+23gO65QZefKpKQRnfz6sD981Nm4B6U=
|
||||||
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=
|
|
||||||
gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
|
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=
|
gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=
|
||||||
|
|||||||
@@ -95,7 +95,7 @@ func ParseCalendarDates(r io.Reader, callback func(types.CalendarDate)) error {
|
|||||||
return nil
|
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)
|
reader := csv.NewReader(r)
|
||||||
if _, err := reader.Read(); err != nil {
|
if _, err := reader.Read(); err != nil {
|
||||||
return err
|
return err
|
||||||
@@ -109,13 +109,13 @@ func ParseFeedInfos(r io.Reader, callback func(types.FeedInfo)) error {
|
|||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
feedInfo := types.FeedInfo{
|
feedInfo := types.FeedInfo{
|
||||||
FeedID: record[0],
|
//FeedID: record[0],
|
||||||
FeedPublisherName: record[1],
|
PublisherName: record[1],
|
||||||
FeedPublisherURL: record[2],
|
PublisherURL: record[2],
|
||||||
FeedLang: record[3],
|
Language: record[3],
|
||||||
FeedVersion: record[4],
|
Version: record[4],
|
||||||
}
|
}
|
||||||
callback(feedInfo)
|
callback(record[0], feedInfo)
|
||||||
}
|
}
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
@@ -135,6 +135,7 @@ func ParseRoutes(r io.Reader, callback func(types.Route)) error {
|
|||||||
}
|
}
|
||||||
routeType, _ := strconv.Atoi(record[4])
|
routeType, _ := strconv.Atoi(record[4])
|
||||||
route := types.Route{
|
route := types.Route{
|
||||||
|
Trips: make([]*types.Trip, 0),
|
||||||
RouteID: record[0],
|
RouteID: record[0],
|
||||||
AgencyID: record[1],
|
AgencyID: record[1],
|
||||||
RouteShortName: record[2],
|
RouteShortName: record[2],
|
||||||
@@ -147,7 +148,7 @@ func ParseRoutes(r io.Reader, callback func(types.Route)) error {
|
|||||||
return nil
|
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)
|
reader := csv.NewReader(r)
|
||||||
if _, err := reader.Read(); err != nil {
|
if _, err := reader.Read(); err != nil {
|
||||||
return err
|
return err
|
||||||
@@ -164,10 +165,10 @@ func ParseStopTimes(r io.Reader, callback func(types.StopTime)) error {
|
|||||||
pickupType, _ := strconv.Atoi(record[5])
|
pickupType, _ := strconv.Atoi(record[5])
|
||||||
dropOffType, _ := strconv.Atoi(record[6])
|
dropOffType, _ := strconv.Atoi(record[6])
|
||||||
stopTime := types.StopTime{
|
stopTime := types.StopTime{
|
||||||
TripID: record[0],
|
TripId: record[0],
|
||||||
ArrivalTime: record[1],
|
ArrivalTime: types.ParseTimeString(record[1]),
|
||||||
DepartureTime: record[2],
|
DepartureTime: types.ParseTimeString(record[2]),
|
||||||
StopID: record[3],
|
StopId: record[3],
|
||||||
StopSequence: stopSequence,
|
StopSequence: stopSequence,
|
||||||
PickupType: pickupType,
|
PickupType: pickupType,
|
||||||
DropOffType: dropOffType,
|
DropOffType: dropOffType,
|
||||||
@@ -194,7 +195,9 @@ func ParseStops(r io.Reader, callback func(types.Stop)) error {
|
|||||||
stopLon, _ := strconv.ParseFloat(record[3], 64)
|
stopLon, _ := strconv.ParseFloat(record[3], 64)
|
||||||
locationType, _ := strconv.Atoi(record[4])
|
locationType, _ := strconv.Atoi(record[4])
|
||||||
stop := types.Stop{
|
stop := types.Stop{
|
||||||
StopID: record[0],
|
Trips: make(map[string]*types.Trip),
|
||||||
|
Transfers: make([]*types.Transfer, 0),
|
||||||
|
StopId: record[0],
|
||||||
StopName: record[1],
|
StopName: record[1],
|
||||||
StopLat: stopLat,
|
StopLat: stopLat,
|
||||||
StopLon: stopLon,
|
StopLon: stopLon,
|
||||||
@@ -221,12 +224,12 @@ func ParseTransfers(r io.Reader, callback func(types.Transfer)) error {
|
|||||||
transferType, _ := strconv.Atoi(record[2])
|
transferType, _ := strconv.Atoi(record[2])
|
||||||
minTransferTime, _ := strconv.Atoi(record[3])
|
minTransferTime, _ := strconv.Atoi(record[3])
|
||||||
transfer := types.Transfer{
|
transfer := types.Transfer{
|
||||||
FromStopID: record[0],
|
FromStopId: record[0],
|
||||||
ToStopID: record[1],
|
ToStopId: record[1],
|
||||||
TransferType: transferType,
|
TransferType: transferType,
|
||||||
MinTransferTime: minTransferTime,
|
MinTransferTime: minTransferTime,
|
||||||
FromTripID: record[4],
|
FromTripId: record[4],
|
||||||
ToTripID: record[5],
|
ToTripId: record[5],
|
||||||
}
|
}
|
||||||
callback(transfer)
|
callback(transfer)
|
||||||
}
|
}
|
||||||
@@ -247,9 +250,9 @@ func ParseTrips(r io.Reader, callback func(types.Trip)) error {
|
|||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
trip := types.Trip{
|
trip := types.Trip{
|
||||||
RouteID: record[0],
|
RouteId: record[0],
|
||||||
ServiceID: record[1],
|
ServiceId: record[1],
|
||||||
TripID: record[2],
|
TripId: record[2],
|
||||||
TripHeadsign: record[3],
|
TripHeadsign: record[3],
|
||||||
TripShortName: record[4],
|
TripShortName: record[4],
|
||||||
}
|
}
|
||||||
|
|||||||
103
pkg/reader/loader.go
Normal file
103
pkg/reader/loader.go
Normal file
@@ -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
|
||||||
|
}
|
||||||
@@ -1,9 +1,9 @@
|
|||||||
package types
|
package types
|
||||||
|
|
||||||
type FeedInfo struct {
|
type FeedInfo struct {
|
||||||
FeedID string `json:"feed_id" csv:"feed_id"`
|
// FeedID string `json:"feed_id" csv:"feed_id"`
|
||||||
FeedPublisherName string `json:"feed_publisher_name" csv:"feed_publisher_name"`
|
PublisherName string `json:"feed_publisher_name" csv:"feed_publisher_name"`
|
||||||
FeedPublisherURL string `json:"feed_publisher_url" csv:"feed_publisher_url"`
|
PublisherURL string `json:"feed_publisher_url" csv:"feed_publisher_url"`
|
||||||
FeedLang string `json:"feed_lang" csv:"feed_lang"`
|
Language string `json:"feed_lang" csv:"feed_lang"`
|
||||||
FeedVersion string `json:"feed_version" csv:"feed_version"`
|
Version string `json:"feed_version" csv:"feed_version"`
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,17 +1,18 @@
|
|||||||
package types
|
package types
|
||||||
|
|
||||||
import (
|
import (
|
||||||
|
"iter"
|
||||||
"time"
|
"time"
|
||||||
)
|
)
|
||||||
|
|
||||||
type Stop struct {
|
type Stop struct {
|
||||||
trips map[string]*Trip
|
Trips map[string]*Trip `json:"-" csv:"-"`
|
||||||
StopID string `json:"stop_id" csv:"stop_id"`
|
StopId string `json:"stop_id" csv:"stop_id"`
|
||||||
StopName string `json:"stop_name" csv:"stop_name"`
|
StopName string `json:"stop_name" csv:"stop_name"`
|
||||||
StopLat float64 `json:"stop_lat" csv:"stop_lat"`
|
StopLat float64 `json:"stop_lat" csv:"stop_lat"`
|
||||||
StopLon float64 `json:"stop_lon" csv:"stop_lon"`
|
StopLon float64 `json:"stop_lon" csv:"stop_lon"`
|
||||||
LocationType int `json:"location_type" csv:"location_type"`
|
LocationType int `json:"location_type" csv:"location_type"`
|
||||||
Transfers []*Transfer `json:"transfers" csv:"transfers"`
|
Transfers []*Transfer `json:"-" csv:"transfers"`
|
||||||
}
|
}
|
||||||
|
|
||||||
func (s *Stop) AddTransfer(transfer *Transfer) {
|
func (s *Stop) AddTransfer(transfer *Transfer) {
|
||||||
@@ -22,21 +23,71 @@ func (s *Stop) AddTransfer(transfer *Transfer) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
func (s *Stop) AddTrip(trip *Trip) {
|
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 {
|
type TripWithDepartureTime struct {
|
||||||
var trips []*Trip
|
*Trip
|
||||||
for _, trip := range s.trips {
|
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 {
|
for _, stop := range trip.Stops {
|
||||||
if stop.StopID == s.StopID && stop.DepartsAfter(time) {
|
if stop.StopId == s.StopId && stop.ArrivalTime >= startAfterMidnight {
|
||||||
trips = append(trips, trip)
|
|
||||||
|
if !yield(&TripWithDepartureTime{Trip: trip, DepartureTime: stop.DepartureTime}) {
|
||||||
|
return
|
||||||
|
}
|
||||||
break
|
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
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
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
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
return trips
|
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,24 +1,34 @@
|
|||||||
package types
|
package types
|
||||||
|
|
||||||
import (
|
import (
|
||||||
|
"fmt"
|
||||||
|
"iter"
|
||||||
"strconv"
|
"strconv"
|
||||||
"strings"
|
"strings"
|
||||||
"time"
|
"time"
|
||||||
)
|
)
|
||||||
|
|
||||||
|
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)
|
||||||
|
}
|
||||||
|
|
||||||
type StopTime struct {
|
type StopTime struct {
|
||||||
departureTime int
|
|
||||||
Stop *Stop `json:"stop"`
|
Stop *Stop `json:"stop"`
|
||||||
TripID string `json:"trip_id" csv:"trip_id"`
|
TripId string `json:"trip_id" csv:"trip_id"`
|
||||||
ArrivalTime string `json:"arrival_time" csv:"arrival_time"`
|
ArrivalTime SecondsAfterMidnight `json:"arrival_time" csv:"arrival_time"`
|
||||||
DepartureTime string `json:"departure_time" csv:"departure_time"`
|
DepartureTime SecondsAfterMidnight `json:"departure_time" csv:"departure_time"`
|
||||||
StopID string `json:"stop_id" csv:"stop_id"`
|
StopId string `json:"stop_id" csv:"stop_id"`
|
||||||
StopSequence int `json:"stop_sequence" csv:"stop_sequence"`
|
StopSequence int `json:"stop_sequence" csv:"stop_sequence"`
|
||||||
PickupType int `json:"pickup_type" csv:"pickup_type"`
|
PickupType int `json:"pickup_type" csv:"pickup_type"`
|
||||||
DropOffType int `json:"drop_off_type" csv:"drop_off_type"`
|
DropOffType int `json:"drop_off_type" csv:"drop_off_type"`
|
||||||
}
|
}
|
||||||
|
|
||||||
func parseTime(s string) int {
|
func ParseTimeString(s string) SecondsAfterMidnight {
|
||||||
parts := strings.Split(s, ":")
|
parts := strings.Split(s, ":")
|
||||||
if len(parts) != 3 {
|
if len(parts) != 3 {
|
||||||
return 0
|
return 0
|
||||||
@@ -26,37 +36,29 @@ func parseTime(s string) int {
|
|||||||
h, _ := strconv.Atoi(parts[0])
|
h, _ := strconv.Atoi(parts[0])
|
||||||
m, _ := strconv.Atoi(parts[1])
|
m, _ := strconv.Atoi(parts[1])
|
||||||
sec, _ := strconv.Atoi(parts[2])
|
sec, _ := strconv.Atoi(parts[2])
|
||||||
return h*3600 + m*60 + sec
|
return SecondsAfterMidnight(h*3600 + m*60 + sec)
|
||||||
}
|
}
|
||||||
|
|
||||||
func (st *StopTime) DepartTimeAsSeconds() int {
|
func AsSecondsAfterMidnight(when time.Time) SecondsAfterMidnight {
|
||||||
if st.departureTime > 0 {
|
return SecondsAfterMidnight(when.Hour()*3600 + when.Minute()*60 + when.Second())
|
||||||
return st.departureTime
|
|
||||||
}
|
|
||||||
if st.DepartureTime == "" {
|
|
||||||
return 0
|
|
||||||
}
|
|
||||||
|
|
||||||
st.departureTime = parseTime(st.DepartureTime)
|
|
||||||
return st.departureTime
|
|
||||||
}
|
}
|
||||||
|
|
||||||
func (st *StopTime) DepartsAfter(when time.Time) bool {
|
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] {
|
func (st *StopTime) GetPossibleTrips() iter.Seq[*Trip] {
|
||||||
// return func(yield func(*Trip) bool) {
|
return func(yield func(*Trip) bool) {
|
||||||
// for _, trip := range st.Stop.trips {
|
for _, trip := range st.Stop.Trips {
|
||||||
// if trip.TripID != st.TripID {
|
if trip.TripId != st.TripId {
|
||||||
// if !yield(trip) {
|
if !yield(trip) {
|
||||||
// return
|
return
|
||||||
// }
|
}
|
||||||
// }
|
}
|
||||||
// }
|
}
|
||||||
// }
|
}
|
||||||
// }
|
}
|
||||||
|
|
||||||
func (st *StopTime) SetStop(stop *Stop) {
|
func (st *StopTime) SetStop(stop *Stop) {
|
||||||
st.Stop = stop
|
st.Stop = stop
|
||||||
|
|||||||
@@ -1,10 +1,10 @@
|
|||||||
package types
|
package types
|
||||||
|
|
||||||
type Transfer struct {
|
type Transfer struct {
|
||||||
FromStopID string `json:"from_stop_id" csv:"from_stop_id"`
|
FromStopId string `json:"from_stop_id" csv:"from_stop_id"`
|
||||||
ToStopID string `json:"to_stop_id" csv:"to_stop_id"`
|
ToStopId string `json:"to_stop_id" csv:"to_stop_id"`
|
||||||
TransferType int `json:"transfer_type" csv:"transfer_type"`
|
TransferType int `json:"transfer_type" csv:"transfer_type"`
|
||||||
MinTransferTime int `json:"min_transfer_time" csv:"min_transfer_time"`
|
MinTransferTime int `json:"min_transfer_time" csv:"min_transfer_time"`
|
||||||
FromTripID string `json:"from_trip_id" csv:"from_trip_id"`
|
FromTripId string `json:"from_trip_id" csv:"from_trip_id"`
|
||||||
ToTripID string `json:"to_trip_id" csv:"to_trip_id"`
|
ToTripId string `json:"to_trip_id" csv:"to_trip_id"`
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -8,9 +8,9 @@ import (
|
|||||||
type Trip struct {
|
type Trip struct {
|
||||||
*Route
|
*Route
|
||||||
Stops []*StopTime `json:"stops" csv:"stops"`
|
Stops []*StopTime `json:"stops" csv:"stops"`
|
||||||
RouteID string `json:"route_id" csv:"route_id"`
|
RouteId string `json:"route_id" csv:"route_id"`
|
||||||
ServiceID string `json:"service_id" csv:"service_id"`
|
ServiceId string `json:"service_id" csv:"service_id"`
|
||||||
TripID string `json:"trip_id" csv:"trip_id"`
|
TripId string `json:"trip_id" csv:"trip_id"`
|
||||||
TripHeadsign string `json:"trip_headsign" csv:"trip_headsign"`
|
TripHeadsign string `json:"trip_headsign" csv:"trip_headsign"`
|
||||||
TripShortName string `json:"trip_short_name" csv:"trip_short_name"`
|
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
|
started := false
|
||||||
for _, st := range t.Stops {
|
for _, st := range t.Stops {
|
||||||
if !started {
|
if !started {
|
||||||
if st.StopID == stop.StopID && st.PickupType == 0 {
|
if st.StopId == stop.StopId && st.PickupType == 0 {
|
||||||
started = true
|
started = true
|
||||||
}
|
}
|
||||||
continue
|
continue
|
||||||
@@ -44,3 +44,12 @@ func (t *Trip) AddStopTime(stopTime *StopTime) {
|
|||||||
}
|
}
|
||||||
t.Stops = append(t.Stops, 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
|
||||||
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user