some custom stuff
This commit is contained in:
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
|
||||
|
||||
import (
|
||||
"container/heap"
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
"log"
|
||||
"net/http"
|
||||
"os"
|
||||
"sort"
|
||||
"strconv"
|
||||
"sync"
|
||||
"time"
|
||||
|
||||
"git.tornberg.me/go-gtfs/pkg/reader"
|
||||
"git.tornberg.me/go-gtfs/pkg/types"
|
||||
)
|
||||
|
||||
// TripPlanner handles preprocessed transit data for efficient routing
|
||||
type TripPlanner struct {
|
||||
stops map[string]*types.Stop
|
||||
trips map[string]*types.Trip
|
||||
routes map[string]*types.Route
|
||||
agencies map[string]*types.Agency
|
||||
transfers []types.Transfer
|
||||
stopTimes map[string][]types.StopTime
|
||||
graph map[string][]Edge
|
||||
weightedStops map[string][]Connection
|
||||
}
|
||||
|
||||
type StopWithPossibleConnections struct {
|
||||
*types.Stop
|
||||
PossibleConnections []Connection
|
||||
}
|
||||
|
||||
type Connection struct {
|
||||
*types.Stop
|
||||
Distance float64
|
||||
Time time.Duration
|
||||
}
|
||||
|
||||
const (
|
||||
transferPenalty = 90 * time.Minute
|
||||
maxTransfers = 4
|
||||
maxWaitBetweenTrips = 1 * time.Hour
|
||||
//trajectoryAngleTolerance = 220.0
|
||||
maxTravelDuration = 12 * time.Hour
|
||||
maxDetourFactor = 2
|
||||
)
|
||||
|
||||
// NewTripPlanner creates a new trip planner instance
|
||||
func NewTripPlanner() *TripPlanner {
|
||||
return &TripPlanner{
|
||||
stops: make(map[string]*types.Stop),
|
||||
trips: make(map[string]*types.Trip),
|
||||
routes: make(map[string]*types.Route),
|
||||
agencies: make(map[string]*types.Agency),
|
||||
stopTimes: make(map[string][]types.StopTime),
|
||||
graph: make(map[string][]Edge),
|
||||
weightedStops: make(map[string][]Connection),
|
||||
}
|
||||
}
|
||||
|
||||
// LoadData loads all GTFS data
|
||||
func (tp *TripPlanner) LoadData(dataDir string) error {
|
||||
files := []string{"agency", "routes", "stops", "trips", "stop_times", "transfers"}
|
||||
for _, file := range files {
|
||||
|
||||
f, err := os.Open(dataDir + "/" + file + ".txt")
|
||||
if err != nil {
|
||||
log.Fatalf("failed to open %s: %v", file, err)
|
||||
}
|
||||
|
||||
switch file {
|
||||
case "agency":
|
||||
err = reader.ParseAgencies(f, func(a types.Agency) {
|
||||
tp.agencies[a.AgencyID] = &a
|
||||
})
|
||||
case "routes":
|
||||
err = reader.ParseRoutes(f, func(r types.Route) {
|
||||
tp.routes[r.RouteID] = &r
|
||||
if ag, ok := tp.agencies[r.AgencyID]; ok {
|
||||
r.Agency = ag
|
||||
ag.AddRoute(&r)
|
||||
}
|
||||
})
|
||||
case "stops":
|
||||
err = reader.ParseStops(f, func(s types.Stop) {
|
||||
tp.stops[s.StopID] = &s
|
||||
})
|
||||
case "trips":
|
||||
err = reader.ParseTrips(f, func(t types.Trip) {
|
||||
trip := t
|
||||
if route, ok := tp.routes[trip.RouteID]; ok {
|
||||
trip.SetRoute(route)
|
||||
route.AddTrip(&trip)
|
||||
} else {
|
||||
log.Printf("route %s not found", trip.RouteID)
|
||||
}
|
||||
if agency, ok := tp.agencies[trip.AgencyID]; ok {
|
||||
trip.Agency = agency
|
||||
} else {
|
||||
log.Printf("agency %s not found", trip.AgencyID)
|
||||
}
|
||||
tp.trips[trip.TripID] = &trip
|
||||
})
|
||||
case "stop_times":
|
||||
err = reader.ParseStopTimes(f, func(st types.StopTime) {
|
||||
stop, ok := tp.stops[st.StopID]
|
||||
if ok {
|
||||
st.SetStop(stop)
|
||||
} else {
|
||||
log.Printf("stop %s not found", st.StopID)
|
||||
|
||||
}
|
||||
trp, ok := tp.trips[st.TripID]
|
||||
if !ok {
|
||||
log.Printf("trip %s not found", st.TripID)
|
||||
|
||||
} else {
|
||||
stop.AddTrip(trp)
|
||||
trp.AddStopTime(&st)
|
||||
}
|
||||
|
||||
})
|
||||
case "transfers":
|
||||
err = reader.ParseTransfers(f, func(tr types.Transfer) {
|
||||
tp.transfers = append(tp.transfers, tr)
|
||||
stop, ok := tp.stops[tr.FromStopID]
|
||||
if ok {
|
||||
stop.AddTransfer(&tr)
|
||||
} else {
|
||||
log.Printf("stop %s not found for transfer", tr.FromStopID)
|
||||
}
|
||||
|
||||
})
|
||||
}
|
||||
if err != nil {
|
||||
log.Printf("failed to parse %s: %v", file, err)
|
||||
}
|
||||
f.Close()
|
||||
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
// Preprocess builds the routing graph and precomputes routes
|
||||
func (tp *TripPlanner) Preprocess() error {
|
||||
|
||||
if hosjo, ok := tp.stops["740025287"]; ok {
|
||||
trips := hosjo.GetTripsAfter(time.Now())
|
||||
for _, trip := range trips {
|
||||
log.Printf("Trip %s (%s):", trip.TripShortName, trip.TripHeadsign)
|
||||
for stop := range trip.GetDirectPossibleDestinations(hosjo, time.Now()) {
|
||||
log.Printf("- Stop %s at %s", stop.Stop.StopName, stop.DepartureTime)
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
// Build graph with trip edges
|
||||
for tripID, sts := range tp.stopTimes {
|
||||
sort.Slice(sts, func(i, j int) bool {
|
||||
return sts[i].StopSequence < sts[j].StopSequence
|
||||
})
|
||||
for i := 0; i < len(sts)-1; i++ {
|
||||
from := sts[i].StopID
|
||||
to := sts[i+1].StopID
|
||||
departure := parseTime(sts[i].DepartureTime)
|
||||
arrival := parseTime(sts[i+1].ArrivalTime)
|
||||
timeDiff := arrival - departure
|
||||
if timeDiff > 0 {
|
||||
tp.graph[from] = append(tp.graph[from], Edge{To: to, TripID: tripID, Time: timeDiff, DepartureTime: departure})
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Add transfer edges
|
||||
for _, tr := range tp.transfers {
|
||||
if tr.TransferType == 2 { // minimum transfer time
|
||||
tp.graph[tr.FromStopID] = append(tp.graph[tr.FromStopID], Edge{
|
||||
To: tr.ToStopID,
|
||||
TripID: "transfer",
|
||||
Time: float64(tr.MinTransferTime),
|
||||
DepartureTime: 0,
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
tp.stopTimes = nil
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
// FindRoute finds the best route between two stops starting at the given time
|
||||
func (tp *TripPlanner) FindRoute(from, to string, when time.Time) *Route {
|
||||
routes := tp.FindRoutes(from, to, when, 1)
|
||||
if len(routes) > 0 {
|
||||
return routes[0]
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
// FindRoutes finds the best routes (up to num) between two stops starting at the given time
|
||||
func (tp *TripPlanner) FindRoutes(from, to string, when time.Time, num int) []*Route {
|
||||
var allRoutes []*Route
|
||||
seen := make(map[string]bool) // to avoid duplicates based on departure and arrival times
|
||||
for i := 0; i < num*20 && len(allRoutes) < num; i++ {
|
||||
route := tp.findRoute(from, to, when.Add(time.Duration(i*5)*time.Minute))
|
||||
if route != nil {
|
||||
key := fmt.Sprintf("%d-%d", route.Legs[0].DepartureTime.Unix(), route.Legs[len(route.Legs)-1].ArrivalTime.Unix())
|
||||
if !seen[key] {
|
||||
seen[key] = true
|
||||
allRoutes = append(allRoutes, route)
|
||||
log.Printf("Found route %d: departure %v, arrival %v, duration %v", len(allRoutes), route.Legs[0].DepartureTime, route.Legs[len(route.Legs)-1].ArrivalTime, route.Duration())
|
||||
}
|
||||
}
|
||||
}
|
||||
log.Printf("Total routes found: %d", len(allRoutes))
|
||||
|
||||
return allRoutes
|
||||
}
|
||||
|
||||
// findRoute implements a time-aware Dijkstra algorithm for routing
|
||||
func (tp *TripPlanner) findRoute(start, end string, when time.Time) *Route {
|
||||
startStop := tp.GetStop(start)
|
||||
if startStop == nil {
|
||||
return nil
|
||||
}
|
||||
goalStop := tp.GetStop(end)
|
||||
if goalStop == nil {
|
||||
return nil
|
||||
}
|
||||
maxAllowedDistance := haversine(startStop.StopLat, startStop.StopLon, goalStop.StopLat, goalStop.StopLon) * maxDetourFactor
|
||||
|
||||
arrival := make(map[string]time.Time)
|
||||
cost := make(map[string]time.Time)
|
||||
prev := make(map[string]PathInfo)
|
||||
pq := &priorityQueue{}
|
||||
heap.Init(pq)
|
||||
|
||||
arrival[start] = when
|
||||
cost[start] = when
|
||||
prev[start] = PathInfo{Prev: "", TripID: "", DepartureTime: when, Transfers: 0, LastTrip: "", WaitDuration: 0}
|
||||
heap.Push(pq, &pqItem{Stop: start, Cost: when})
|
||||
|
||||
for pq.Len() > 0 {
|
||||
item := heap.Pop(pq).(*pqItem)
|
||||
current := item.Stop
|
||||
if storedCost, ok := cost[current]; !ok || item.Cost.After(storedCost) {
|
||||
continue
|
||||
}
|
||||
|
||||
currentArrival, ok := arrival[current]
|
||||
if !ok || currentArrival.IsZero() {
|
||||
|
||||
continue
|
||||
}
|
||||
|
||||
if current == end {
|
||||
stopsPath := []string{}
|
||||
current := end
|
||||
visited := make(map[string]bool)
|
||||
for current != "" {
|
||||
if visited[current] {
|
||||
break
|
||||
}
|
||||
visited[current] = true
|
||||
stopsPath = append([]string{current}, stopsPath...)
|
||||
if current == start {
|
||||
break
|
||||
}
|
||||
info, ok := prev[current]
|
||||
if !ok || info.Prev == "" {
|
||||
break
|
||||
}
|
||||
current = info.Prev
|
||||
}
|
||||
legs := []Leg{}
|
||||
var currentLeg *Leg
|
||||
for i := 0; i < len(stopsPath)-1; i++ {
|
||||
from := stopsPath[i]
|
||||
to := stopsPath[i+1]
|
||||
tripID := prev[to].TripID
|
||||
if tripID != "transfer" {
|
||||
if currentLeg == nil || currentLeg.TripID != tripID {
|
||||
if currentLeg != nil {
|
||||
legs = append(legs, *currentLeg)
|
||||
}
|
||||
trip := tp.GetTrip(tripID)
|
||||
route := tp.GetRoute(trip.RouteID)
|
||||
currentLeg = &Leg{
|
||||
TripID: tripID,
|
||||
From: from,
|
||||
FromStop: tp.GetStop(from),
|
||||
Trip: trip,
|
||||
Agency: tp.GetAgency(route.AgencyID),
|
||||
Route: route,
|
||||
Stops: []string{from},
|
||||
DepartureTime: prev[to].DepartureTime,
|
||||
}
|
||||
}
|
||||
currentLeg.To = to
|
||||
currentLeg.ToStop = tp.GetStop(to)
|
||||
currentLeg.Stops = append(currentLeg.Stops, to)
|
||||
currentLeg.ArrivalTime = arrival[to]
|
||||
}
|
||||
}
|
||||
if currentLeg != nil {
|
||||
currentLeg.To = stopsPath[len(stopsPath)-1]
|
||||
currentLeg.ToStop = tp.GetStop(currentLeg.To)
|
||||
currentLeg.ArrivalTime = arrival[stopsPath[len(stopsPath)-1]]
|
||||
legs = append(legs, *currentLeg)
|
||||
}
|
||||
|
||||
return &Route{Legs: legs}
|
||||
}
|
||||
|
||||
currentStop := tp.GetStop(current)
|
||||
if currentStop == nil {
|
||||
continue
|
||||
}
|
||||
//currentBearing := bearing(currentStop.StopLat, currentStop.StopLon, goalStop.StopLat, goalStop.StopLon)
|
||||
|
||||
for _, edge := range tp.graph[current] {
|
||||
if edge.To == current {
|
||||
continue
|
||||
}
|
||||
if info, ok := prev[current]; ok && info.Prev == edge.To && info.TripID == edge.TripID {
|
||||
continue
|
||||
}
|
||||
nextStop := tp.GetStop(edge.To)
|
||||
if nextStop == nil {
|
||||
continue
|
||||
}
|
||||
|
||||
distanceToGoal := haversine(nextStop.StopLat, nextStop.StopLon, goalStop.StopLat, goalStop.StopLon)
|
||||
if distanceToGoal > maxAllowedDistance {
|
||||
continue
|
||||
}
|
||||
|
||||
// if edge.TripID != "transfer" {
|
||||
// edgeBearing := bearing(currentStop.StopLat, currentStop.StopLon, nextStop.StopLat, nextStop.StopLon)
|
||||
// if angleDifference(currentBearing, edgeBearing) > trajectoryAngleTolerance {
|
||||
// continue
|
||||
// }
|
||||
// }
|
||||
|
||||
var arrivalTime time.Time
|
||||
var departureTime time.Time
|
||||
var waitDuration time.Duration
|
||||
|
||||
if edge.TripID == "transfer" {
|
||||
waitDuration = time.Duration(edge.Time) * time.Second
|
||||
if waitDuration > maxWaitBetweenTrips {
|
||||
continue
|
||||
}
|
||||
arrivalTime = currentArrival.Add(waitDuration)
|
||||
departureTime = arrivalTime
|
||||
} else {
|
||||
depSec := edge.DepartureTime
|
||||
day := currentArrival.Truncate(24 * time.Hour)
|
||||
departure := day.Add(time.Duration(depSec) * time.Second)
|
||||
if departure.Before(currentArrival) {
|
||||
departure = departure.Add(24 * time.Hour)
|
||||
}
|
||||
if departure.After(currentArrival) || departure.Equal(currentArrival) {
|
||||
arrivalTime = departure.Add(time.Duration(edge.Time) * time.Second)
|
||||
departureTime = departure
|
||||
waitDuration = departureTime.Sub(currentArrival)
|
||||
if waitDuration > maxWaitBetweenTrips {
|
||||
continue
|
||||
}
|
||||
} else {
|
||||
continue
|
||||
}
|
||||
}
|
||||
|
||||
if arrivalTime.Sub(when) > maxTravelDuration {
|
||||
continue
|
||||
}
|
||||
|
||||
currentTransfers := prev[current].Transfers
|
||||
lastTrip := prev[current].LastTrip
|
||||
newTransfers := currentTransfers
|
||||
var newLastTrip string
|
||||
if edge.TripID == "transfer" {
|
||||
newLastTrip = lastTrip
|
||||
} else {
|
||||
newLastTrip = edge.TripID
|
||||
if lastTrip != "" && lastTrip != edge.TripID {
|
||||
newTransfers++
|
||||
}
|
||||
}
|
||||
if newTransfers > maxTransfers {
|
||||
continue
|
||||
}
|
||||
|
||||
costTime := arrivalTime
|
||||
if edge.TripID != "transfer" && lastTrip != "" && lastTrip != edge.TripID {
|
||||
costTime = costTime.Add(transferPenalty)
|
||||
}
|
||||
|
||||
existingCost, hasCost := cost[edge.To]
|
||||
existingArrival := arrival[edge.To]
|
||||
existingInfo, havePrev := prev[edge.To]
|
||||
shouldRelax := !hasCost || existingCost.IsZero() || costTime.Before(existingCost)
|
||||
if !shouldRelax && costTime.Equal(existingCost) {
|
||||
if existingArrival.IsZero() || arrivalTime.Before(existingArrival) {
|
||||
shouldRelax = true
|
||||
} else if havePrev && arrivalTime.Equal(existingArrival) {
|
||||
if newTransfers < existingInfo.Transfers {
|
||||
shouldRelax = true
|
||||
} else if waitDuration < existingInfo.WaitDuration {
|
||||
shouldRelax = true
|
||||
}
|
||||
}
|
||||
}
|
||||
if shouldRelax {
|
||||
ancestor := current
|
||||
createsCycle := false
|
||||
for ancestor != "" {
|
||||
if ancestor == edge.To {
|
||||
createsCycle = true
|
||||
break
|
||||
}
|
||||
info, ok := prev[ancestor]
|
||||
if !ok {
|
||||
break
|
||||
}
|
||||
ancestor = info.Prev
|
||||
}
|
||||
if createsCycle {
|
||||
continue
|
||||
}
|
||||
arrival[edge.To] = arrivalTime
|
||||
cost[edge.To] = costTime
|
||||
prev[edge.To] = PathInfo{Prev: current, TripID: edge.TripID, DepartureTime: departureTime, Transfers: newTransfers, LastTrip: newLastTrip, WaitDuration: waitDuration}
|
||||
heap.Push(pq, &pqItem{Stop: edge.To, Cost: costTime})
|
||||
}
|
||||
}
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func (tp *TripPlanner) GetRoute(routeId string) *types.Route {
|
||||
if routeId == "" {
|
||||
return nil
|
||||
}
|
||||
route, ok := tp.routes[routeId]
|
||||
if !ok {
|
||||
return nil
|
||||
}
|
||||
return route
|
||||
}
|
||||
|
||||
func (tp *TripPlanner) GetAgency(agencyId string) *types.Agency {
|
||||
if agencyId == "" {
|
||||
return nil
|
||||
}
|
||||
agency, ok := tp.agencies[agencyId]
|
||||
if !ok {
|
||||
return nil
|
||||
}
|
||||
return agency
|
||||
}
|
||||
|
||||
func (tp *TripPlanner) GetTrip(tripId string) *types.Trip {
|
||||
if tripId == "" {
|
||||
return nil
|
||||
}
|
||||
trip, ok := tp.trips[tripId]
|
||||
if !ok {
|
||||
return nil
|
||||
}
|
||||
return trip
|
||||
}
|
||||
|
||||
func (tp *TripPlanner) GetStop(prev string) *types.Stop {
|
||||
if prev == "" {
|
||||
return nil
|
||||
}
|
||||
stop, ok := tp.stops[prev]
|
||||
if !ok {
|
||||
return nil
|
||||
}
|
||||
return stop
|
||||
}
|
||||
|
||||
type Edge struct {
|
||||
To string
|
||||
TripID string
|
||||
Time float64
|
||||
DepartureTime float64
|
||||
Time types.SecondsAfterMidnight
|
||||
DepartureTime types.SecondsAfterMidnight
|
||||
}
|
||||
|
||||
type TripDetail struct {
|
||||
@@ -501,42 +27,33 @@ type TripDetail struct {
|
||||
}
|
||||
|
||||
type Leg struct {
|
||||
From string `json:"-"`
|
||||
FromStop *types.Stop `json:"from"`
|
||||
To string `json:"-"`
|
||||
ToStop *types.Stop `json:"to"`
|
||||
TripID string `json:"-"`
|
||||
Trip *types.Trip `json:"trip"`
|
||||
Stops []string `json:"stops"`
|
||||
Agency *types.Agency `json:"agency"`
|
||||
Route *types.Route `json:"route"`
|
||||
DepartureTime time.Time `json:"departure_time"`
|
||||
ArrivalTime time.Time `json:"arrival_time"`
|
||||
From *types.StopTime `json:"start"`
|
||||
To *types.StopTime `json:"end"`
|
||||
}
|
||||
|
||||
type Route struct {
|
||||
Legs []Leg `json:"legs"`
|
||||
}
|
||||
|
||||
func (r *Route) EndTime() time.Time {
|
||||
if len(r.Legs) == 0 {
|
||||
return time.Time{}
|
||||
}
|
||||
return r.Legs[len(r.Legs)-1].ArrivalTime
|
||||
}
|
||||
|
||||
func (r *Route) StartTime() time.Time {
|
||||
if len(r.Legs) == 0 {
|
||||
return time.Time{}
|
||||
}
|
||||
return r.Legs[0].DepartureTime
|
||||
}
|
||||
|
||||
func (r *Route) Duration() time.Duration {
|
||||
func (r *Route) EndTime() types.SecondsAfterMidnight {
|
||||
if len(r.Legs) == 0 {
|
||||
return 0
|
||||
}
|
||||
return r.Legs[len(r.Legs)-1].ArrivalTime.Sub(r.Legs[0].DepartureTime)
|
||||
return r.Legs[len(r.Legs)-1].To.ArrivalTime
|
||||
}
|
||||
|
||||
func (r *Route) StartTime() types.SecondsAfterMidnight {
|
||||
if len(r.Legs) == 0 {
|
||||
return 0
|
||||
}
|
||||
return r.Legs[0].From.DepartureTime
|
||||
}
|
||||
|
||||
func (r *Route) Duration() int {
|
||||
if len(r.Legs) == 0 {
|
||||
return 0
|
||||
}
|
||||
return int(r.Legs[len(r.Legs)-1].To.ArrivalTime - r.Legs[0].From.DepartureTime)
|
||||
}
|
||||
|
||||
type PathInfo struct {
|
||||
@@ -549,36 +66,43 @@ type PathInfo struct {
|
||||
}
|
||||
|
||||
func main() {
|
||||
tp := NewTripPlanner()
|
||||
wg := &sync.WaitGroup{}
|
||||
if err := tp.LoadData("data"); err != nil {
|
||||
fmt.Printf("Failed to load data: %v\n", err)
|
||||
os.Exit(1)
|
||||
tripData, err := reader.LoadTripData("data")
|
||||
if err != nil {
|
||||
log.Fatalf("unable to load data %v", err)
|
||||
}
|
||||
wg.Wait()
|
||||
tp := NewTripPlanner(tripData)
|
||||
|
||||
if err := tp.Preprocess(); err != nil {
|
||||
fmt.Printf("Failed to preprocess data: %v\n", err)
|
||||
os.Exit(1)
|
||||
}
|
||||
|
||||
if hosjo, ok := tp.Stops["740025287"]; ok {
|
||||
trips := hosjo.GetTripsAfter(time.Now())
|
||||
for trip := range trips {
|
||||
log.Printf("Trip %s (%s):", trip.TripShortName, trip.TripHeadsign)
|
||||
for stop := range trip.GetDirectPossibleDestinations(hosjo, time.Now()) {
|
||||
log.Printf("- Stop %s at %s", stop.Stop.StopName, types.AsTime(stop.ArrivalTime))
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
http.HandleFunc("/api/stops", func(w http.ResponseWriter, r *http.Request) {
|
||||
w.Header().Set("Access-Control-Allow-Origin", "*")
|
||||
w.Header().Set("Access-Control-Allow-Methods", "GET")
|
||||
w.Header().Set("Access-Control-Allow-Headers", "Content-Type")
|
||||
|
||||
stopList := []types.Stop{}
|
||||
for _, s := range tp.stops {
|
||||
if _, hasConnections := tp.graph[s.StopID]; hasConnections {
|
||||
for _, s := range tp.Stops {
|
||||
if len(s.Trips) > 0 {
|
||||
stopList = append(stopList, *s)
|
||||
}
|
||||
}
|
||||
w.WriteHeader(http.StatusOK)
|
||||
w.Header().Set("Content-Type", "application/json")
|
||||
json.NewEncoder(w).Encode(stopList)
|
||||
})
|
||||
|
||||
http.HandleFunc("/api/route", func(w http.ResponseWriter, r *http.Request) {
|
||||
w.Header().Set("Access-Control-Allow-Origin", "*")
|
||||
w.Header().Set("Access-Control-Allow-Methods", "GET")
|
||||
w.Header().Set("Access-Control-Allow-Headers", "Content-Type")
|
||||
|
||||
from := r.URL.Query().Get("from")
|
||||
to := r.URL.Query().Get("to")
|
||||
whenStr := r.URL.Query().Get("when")
|
||||
@@ -591,7 +115,7 @@ func main() {
|
||||
}
|
||||
when := time.Now()
|
||||
if whenStr != "" {
|
||||
if parsed, err := time.Parse(time.RFC3339, whenStr); err == nil {
|
||||
if parsed, err := time.Parse(time.DateTime, whenStr); err == nil {
|
||||
when = parsed
|
||||
}
|
||||
}
|
||||
@@ -600,14 +124,17 @@ func main() {
|
||||
json.NewEncoder(w).Encode(map[string]string{"error": "from and to parameters required"})
|
||||
return
|
||||
}
|
||||
routes := tp.FindRoutes(from, to, when, num)
|
||||
if len(routes) == 0 {
|
||||
log.Printf("using num %v", num)
|
||||
w.WriteHeader(http.StatusOK)
|
||||
log.Printf("start time %v", when)
|
||||
route, err := tp.FindRoute(from, to, when)
|
||||
if err != nil {
|
||||
w.WriteHeader(http.StatusNotFound)
|
||||
json.NewEncoder(w).Encode(map[string]string{"error": "no route found"})
|
||||
return
|
||||
}
|
||||
w.Header().Set("Content-Type", "application/json")
|
||||
json.NewEncoder(w).Encode(routes)
|
||||
json.NewEncoder(w).Encode(route)
|
||||
})
|
||||
log.Printf("Listening on 8080")
|
||||
http.ListenAndServe(":8080", nil)
|
||||
|
||||
@@ -1,26 +1,32 @@
|
||||
package main
|
||||
|
||||
import (
|
||||
"os"
|
||||
"testing"
|
||||
"time"
|
||||
|
||||
"git.tornberg.me/go-gtfs/pkg/reader"
|
||||
)
|
||||
|
||||
func TestFindRoute(t *testing.T) {
|
||||
os.Chdir("../..")
|
||||
tp := NewTripPlanner()
|
||||
var tripData *reader.TripData
|
||||
|
||||
err := tp.LoadData("data")
|
||||
func init() {
|
||||
var err error
|
||||
tripData, err = reader.LoadTripData("../../data")
|
||||
if err != nil {
|
||||
t.Fatalf("Failed to load data: %v", err)
|
||||
panic("Failed to load trip data: " + err.Error())
|
||||
}
|
||||
}
|
||||
|
||||
err = tp.Preprocess()
|
||||
func TestFindRouteToStockholm(t *testing.T) {
|
||||
|
||||
tp := NewTripPlanner(tripData)
|
||||
|
||||
//tp.Preprocess()
|
||||
|
||||
route, err := tp.FindRoute("740000030", "740000001", time.Now().Add(time.Hour*-16))
|
||||
if err != nil {
|
||||
t.Fatalf("Failed to preprocess: %v", err)
|
||||
t.Fatalf("Error finding route: %v", err)
|
||||
}
|
||||
|
||||
route := tp.FindRoute("740000030", "740000001", time.Now())
|
||||
if route == nil {
|
||||
t.Fatal("No route found from Falun Centralstation to Stockholm Centralstation")
|
||||
}
|
||||
@@ -29,50 +35,17 @@ func TestFindRoute(t *testing.T) {
|
||||
t.Fatal("Route has no legs")
|
||||
}
|
||||
|
||||
stops := []string{}
|
||||
if len(route.Legs) > 0 {
|
||||
stops = append(stops, route.Legs[0].From)
|
||||
for _, leg := range route.Legs {
|
||||
stops = append(stops, leg.To)
|
||||
}
|
||||
}
|
||||
|
||||
if len(stops) < 2 {
|
||||
t.Fatal("Route path is too short")
|
||||
}
|
||||
|
||||
if stops[0] != "740000030" {
|
||||
t.Errorf("Route does not start at Falun Centralstation (740000030), starts at %s", stops[0])
|
||||
}
|
||||
|
||||
if stops[len(stops)-1] != "740000001" {
|
||||
t.Errorf("Route does not end at Stockholm Centralstation (740000001), ends at %s", stops[len(stops)-1])
|
||||
}
|
||||
|
||||
// Additional check: ensure all stops in path exist
|
||||
for _, stopID := range stops {
|
||||
if s, exists := tp.stops[stopID]; !exists {
|
||||
t.Errorf("Stop %s in path does not exist", stopID)
|
||||
} else {
|
||||
t.Logf("stop: %s", s.StopName)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func TestFindRouteToMalmo(t *testing.T) {
|
||||
tp := NewTripPlanner()
|
||||
tp := NewTripPlanner(tripData)
|
||||
|
||||
err := tp.LoadData("data")
|
||||
//tp.Preprocess()
|
||||
|
||||
route, err := tp.FindRoute("740000030", "740000003", time.Now().Add(time.Hour*-16))
|
||||
if err != nil {
|
||||
t.Fatalf("Failed to load data: %v", err)
|
||||
t.Fatalf("Error finding route: %v", err)
|
||||
}
|
||||
|
||||
err = tp.Preprocess()
|
||||
if err != nil {
|
||||
t.Fatalf("Failed to preprocess: %v", err)
|
||||
}
|
||||
|
||||
route := tp.FindRoute("740000030", "740000003", time.Now())
|
||||
if route == nil {
|
||||
t.Fatal("No route found from Falun Centralstation to Malmö Centralstation")
|
||||
}
|
||||
@@ -81,30 +54,4 @@ func TestFindRouteToMalmo(t *testing.T) {
|
||||
t.Fatal("Route has no legs")
|
||||
}
|
||||
|
||||
stops := []string{}
|
||||
if len(route.Legs) > 0 {
|
||||
stops = append(stops, route.Legs[0].From)
|
||||
for _, leg := range route.Legs {
|
||||
stops = append(stops, leg.To)
|
||||
}
|
||||
}
|
||||
|
||||
if len(stops) < 2 {
|
||||
t.Fatal("Route path is too short")
|
||||
}
|
||||
|
||||
if stops[0] != "740000030" {
|
||||
t.Errorf("Route does not start at Falun Centralstation (740000030), starts at %s", stops[0])
|
||||
}
|
||||
|
||||
if stops[len(stops)-1] != "740000003" {
|
||||
t.Errorf("Route does not end at Malmö Centralstation (740000003), ends at %s", stops[len(stops)-1])
|
||||
}
|
||||
|
||||
// Additional check: ensure all stops in path exist
|
||||
for _, stopID := range stops {
|
||||
if _, exists := tp.stops[stopID]; !exists {
|
||||
t.Errorf("Stop %s in path does not exist", stopID)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
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
|
||||
}
|
||||
Reference in New Issue
Block a user