Compare commits
3 Commits
1bebded484
...
d2b9e9c9ad
| Author | SHA1 | Date | |
|---|---|---|---|
| d2b9e9c9ad | |||
| 716bd0816c | |||
| 5dc296805a |
@@ -1,261 +0,0 @@
|
|||||||
package main
|
|
||||||
|
|
||||||
// import (
|
|
||||||
// "container/heap"
|
|
||||||
// "time"
|
|
||||||
|
|
||||||
// "git.tornberg.me/go-gtfs/pkg/types"
|
|
||||||
// )
|
|
||||||
|
|
||||||
// // CostFactors defines the parameters for the cost function in A* search.
|
|
||||||
// type CostFactors struct {
|
|
||||||
// TransferPenalty time.Duration
|
|
||||||
// MaxTransfers int
|
|
||||||
// MaxWaitBetweenTrips time.Duration
|
|
||||||
// MaxTravelDuration time.Duration
|
|
||||||
// MaxDetourFactor float64
|
|
||||||
// }
|
|
||||||
|
|
||||||
// // Heuristic function estimates the cost from a node to the goal.
|
|
||||||
// // For pathfinding on a map, this is typically the straight-line distance.
|
|
||||||
// type Heuristic func(from, to *types.Stop) time.Duration
|
|
||||||
|
|
||||||
// // AStarPlanner uses the A* algorithm to find routes.
|
|
||||||
// type AStarPlanner struct {
|
|
||||||
// *TripPlanner
|
|
||||||
// CostFactors CostFactors
|
|
||||||
// Heuristic Heuristic
|
|
||||||
// }
|
|
||||||
|
|
||||||
// // NewAStarPlanner creates a new planner that uses the A* algorithm.
|
|
||||||
// func NewAStarPlanner(tp *TripPlanner, factors CostFactors, heuristic Heuristic) *AStarPlanner {
|
|
||||||
// return &AStarPlanner{
|
|
||||||
// TripPlanner: tp,
|
|
||||||
// CostFactors: factors,
|
|
||||||
// Heuristic: heuristic,
|
|
||||||
// }
|
|
||||||
// }
|
|
||||||
|
|
||||||
// // findRoute implements a time-aware A* algorithm for routing.
|
|
||||||
// func (p *AStarPlanner) findRoute(start, end string, when time.Time) *Route {
|
|
||||||
// startStop := p.GetStop(start)
|
|
||||||
// if startStop == nil {
|
|
||||||
// return nil
|
|
||||||
// }
|
|
||||||
// goalStop := p.GetStop(end)
|
|
||||||
// if goalStop == nil {
|
|
||||||
// return nil
|
|
||||||
// }
|
|
||||||
// maxAllowedDistance := haversine(startStop.StopLat, startStop.StopLon, goalStop.StopLat, goalStop.StopLon) * p.CostFactors.MaxDetourFactor
|
|
||||||
|
|
||||||
// arrival := make(map[string]time.Time)
|
|
||||||
// cost := make(map[string]time.Time)
|
|
||||||
// prev := make(map[string]PathInfo)
|
|
||||||
// pq := &priorityQueue{}
|
|
||||||
// heap.Init(pq)
|
|
||||||
|
|
||||||
// arrival[start] = when
|
|
||||||
// cost[start] = when
|
|
||||||
// prev[start] = PathInfo{Prev: "", TripID: "", DepartureTime: when, Transfers: 0, LastTrip: "", WaitDuration: 0}
|
|
||||||
// heap.Push(pq, &pqItem{Stop: start, Cost: when})
|
|
||||||
|
|
||||||
// for pq.Len() > 0 {
|
|
||||||
// item := heap.Pop(pq).(*pqItem)
|
|
||||||
// current := item.Stop
|
|
||||||
// if storedCost, ok := cost[current]; !ok || item.Cost.After(storedCost) {
|
|
||||||
// continue
|
|
||||||
// }
|
|
||||||
|
|
||||||
// currentArrival, ok := arrival[current]
|
|
||||||
// if !ok || currentArrival.IsZero() {
|
|
||||||
// continue
|
|
||||||
// }
|
|
||||||
|
|
||||||
// if current == end {
|
|
||||||
// // Reconstruct path
|
|
||||||
// return reconstructPath(p.TripPlanner, prev, start, end, arrival)
|
|
||||||
// }
|
|
||||||
|
|
||||||
// currentStop := p.GetStop(current)
|
|
||||||
// if currentStop == nil {
|
|
||||||
// continue
|
|
||||||
// }
|
|
||||||
|
|
||||||
// for _, edge := range p.graph[current] {
|
|
||||||
// if edge.To == current {
|
|
||||||
// continue
|
|
||||||
// }
|
|
||||||
// if info, ok := prev[current]; ok && info.Prev == edge.To && info.TripID == edge.TripID {
|
|
||||||
// continue
|
|
||||||
// }
|
|
||||||
// nextStop := p.GetStop(edge.To)
|
|
||||||
// if nextStop == nil {
|
|
||||||
// continue
|
|
||||||
// }
|
|
||||||
|
|
||||||
// distanceToGoal := haversine(nextStop.StopLat, nextStop.StopLon, goalStop.StopLat, goalStop.StopLon)
|
|
||||||
// if distanceToGoal > maxAllowedDistance {
|
|
||||||
// continue
|
|
||||||
// }
|
|
||||||
|
|
||||||
// var arrivalTime time.Time
|
|
||||||
// var departureTime time.Time
|
|
||||||
// var waitDuration time.Duration
|
|
||||||
|
|
||||||
// if edge.TripID == "transfer" {
|
|
||||||
// waitDuration = time.Duration(edge.Time) * time.Second
|
|
||||||
// if waitDuration > p.CostFactors.MaxWaitBetweenTrips {
|
|
||||||
// continue
|
|
||||||
// }
|
|
||||||
// arrivalTime = currentArrival.Add(waitDuration)
|
|
||||||
// departureTime = arrivalTime
|
|
||||||
// } else {
|
|
||||||
// depSec := edge.DepartureTime
|
|
||||||
// day := currentArrival.Truncate(24 * time.Hour)
|
|
||||||
// departure := day.Add(time.Duration(depSec) * time.Second)
|
|
||||||
// if departure.Before(currentArrival) {
|
|
||||||
// departure = departure.Add(24 * time.Hour)
|
|
||||||
// }
|
|
||||||
// if departure.After(currentArrival) || departure.Equal(currentArrival) {
|
|
||||||
// arrivalTime = departure.Add(time.Duration(edge.Time) * time.Second)
|
|
||||||
// departureTime = departure
|
|
||||||
// waitDuration = departureTime.Sub(currentArrival)
|
|
||||||
// if waitDuration > p.CostFactors.MaxWaitBetweenTrips {
|
|
||||||
// continue
|
|
||||||
// }
|
|
||||||
// } else {
|
|
||||||
// continue
|
|
||||||
// }
|
|
||||||
// }
|
|
||||||
|
|
||||||
// if arrivalTime.Sub(when) > p.CostFactors.MaxTravelDuration {
|
|
||||||
// continue
|
|
||||||
// }
|
|
||||||
|
|
||||||
// currentTransfers := prev[current].Transfers
|
|
||||||
// lastTrip := prev[current].LastTrip
|
|
||||||
// newTransfers := currentTransfers
|
|
||||||
// var newLastTrip string
|
|
||||||
// if edge.TripID == "transfer" {
|
|
||||||
// newLastTrip = lastTrip
|
|
||||||
// } else {
|
|
||||||
// newLastTrip = edge.TripID
|
|
||||||
// if lastTrip != "" && lastTrip != edge.TripID {
|
|
||||||
// newTransfers++
|
|
||||||
// }
|
|
||||||
// }
|
|
||||||
// if newTransfers > p.CostFactors.MaxTransfers {
|
|
||||||
// continue
|
|
||||||
// }
|
|
||||||
|
|
||||||
// // A* cost calculation: g(n) + h(n)
|
|
||||||
// // g(n) is the actual cost from the start, which is the arrival time with penalties.
|
|
||||||
// gCost := arrivalTime
|
|
||||||
// if edge.TripID != "transfer" && lastTrip != "" && lastTrip != edge.TripID {
|
|
||||||
// gCost = gCost.Add(p.CostFactors.TransferPenalty)
|
|
||||||
// }
|
|
||||||
|
|
||||||
// // h(n) is the heuristic cost from the current node to the goal.
|
|
||||||
// hCost := p.Heuristic(nextStop, goalStop)
|
|
||||||
// fCost := gCost.Add(hCost)
|
|
||||||
|
|
||||||
// existingCost, hasCost := cost[edge.To]
|
|
||||||
// existingArrival := arrival[edge.To]
|
|
||||||
// existingInfo, havePrev := prev[edge.To]
|
|
||||||
// shouldRelax := !hasCost || existingCost.IsZero() || fCost.Before(existingCost)
|
|
||||||
// if !shouldRelax && fCost.Equal(existingCost) {
|
|
||||||
// if existingArrival.IsZero() || arrivalTime.Before(existingArrival) {
|
|
||||||
// shouldRelax = true
|
|
||||||
// } else if havePrev && arrivalTime.Equal(existingArrival) {
|
|
||||||
// if newTransfers < existingInfo.Transfers {
|
|
||||||
// shouldRelax = true
|
|
||||||
// } else if waitDuration < existingInfo.WaitDuration {
|
|
||||||
// shouldRelax = true
|
|
||||||
// }
|
|
||||||
// }
|
|
||||||
// }
|
|
||||||
// if shouldRelax {
|
|
||||||
// ancestor := current
|
|
||||||
// createsCycle := false
|
|
||||||
// for ancestor != "" {
|
|
||||||
// if ancestor == edge.To {
|
|
||||||
// createsCycle = true
|
|
||||||
// break
|
|
||||||
// }
|
|
||||||
// info, ok := prev[ancestor]
|
|
||||||
// if !ok {
|
|
||||||
// break
|
|
||||||
// }
|
|
||||||
// ancestor = info.Prev
|
|
||||||
// }
|
|
||||||
// if createsCycle {
|
|
||||||
// continue
|
|
||||||
// }
|
|
||||||
// arrival[edge.To] = arrivalTime
|
|
||||||
// cost[edge.To] = fCost
|
|
||||||
// prev[edge.To] = PathInfo{Prev: current, TripID: edge.TripID, DepartureTime: departureTime, Transfers: newTransfers, LastTrip: newLastTrip, WaitDuration: waitDuration}
|
|
||||||
// heap.Push(pq, &pqItem{Stop: edge.To, Cost: fCost})
|
|
||||||
// }
|
|
||||||
// }
|
|
||||||
// }
|
|
||||||
// return nil
|
|
||||||
// }
|
|
||||||
|
|
||||||
// func reconstructPath(tp *TripPlanner, prev map[string]PathInfo, start, end string, arrival map[string]time.Time) *Route {
|
|
||||||
// stopsPath := []string{}
|
|
||||||
// current := end
|
|
||||||
// visited := make(map[string]bool)
|
|
||||||
// for current != "" {
|
|
||||||
// if visited[current] {
|
|
||||||
// break
|
|
||||||
// }
|
|
||||||
// visited[current] = true
|
|
||||||
// stopsPath = append([]string{current}, stopsPath...)
|
|
||||||
// if current == start {
|
|
||||||
// break
|
|
||||||
// }
|
|
||||||
// info, ok := prev[current]
|
|
||||||
// if !ok || info.Prev == "" {
|
|
||||||
// break
|
|
||||||
// }
|
|
||||||
// current = info.Prev
|
|
||||||
// }
|
|
||||||
// legs := []Leg{}
|
|
||||||
// var currentLeg *Leg
|
|
||||||
// for i := 0; i < len(stopsPath)-1; i++ {
|
|
||||||
// from := stopsPath[i]
|
|
||||||
// to := stopsPath[i+1]
|
|
||||||
// tripID := prev[to].TripID
|
|
||||||
// if tripID != "transfer" {
|
|
||||||
// if currentLeg == nil || currentLeg.TripID != tripID {
|
|
||||||
// if currentLeg != nil {
|
|
||||||
// legs = append(legs, *currentLeg)
|
|
||||||
// }
|
|
||||||
// trip := tp.GetTrip(tripID)
|
|
||||||
// route := tp.GetRoute(trip.RouteId)
|
|
||||||
// currentLeg = &Leg{
|
|
||||||
// TripID: tripID,
|
|
||||||
// From: from,
|
|
||||||
// FromStop: tp.GetStop(from),
|
|
||||||
// Trip: trip,
|
|
||||||
// Agency: tp.GetAgency(route.AgencyID),
|
|
||||||
// Route: route,
|
|
||||||
// Stops: []string{from},
|
|
||||||
// DepartureTime: prev[to].DepartureTime,
|
|
||||||
// }
|
|
||||||
// }
|
|
||||||
// currentLeg.To = to
|
|
||||||
// currentLeg.ToStop = tp.GetStop(to)
|
|
||||||
// currentLeg.Stops = append(currentLeg.Stops, to)
|
|
||||||
// currentLeg.ArrivalTime = arrival[to]
|
|
||||||
// }
|
|
||||||
// }
|
|
||||||
// if currentLeg != nil {
|
|
||||||
// currentLeg.To = stopsPath[len(stopsPath)-1]
|
|
||||||
// currentLeg.ToStop = tp.GetStop(currentLeg.To)
|
|
||||||
// currentLeg.ArrivalTime = arrival[stopsPath[len(stopsPath)-1]]
|
|
||||||
// legs = append(legs, *currentLeg)
|
|
||||||
// }
|
|
||||||
|
|
||||||
// return &Route{Legs: legs}
|
|
||||||
// }
|
|
||||||
@@ -1,217 +0,0 @@
|
|||||||
package main
|
|
||||||
|
|
||||||
// import (
|
|
||||||
// "sort"
|
|
||||||
// "time"
|
|
||||||
|
|
||||||
// "git.tornberg.me/go-gtfs/pkg/reader"
|
|
||||||
// "git.tornberg.me/go-gtfs/pkg/types"
|
|
||||||
// )
|
|
||||||
|
|
||||||
// // Connection represents a single leg of a trip between two stops.
|
|
||||||
// type CSAConnection struct {
|
|
||||||
// DepartureStopID string
|
|
||||||
// ArrivalStopID string
|
|
||||||
// DepartureTime types.SecondsAfterMidnight
|
|
||||||
// ArrivalTime types.SecondsAfterMidnight
|
|
||||||
// TripID string
|
|
||||||
// }
|
|
||||||
|
|
||||||
// // CSAPlanner uses the Connection Scan Algorithm for routing.
|
|
||||||
// type CSAPlanner struct {
|
|
||||||
// *reader.TripData
|
|
||||||
// connections []CSAConnection
|
|
||||||
// }
|
|
||||||
|
|
||||||
// // NewCSAPlanner creates and preprocesses data for the Connection Scan Algorithm.
|
|
||||||
// func NewCSAPlanner(data *reader.TripData) *CSAPlanner {
|
|
||||||
// p := &CSAPlanner{
|
|
||||||
// TripData: data,
|
|
||||||
// }
|
|
||||||
// p.preprocess()
|
|
||||||
// return p
|
|
||||||
// }
|
|
||||||
|
|
||||||
// // preprocess creates a sorted list of all connections.
|
|
||||||
// func (p *CSAPlanner) preprocess() {
|
|
||||||
// p.connections = make([]CSAConnection, 0)
|
|
||||||
// for tripID, trip := range p.Trips {
|
|
||||||
// sts := trip.Stops
|
|
||||||
// sort.Slice(sts, func(i, j int) bool {
|
|
||||||
// return sts[i].StopSequence < sts[j].StopSequence
|
|
||||||
// })
|
|
||||||
// for i := 0; i < len(sts)-1; i++ {
|
|
||||||
// from := sts[i]
|
|
||||||
// to := sts[i+1]
|
|
||||||
// if from.DepartureTime < to.ArrivalTime {
|
|
||||||
// p.connections = append(p.connections, CSAConnection{
|
|
||||||
// DepartureStopID: from.StopId,
|
|
||||||
// ArrivalStopID: to.StopId,
|
|
||||||
// DepartureTime: from.DepartureTime,
|
|
||||||
// ArrivalTime: to.ArrivalTime,
|
|
||||||
// TripID: tripID,
|
|
||||||
// })
|
|
||||||
// }
|
|
||||||
// }
|
|
||||||
// }
|
|
||||||
// // Sort connections by departure time, which is crucial for the algorithm.
|
|
||||||
// sort.Slice(p.connections, func(i, j int) bool {
|
|
||||||
// return p.connections[i].DepartureTime < p.connections[j].DepartureTime
|
|
||||||
// })
|
|
||||||
// }
|
|
||||||
|
|
||||||
// // FindRoute finds the best route using the Connection Scan Algorithm.
|
|
||||||
// func (p *CSAPlanner) FindRoute(startStopID, endStopID string, when time.Time) *Route {
|
|
||||||
// earliestArrival := make(map[string]time.Time)
|
|
||||||
// journeyPointers := make(map[string]CSAConnection) // To reconstruct the path
|
|
||||||
|
|
||||||
// startTime := types.AsSecondsAfterMidnight(when)
|
|
||||||
// day := when.Truncate(24 * time.Hour)
|
|
||||||
|
|
||||||
// // Initialize earliest arrival times
|
|
||||||
// for stopID := range p.Stops {
|
|
||||||
// earliestArrival[stopID] = time.Time{} // Zero time represents infinity
|
|
||||||
// }
|
|
||||||
// earliestArrival[startStopID] = when
|
|
||||||
|
|
||||||
// // Find the starting point in the connections array
|
|
||||||
// firstConnectionIdx := sort.Search(len(p.connections), func(i int) bool {
|
|
||||||
// return p.connections[i].DepartureTime >= startTime
|
|
||||||
// })
|
|
||||||
|
|
||||||
// // Scan through connections
|
|
||||||
// for i := firstConnectionIdx; i < len(p.connections); i++ {
|
|
||||||
// conn := p.connections[i]
|
|
||||||
|
|
||||||
// depStopArrival, reachable := earliestArrival[conn.DepartureStopID]
|
|
||||||
// if !reachable || depStopArrival.IsZero() {
|
|
||||||
// continue // Cannot reach the departure stop of this connection yet
|
|
||||||
// }
|
|
||||||
|
|
||||||
// connDepartureTime := day.Add(time.Duration(conn.DepartureTime) * time.Second)
|
|
||||||
// if connDepartureTime.Before(depStopArrival) {
|
|
||||||
// connDepartureTime = connDepartureTime.Add(24 * time.Hour) // Next day
|
|
||||||
// }
|
|
||||||
|
|
||||||
// if !depStopArrival.IsZero() && connDepartureTime.After(depStopArrival) {
|
|
||||||
// // We can catch this connection
|
|
||||||
// connArrivalTime := day.Add(time.Duration(conn.ArrivalTime) * time.Second)
|
|
||||||
// if connArrivalTime.Before(connDepartureTime) {
|
|
||||||
// connArrivalTime = connArrivalTime.Add(24 * time.Hour)
|
|
||||||
// }
|
|
||||||
|
|
||||||
// // Check if this connection offers a better arrival time at the destination stop
|
|
||||||
// currentBestArrival, hasArrival := earliestArrival[conn.ArrivalStopID]
|
|
||||||
// if !hasArrival || currentBestArrival.IsZero() || connArrivalTime.Before(currentBestArrival) {
|
|
||||||
// earliestArrival[conn.ArrivalStopID] = connArrivalTime
|
|
||||||
// journeyPointers[conn.ArrivalStopID] = conn
|
|
||||||
// }
|
|
||||||
// }
|
|
||||||
// }
|
|
||||||
|
|
||||||
// // Reconstruct the path if the destination was reached
|
|
||||||
// if _, ok := journeyPointers[endStopID]; !ok {
|
|
||||||
// return nil // No path found
|
|
||||||
// }
|
|
||||||
|
|
||||||
// return p.reconstructCSAPath(startStopID, endStopID, journeyPointers)
|
|
||||||
// }
|
|
||||||
|
|
||||||
// // reconstructCSAPath builds the route from the journey pointers.
|
|
||||||
// func (p *CSAPlanner) reconstructCSAPath(startStopID, endStopID string, pointers map[string]CSAConnection) *Route {
|
|
||||||
// var path []CSAConnection
|
|
||||||
// currentStopID := endStopID
|
|
||||||
// for currentStopID != startStopID {
|
|
||||||
// conn, ok := pointers[currentStopID]
|
|
||||||
// if !ok {
|
|
||||||
// break // Should not happen if a path was found
|
|
||||||
// }
|
|
||||||
// path = append([]CSAConnection{conn}, path...)
|
|
||||||
// currentStopID = conn.DepartureStopID
|
|
||||||
// }
|
|
||||||
|
|
||||||
// if len(path) == 0 {
|
|
||||||
// return nil
|
|
||||||
// }
|
|
||||||
|
|
||||||
// // Group connections into legs
|
|
||||||
// var legs []Leg
|
|
||||||
// if len(path) > 0 {
|
|
||||||
// currentLeg := p.connectionToLeg(path[0])
|
|
||||||
// for i := 1; i < len(path); i++ {
|
|
||||||
// if path[i].TripID == currentLeg.TripID {
|
|
||||||
// // Continue the current leg
|
|
||||||
// currentLeg.To = path[i].ArrivalStopID
|
|
||||||
// currentLeg.ToStop = p.GetStop(currentLeg.To)
|
|
||||||
// currentLeg.Stops = append(currentLeg.Stops, currentLeg.To)
|
|
||||||
// } else {
|
|
||||||
// // New leg
|
|
||||||
// legs = append(legs, *currentLeg)
|
|
||||||
// currentLeg = p.connectionToLeg(path[i])
|
|
||||||
// }
|
|
||||||
// }
|
|
||||||
// legs = append(legs, *currentLeg)
|
|
||||||
// }
|
|
||||||
|
|
||||||
// return &Route{Legs: legs}
|
|
||||||
// }
|
|
||||||
|
|
||||||
// func (p *CSAPlanner) connectionToLeg(conn CSAConnection) *Leg {
|
|
||||||
// trip := p.GetTrip(conn.TripID)
|
|
||||||
// route := p.GetRoute(trip.RouteId)
|
|
||||||
// return &Leg{
|
|
||||||
// TripID: conn.TripID,
|
|
||||||
// From: conn.DepartureStopID,
|
|
||||||
// To: conn.ArrivalStopID,
|
|
||||||
// FromStop: p.GetStop(conn.DepartureStopID),
|
|
||||||
// ToStop: p.GetStop(conn.ArrivalStopID),
|
|
||||||
// Trip: trip,
|
|
||||||
// Agency: p.GetAgency(route.AgencyID),
|
|
||||||
// Route: route,
|
|
||||||
// Stops: []string{conn.DepartureStopID, conn.ArrivalStopID},
|
|
||||||
// }
|
|
||||||
// }
|
|
||||||
|
|
||||||
// func (p *CSAPlanner) GetRoute(routeId string) *types.Route {
|
|
||||||
// if routeId == "" {
|
|
||||||
// return nil
|
|
||||||
// }
|
|
||||||
// route, ok := p.Routes[routeId]
|
|
||||||
// if !ok {
|
|
||||||
// return nil
|
|
||||||
// }
|
|
||||||
// return route
|
|
||||||
// }
|
|
||||||
|
|
||||||
// func (p *CSAPlanner) GetAgency(agencyId string) *types.Agency {
|
|
||||||
// if agencyId == "" {
|
|
||||||
// return nil
|
|
||||||
// }
|
|
||||||
// agency, ok := p.Agencies[agencyId]
|
|
||||||
// if !ok {
|
|
||||||
// return nil
|
|
||||||
// }
|
|
||||||
// return agency
|
|
||||||
// }
|
|
||||||
|
|
||||||
// func (p *CSAPlanner) GetTrip(tripId string) *types.Trip {
|
|
||||||
// if tripId == "" {
|
|
||||||
// return nil
|
|
||||||
// }
|
|
||||||
// trip, ok := p.Trips[tripId]
|
|
||||||
// if !ok {
|
|
||||||
// return nil
|
|
||||||
// }
|
|
||||||
// return trip
|
|
||||||
// }
|
|
||||||
|
|
||||||
// func (p *CSAPlanner) GetStop(stopID string) *types.Stop {
|
|
||||||
// if stopID == "" {
|
|
||||||
// return nil
|
|
||||||
// }
|
|
||||||
// stop, ok := p.Stops[stopID]
|
|
||||||
// if !ok {
|
|
||||||
// return nil
|
|
||||||
// }
|
|
||||||
// return stop
|
|
||||||
// }
|
|
||||||
@@ -26,8 +26,17 @@ type TripDetail struct {
|
|||||||
Stops []string `json:"stops"`
|
Stops []string `json:"stops"`
|
||||||
}
|
}
|
||||||
|
|
||||||
|
type JSONTrip struct {
|
||||||
|
TripId string `json:"trip_id"`
|
||||||
|
RouteId string `json:"route_id"`
|
||||||
|
AgencyName string `json:"agency_name"`
|
||||||
|
TripHeadsign string `json:"trip_headsign"`
|
||||||
|
TripShortName string `json:"trip_short_name"`
|
||||||
|
}
|
||||||
|
|
||||||
type Leg struct {
|
type Leg struct {
|
||||||
From *types.StopTime `json:"start"`
|
From *types.StopTime `json:"start"`
|
||||||
|
Trip *JSONTrip `json:"trip"`
|
||||||
To *types.StopTime `json:"end"`
|
To *types.StopTime `json:"end"`
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -101,6 +110,68 @@ func main() {
|
|||||||
json.NewEncoder(w).Encode(stopList)
|
json.NewEncoder(w).Encode(stopList)
|
||||||
})
|
})
|
||||||
|
|
||||||
|
http.HandleFunc("/api/trips", func(w http.ResponseWriter, r *http.Request) {
|
||||||
|
from := r.URL.Query().Get("from")
|
||||||
|
whenStr := r.URL.Query().Get("when")
|
||||||
|
if from == "" {
|
||||||
|
w.WriteHeader(http.StatusBadRequest)
|
||||||
|
json.NewEncoder(w).Encode(map[string]string{"error": "from parameter required"})
|
||||||
|
return
|
||||||
|
}
|
||||||
|
stop, ok := tp.Stops[from]
|
||||||
|
if !ok {
|
||||||
|
w.WriteHeader(http.StatusNotFound)
|
||||||
|
json.NewEncoder(w).Encode(map[string]string{"error": "stop not found"})
|
||||||
|
return
|
||||||
|
}
|
||||||
|
when := time.Now()
|
||||||
|
if whenStr != "" {
|
||||||
|
var err error
|
||||||
|
when, err = time.Parse(time.RFC3339, whenStr)
|
||||||
|
if err != nil {
|
||||||
|
w.WriteHeader(http.StatusBadRequest)
|
||||||
|
json.NewEncoder(w).Encode(map[string]string{"error": "invalid when parameter"})
|
||||||
|
return
|
||||||
|
}
|
||||||
|
}
|
||||||
|
trips := []map[string]interface{}{}
|
||||||
|
for trip := range stop.GetTripsAfter(when) {
|
||||||
|
// Find the index of the stop in the trip
|
||||||
|
startIdx := 0
|
||||||
|
for i, st := range trip.Stops {
|
||||||
|
if st.StopId == from {
|
||||||
|
startIdx = i
|
||||||
|
break
|
||||||
|
}
|
||||||
|
}
|
||||||
|
agencyName := ""
|
||||||
|
if trip.Agency != nil {
|
||||||
|
agencyName = trip.Agency.AgencyName
|
||||||
|
}
|
||||||
|
tripData := map[string]interface{}{
|
||||||
|
"trip_id": trip.TripId,
|
||||||
|
"headsign": trip.TripHeadsign,
|
||||||
|
"short_name": trip.TripShortName,
|
||||||
|
"route_id": trip.RouteId,
|
||||||
|
"agency_id": trip.AgencyId,
|
||||||
|
"agency_name": agencyName,
|
||||||
|
"stops": []map[string]interface{}{},
|
||||||
|
}
|
||||||
|
for _, st := range trip.Stops[startIdx:] {
|
||||||
|
tripData["stops"] = append(tripData["stops"].([]map[string]interface{}), map[string]interface{}{
|
||||||
|
"stop_id": st.StopId,
|
||||||
|
"stop_name": st.Stop.StopName,
|
||||||
|
"location": []float64{st.Stop.StopLat, st.Stop.StopLon},
|
||||||
|
"arrival_time": st.ArrivalTime,
|
||||||
|
"departure_time": st.DepartureTime,
|
||||||
|
})
|
||||||
|
}
|
||||||
|
trips = append(trips, tripData)
|
||||||
|
}
|
||||||
|
w.Header().Set("Content-Type", "application/json")
|
||||||
|
json.NewEncoder(w).Encode(trips)
|
||||||
|
})
|
||||||
|
|
||||||
http.HandleFunc("/api/route", func(w http.ResponseWriter, r *http.Request) {
|
http.HandleFunc("/api/route", func(w http.ResponseWriter, r *http.Request) {
|
||||||
|
|
||||||
from := r.URL.Query().Get("from")
|
from := r.URL.Query().Get("from")
|
||||||
@@ -125,7 +196,7 @@ func main() {
|
|||||||
return
|
return
|
||||||
}
|
}
|
||||||
log.Printf("using num %v", num)
|
log.Printf("using num %v", num)
|
||||||
w.WriteHeader(http.StatusOK)
|
|
||||||
log.Printf("start time %v", when)
|
log.Printf("start time %v", when)
|
||||||
route, err := tp.FindRoute(from, to, when)
|
route, err := tp.FindRoute(from, to, when)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
@@ -133,6 +204,7 @@ func main() {
|
|||||||
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.WriteHeader(http.StatusOK)
|
||||||
w.Header().Set("Content-Type", "application/json")
|
w.Header().Set("Content-Type", "application/json")
|
||||||
json.NewEncoder(w).Encode(route)
|
json.NewEncoder(w).Encode(route)
|
||||||
})
|
})
|
||||||
|
|||||||
@@ -5,6 +5,7 @@ import (
|
|||||||
"time"
|
"time"
|
||||||
|
|
||||||
"git.tornberg.me/go-gtfs/pkg/reader"
|
"git.tornberg.me/go-gtfs/pkg/reader"
|
||||||
|
"git.tornberg.me/go-gtfs/pkg/types"
|
||||||
)
|
)
|
||||||
|
|
||||||
var tripData *reader.TripData
|
var tripData *reader.TripData
|
||||||
@@ -23,35 +24,68 @@ func TestFindRouteToStockholm(t *testing.T) {
|
|||||||
|
|
||||||
//tp.Preprocess()
|
//tp.Preprocess()
|
||||||
|
|
||||||
route, err := tp.FindRoute("740000030", "740000001", time.Now().Add(time.Hour*-16))
|
routes, err := tp.FindRoute("740000030", "740000001", time.Now())
|
||||||
if err != nil {
|
if err != nil {
|
||||||
t.Fatalf("Error finding route: %v", err)
|
t.Fatalf("Error finding route: %v", err)
|
||||||
}
|
}
|
||||||
if route == nil {
|
if len(routes) == 0 {
|
||||||
t.Fatal("No route found from Falun Centralstation to Stockholm Centralstation")
|
t.Fatal("No route found from Falun Centralstation to Stockholm Centralstation")
|
||||||
}
|
}
|
||||||
|
|
||||||
if len(route.Legs) < 1 {
|
if len(routes[0].Legs) < 1 {
|
||||||
t.Fatal("Route has no legs")
|
t.Fatal("Route has no legs")
|
||||||
}
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func TestFindRouteWithRequiredChanges(t *testing.T) {
|
||||||
|
tp := NewTripPlanner(tripData)
|
||||||
|
//tp.Preprocess()
|
||||||
|
routes, err := tp.FindRoute("740000030", "740010947", time.Now().Add(time.Hour*-4))
|
||||||
|
if err != nil {
|
||||||
|
t.Fatalf("Error finding route: %v", err)
|
||||||
|
}
|
||||||
|
if len(routes) == 0 {
|
||||||
|
t.Fatal("No route found from Falun Centralstation to Uppsala Centralstation")
|
||||||
|
}
|
||||||
|
if len(routes[0].Legs) < 2 {
|
||||||
|
t.Fatal("Route has less than 2 legs, expected at least one transfer")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
func TestFindRouteToMalmo(t *testing.T) {
|
func TestFindRouteToMalmo(t *testing.T) {
|
||||||
tp := NewTripPlanner(tripData)
|
tp := NewTripPlanner(tripData)
|
||||||
|
|
||||||
//tp.Preprocess()
|
//tp.Preprocess()
|
||||||
|
|
||||||
route, err := tp.FindRoute("740000030", "740000003", time.Now().Add(time.Hour*-16))
|
routes, err := tp.FindRoute("740000030", "740000003", time.Now().Add(time.Hour*-8))
|
||||||
if err != nil {
|
if err != nil {
|
||||||
t.Fatalf("Error finding route: %v", err)
|
t.Fatalf("Error finding route: %v", err)
|
||||||
}
|
}
|
||||||
if route == nil {
|
if len(routes) == 0 {
|
||||||
t.Fatal("No route found from Falun Centralstation to Malmö Centralstation")
|
t.Fatal("No route found from Falun Centralstation to Malmö Centralstation")
|
||||||
}
|
}
|
||||||
|
|
||||||
if len(route.Legs) < 1 {
|
if len(routes[0].Legs) < 1 {
|
||||||
t.Fatal("Route has no legs")
|
t.Fatal("Route has no legs")
|
||||||
}
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func TestDepartuesAfter(t *testing.T) {
|
||||||
|
if hosjo, ok := tripData.Stops["740025287"]; ok {
|
||||||
|
trips := hosjo.GetTripsAfter(time.Now())
|
||||||
|
p := 3
|
||||||
|
for trip := range trips {
|
||||||
|
t.Logf("Trip %s (%s):", trip.TripShortName, trip.TripHeadsign)
|
||||||
|
for stop := range trip.GetDirectPossibleDestinations(hosjo, time.Now()) {
|
||||||
|
t.Logf("- Stop %s at %s", stop.Stop.StopName, types.AsTime(stop.DepartureTime))
|
||||||
|
}
|
||||||
|
p--
|
||||||
|
if p == 0 {
|
||||||
|
break
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|||||||
@@ -1,10 +1,9 @@
|
|||||||
package main
|
package main
|
||||||
|
|
||||||
import (
|
import (
|
||||||
|
"container/heap"
|
||||||
"fmt"
|
"fmt"
|
||||||
"log"
|
"log"
|
||||||
"slices"
|
|
||||||
"sort"
|
|
||||||
"time"
|
"time"
|
||||||
|
|
||||||
"git.tornberg.me/go-gtfs/pkg/reader"
|
"git.tornberg.me/go-gtfs/pkg/reader"
|
||||||
@@ -14,18 +13,6 @@ import (
|
|||||||
// TripPlanner handles preprocessed transit data for efficient routing
|
// TripPlanner handles preprocessed transit data for efficient routing
|
||||||
type TripPlanner struct {
|
type TripPlanner struct {
|
||||||
*reader.TripData
|
*reader.TripData
|
||||||
graph map[string][]Edge
|
|
||||||
}
|
|
||||||
|
|
||||||
type StopWithPossibleConnections struct {
|
|
||||||
*types.Stop
|
|
||||||
PossibleConnections []Connection
|
|
||||||
}
|
|
||||||
|
|
||||||
type Connection struct {
|
|
||||||
*types.Stop
|
|
||||||
Distance float64
|
|
||||||
Time time.Duration
|
|
||||||
}
|
}
|
||||||
|
|
||||||
const (
|
const (
|
||||||
@@ -34,48 +21,49 @@ const (
|
|||||||
maxWaitBetweenTrips = 1 * time.Hour
|
maxWaitBetweenTrips = 1 * time.Hour
|
||||||
//trajectoryAngleTolerance = 220.0
|
//trajectoryAngleTolerance = 220.0
|
||||||
maxTravelDuration = 12 * time.Hour
|
maxTravelDuration = 12 * time.Hour
|
||||||
maxDetourFactor = 2
|
maxInitialWait = 90 * time.Minute
|
||||||
|
searchWindow = 4 * time.Hour
|
||||||
)
|
)
|
||||||
|
|
||||||
// NewTripPlanner creates a new trip planner instance
|
// NewTripPlanner creates a new trip planner instance
|
||||||
func NewTripPlanner(data *reader.TripData) *TripPlanner {
|
func NewTripPlanner(data *reader.TripData) *TripPlanner {
|
||||||
return &TripPlanner{
|
return &TripPlanner{
|
||||||
TripData: data,
|
TripData: data,
|
||||||
graph: make(map[string][]Edge),
|
//graph: make(map[string][]Edge),
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// Preprocess builds the routing graph and precomputes routes
|
// Preprocess builds the routing graph and precomputes routes
|
||||||
func (tp *TripPlanner) Preprocess() error {
|
func (tp *TripPlanner) Preprocess() error {
|
||||||
|
|
||||||
if hosjo, ok := tp.Stops["740025287"]; ok {
|
// if hosjo, ok := tp.Stops["740025287"]; ok {
|
||||||
trips := hosjo.GetTripsAfter(time.Now())
|
// trips := hosjo.GetTripsAfter(time.Now())
|
||||||
for trip := range trips {
|
// for trip := range trips {
|
||||||
log.Printf("Trip %s (%s):", trip.TripShortName, trip.TripHeadsign)
|
// log.Printf("Trip %s (%s):", trip.TripShortName, trip.TripHeadsign)
|
||||||
for stop := range trip.GetDirectPossibleDestinations(hosjo, time.Now()) {
|
// for stop := range trip.GetDirectPossibleDestinations(hosjo, time.Now()) {
|
||||||
log.Printf("- Stop %s at %s", stop.Stop.StopName, types.AsTime(stop.DepartureTime))
|
// log.Printf("- Stop %s at %s", stop.Stop.StopName, types.AsTime(stop.DepartureTime))
|
||||||
}
|
// }
|
||||||
}
|
// }
|
||||||
|
|
||||||
}
|
// }
|
||||||
|
|
||||||
// Build graph with trip edges
|
// // Build graph with trip edges
|
||||||
for tripID, trip := range tp.Trips {
|
// for tripID, trip := range tp.Trips {
|
||||||
sts := trip.Stops
|
// sts := trip.Stops
|
||||||
sort.Slice(sts, func(i, j int) bool {
|
// sort.Slice(sts, func(i, j int) bool {
|
||||||
return sts[i].StopSequence < sts[j].StopSequence
|
// return sts[i].StopSequence < sts[j].StopSequence
|
||||||
})
|
// })
|
||||||
for i := 0; i < len(sts)-1; i++ {
|
// for i := 0; i < len(sts)-1; i++ {
|
||||||
from := sts[i].StopId
|
// from := sts[i].StopId
|
||||||
to := sts[i+1].StopId
|
// to := sts[i+1].StopId
|
||||||
departure := sts[i].DepartureTime
|
// departure := sts[i].DepartureTime
|
||||||
arrival := sts[i+1].DepartureTime
|
// arrival := sts[i+1].DepartureTime
|
||||||
timeDiff := arrival - departure
|
// timeDiff := arrival - departure
|
||||||
if timeDiff > 0 {
|
// if timeDiff > 0 {
|
||||||
tp.graph[from] = append(tp.graph[from], Edge{To: to, TripID: tripID, Time: timeDiff, DepartureTime: departure})
|
// tp.graph[from] = append(tp.graph[from], Edge{To: to, TripID: tripID, Time: timeDiff, DepartureTime: departure})
|
||||||
}
|
// }
|
||||||
}
|
// }
|
||||||
}
|
// }
|
||||||
|
|
||||||
// // Add transfer edges
|
// // Add transfer edges
|
||||||
// for _, tr := range tp.transfers {
|
// for _, tr := range tp.transfers {
|
||||||
@@ -89,133 +77,316 @@ func (tp *TripPlanner) Preprocess() error {
|
|||||||
// }
|
// }
|
||||||
// }
|
// }
|
||||||
|
|
||||||
// tp.stopTimes = nil
|
|
||||||
|
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
type History struct {
|
type searchNode struct {
|
||||||
*types.StopTime
|
stopTime *types.StopTime
|
||||||
DistanceToEnd float64
|
index int
|
||||||
TravelTime types.SecondsAfterMidnight
|
g float64
|
||||||
|
f float64
|
||||||
|
transfers int
|
||||||
|
parent *searchNode
|
||||||
}
|
}
|
||||||
|
|
||||||
func NewHistory(st *types.StopTime, distanceToEnd float64, travelTime types.SecondsAfterMidnight) *History {
|
func nodeKey(st *types.StopTime) string {
|
||||||
return &History{
|
return fmt.Sprintf("%s:%d", st.TripId, st.StopSequence)
|
||||||
StopTime: st,
|
}
|
||||||
DistanceToEnd: distanceToEnd,
|
|
||||||
TravelTime: travelTime,
|
func heuristicSeconds(from, to *types.Stop) float64 {
|
||||||
|
if from == nil || to == nil {
|
||||||
|
return 0
|
||||||
}
|
}
|
||||||
|
const avgSpeedKmh = 60.0
|
||||||
|
distanceKm := from.HaversineDistance(to)
|
||||||
|
return (distanceKm / avgSpeedKmh) * 3600
|
||||||
|
}
|
||||||
|
|
||||||
|
func tripStopIndex(trip *types.Trip, st *types.StopTime) int {
|
||||||
|
if trip == nil {
|
||||||
|
return -1
|
||||||
|
}
|
||||||
|
for i, candidate := range trip.Stops {
|
||||||
|
if candidate == st {
|
||||||
|
return i
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return -1
|
||||||
}
|
}
|
||||||
|
|
||||||
// FindRoutes finds the best routes (up to num) between two stops starting at the given time
|
// FindRoutes finds the best routes (up to num) between two stops starting at the given time
|
||||||
func (tp *TripPlanner) FindRoute(from, to string, when time.Time) (*Route, error) {
|
func (tp *TripPlanner) FindRoute(from, to string, when time.Time) ([]*Route, error) {
|
||||||
|
|
||||||
fromStop := tp.GetStop(from)
|
fromStop := tp.GetStop(from)
|
||||||
toStop := tp.GetStop(to)
|
toStop := tp.GetStop(to)
|
||||||
|
|
||||||
if fromStop == nil || toStop == nil {
|
if fromStop == nil || toStop == nil {
|
||||||
return nil, fmt.Errorf("invalid from or to stop")
|
return nil, fmt.Errorf("invalid from or to stop")
|
||||||
}
|
}
|
||||||
|
|
||||||
possibleNextStops := make([]*types.StopTime, 0)
|
exclude := make(map[string]struct{})
|
||||||
var startTime *types.StopTime
|
results := make([]*Route, 0, 3)
|
||||||
for start, stop := range fromStop.GetStopsAfter(when) {
|
timeCursor := when
|
||||||
if stop.StopId == toStop.StopId {
|
const maxAttempts = 5
|
||||||
return &Route{
|
var lastErr error
|
||||||
Legs: []Leg{NewLeg(start, stop)},
|
|
||||||
}, nil
|
|
||||||
} else if from != stop.StopId {
|
|
||||||
startTime = start
|
|
||||||
possibleNextStops = append(possibleNextStops, stop)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
slices.SortFunc(possibleNextStops, byDistanceTo(*toStop))
|
|
||||||
for _, nextStop := range possibleNextStops {
|
|
||||||
route, err := tp.findRoute(*nextStop, toStop, *NewHistory(startTime, startTime.Stop.HaversineDistance(toStop), types.AsSecondsAfterMidnight(when)), *NewHistory(nextStop, nextStop.Stop.HaversineDistance(toStop), nextStop.ArrivalTime-startTime.ArrivalTime))
|
|
||||||
if err == nil && route != nil {
|
|
||||||
return route, nil
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
return nil, fmt.Errorf("no route found")
|
for attempts := 0; attempts < maxAttempts && len(results) < 3; attempts++ {
|
||||||
}
|
limit := 3 - len(results)
|
||||||
|
routes, err := tp.searchRoutesAStar(fromStop, toStop, timeCursor, exclude, limit)
|
||||||
func byDistanceTo(end types.Stop) func(a, b *types.StopTime) int {
|
if err == nil {
|
||||||
return func(a, b *types.StopTime) int {
|
results = append(results, routes...)
|
||||||
distanceA := haversine(a.Stop.StopLat, a.Stop.StopLon, end.StopLat, end.StopLon)
|
|
||||||
distanceB := haversine(b.Stop.StopLat, b.Stop.StopLon, end.StopLat, end.StopLon)
|
|
||||||
return (int(distanceA) - int(distanceB)) // + (int(b.ArrivalTime - a.ArrivalTime))
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
func shouldTryStop(end *types.Stop, visited ...History) func(possible *types.StopTime) bool {
|
|
||||||
lastDistance := visited[len(visited)-1].Stop.HaversineDistance(end)
|
|
||||||
return func(possible *types.StopTime) bool {
|
|
||||||
distance := possible.Stop.HaversineDistance(end)
|
|
||||||
for _, v := range visited {
|
|
||||||
if v.DistanceToEnd <= distance*1.2 {
|
|
||||||
return false
|
|
||||||
}
|
|
||||||
if v.TripId == possible.TripId || v.StopId == possible.StopId {
|
|
||||||
return false
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
return distance <= lastDistance*1.2
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
func (tp *TripPlanner) findRoute(start types.StopTime, end *types.Stop, changes ...History) (*Route, error) {
|
|
||||||
if len(changes) >= maxTransfers {
|
|
||||||
return nil, fmt.Errorf("max transfers reached")
|
|
||||||
}
|
|
||||||
isOk := shouldTryStop(end, changes...)
|
|
||||||
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 {
|
} else {
|
||||||
if isOk(stop) {
|
lastErr = err
|
||||||
possibleNextStops = append(possibleNextStops, stop)
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
timeCursor = timeCursor.Add(10 * time.Minute)
|
||||||
}
|
}
|
||||||
slices.SortFunc(possibleNextStops, byDistanceTo(*end))
|
|
||||||
|
|
||||||
tries := 15
|
if len(results) == 0 {
|
||||||
for _, nextStop := range possibleNextStops {
|
if lastErr != nil {
|
||||||
route, err := tp.findRoute(*nextStop, end, append(changes, *NewHistory(nextStop, nextStop.Stop.HaversineDistance(end), nextStop.ArrivalTime-start.ArrivalTime))...)
|
return nil, lastErr
|
||||||
if err == nil && route != nil {
|
|
||||||
return route, nil
|
|
||||||
}
|
|
||||||
tries--
|
|
||||||
if tries <= 0 {
|
|
||||||
break
|
|
||||||
}
|
}
|
||||||
|
return nil, fmt.Errorf("no route found")
|
||||||
}
|
}
|
||||||
return nil, fmt.Errorf("no route found")
|
|
||||||
|
return results, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
func CreateLegs(stops []History, finalStop *types.StopTime) []Leg {
|
func (tp *TripPlanner) searchRoutesAStar(fromStop, toStop *types.Stop, when time.Time, exclude map[string]struct{}, limit int) ([]*Route, error) {
|
||||||
legs := make([]Leg, 0, len(stops)+1)
|
if limit <= 0 {
|
||||||
var previousStop *types.StopTime
|
return nil, nil
|
||||||
for _, stop := range stops {
|
|
||||||
if previousStop != nil {
|
|
||||||
legs = append(legs, NewLeg(previousStop, stop.StopTime))
|
|
||||||
}
|
|
||||||
previousStop = stop.StopTime
|
|
||||||
}
|
}
|
||||||
legs = append(legs, NewLeg(previousStop, finalStop))
|
open := priorityQueue{}
|
||||||
return legs
|
heap.Init(&open)
|
||||||
|
bestCost := make(map[string]float64)
|
||||||
|
startSeconds := types.AsSecondsAfterMidnight(when)
|
||||||
|
startSecondsFloat := float64(startSeconds)
|
||||||
|
initCount := 0
|
||||||
|
initialWaitLimit := maxInitialWait.Seconds()
|
||||||
|
searchWindowLimit := searchWindow.Seconds()
|
||||||
|
|
||||||
|
for tripWithDeparture := range fromStop.GetTripsAfter(when) {
|
||||||
|
trip := tripWithDeparture.Trip
|
||||||
|
startStopTime, ok := trip.Has(fromStop)
|
||||||
|
if !ok {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
idx := tripStopIndex(trip, startStopTime)
|
||||||
|
if idx < 0 {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
departureDelta := float64(startStopTime.DepartureTime) - startSecondsFloat
|
||||||
|
if searchWindow > 0 && departureDelta > searchWindowLimit {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
wait := departureDelta
|
||||||
|
if wait < 0 {
|
||||||
|
wait = 0
|
||||||
|
}
|
||||||
|
if maxInitialWait > 0 && wait > initialWaitLimit {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
node := &searchNode{
|
||||||
|
stopTime: startStopTime,
|
||||||
|
index: idx,
|
||||||
|
g: wait,
|
||||||
|
transfers: 0,
|
||||||
|
}
|
||||||
|
node.f = node.g + heuristicSeconds(startStopTime.Stop, toStop)
|
||||||
|
heap.Push(&open, &pqItem{node: node, priority: node.f})
|
||||||
|
bestCost[nodeKey(startStopTime)] = node.g
|
||||||
|
initCount++
|
||||||
|
}
|
||||||
|
|
||||||
|
if initCount == 0 {
|
||||||
|
return nil, fmt.Errorf("no departures from %s after %s", fromStop.StopName, when.Format(time.RFC3339))
|
||||||
|
}
|
||||||
|
|
||||||
|
routes := make([]*Route, 0, limit)
|
||||||
|
|
||||||
|
for open.Len() > 0 && len(routes) < limit {
|
||||||
|
current := heap.Pop(&open).(*pqItem).node
|
||||||
|
if current.stopTime.StopId == toStop.StopId {
|
||||||
|
route := buildRouteFromNode(current)
|
||||||
|
if route != nil {
|
||||||
|
signature := routeSignature(route)
|
||||||
|
if signature == "" {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
if _, seen := exclude[signature]; seen {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
exclude[signature] = struct{}{}
|
||||||
|
routes = append(routes, route)
|
||||||
|
if len(routes) >= limit {
|
||||||
|
break
|
||||||
|
}
|
||||||
|
}
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
if current.g > maxTravelDuration.Seconds() {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
if searchWindow > 0 {
|
||||||
|
currentDelta := float64(current.stopTime.DepartureTime) - startSecondsFloat
|
||||||
|
if currentDelta > searchWindowLimit {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
tpp := tp.Trips[current.stopTime.TripId]
|
||||||
|
if tpp != nil {
|
||||||
|
for i := current.index + 1; i < len(tpp.Stops); i++ {
|
||||||
|
next := tpp.Stops[i]
|
||||||
|
if next.DropOffType == 1 {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
travel := next.ArrivalTime - current.stopTime.DepartureTime
|
||||||
|
if travel <= 0 {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
newCost := current.g + float64(travel)
|
||||||
|
if newCost > maxTravelDuration.Seconds() {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
if searchWindow > 0 {
|
||||||
|
nextDepartureDelta := float64(next.DepartureTime) - startSecondsFloat
|
||||||
|
if nextDepartureDelta > searchWindowLimit {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
}
|
||||||
|
key := nodeKey(next)
|
||||||
|
if prev, ok := bestCost[key]; ok && newCost >= prev {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
nextNode := &searchNode{
|
||||||
|
stopTime: next,
|
||||||
|
index: i,
|
||||||
|
g: newCost,
|
||||||
|
transfers: current.transfers,
|
||||||
|
parent: current,
|
||||||
|
}
|
||||||
|
nextNode.f = newCost + heuristicSeconds(next.Stop, toStop)
|
||||||
|
bestCost[key] = newCost
|
||||||
|
heap.Push(&open, &pqItem{node: nextNode, priority: nextNode.f})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if current.transfers >= maxTransfers {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
for _, otherTrip := range current.stopTime.Stop.Trips {
|
||||||
|
if otherTrip.TripId == current.stopTime.TripId {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
transferStopTime, ok := otherTrip.Has(current.stopTime.Stop)
|
||||||
|
if !ok {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
if transferStopTime.DepartureTime <= current.stopTime.ArrivalTime {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
wait := transferStopTime.DepartureTime - current.stopTime.ArrivalTime
|
||||||
|
if wait > types.SecondsAfterMidnight(maxWaitBetweenTrips.Seconds()) {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
newCost := current.g + float64(wait) + transferPenalty.Seconds()
|
||||||
|
if newCost > maxTravelDuration.Seconds() {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
if searchWindow > 0 {
|
||||||
|
transferDepartureDelta := float64(transferStopTime.DepartureTime) - startSecondsFloat
|
||||||
|
if transferDepartureDelta > searchWindowLimit {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
}
|
||||||
|
idx := tripStopIndex(otherTrip, transferStopTime)
|
||||||
|
if idx < 0 {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
key := nodeKey(transferStopTime)
|
||||||
|
if prev, ok := bestCost[key]; ok && newCost >= prev {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
nextNode := &searchNode{
|
||||||
|
stopTime: transferStopTime,
|
||||||
|
index: idx,
|
||||||
|
g: newCost,
|
||||||
|
transfers: current.transfers + 1,
|
||||||
|
parent: current,
|
||||||
|
}
|
||||||
|
nextNode.f = newCost + heuristicSeconds(transferStopTime.Stop, toStop)
|
||||||
|
bestCost[key] = newCost
|
||||||
|
heap.Push(&open, &pqItem{node: nextNode, priority: nextNode.f})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if len(routes) == 0 {
|
||||||
|
return nil, fmt.Errorf("no route found")
|
||||||
|
}
|
||||||
|
|
||||||
|
return routes, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func buildRouteFromNode(goal *searchNode) *Route {
|
||||||
|
if goal == nil {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
nodes := make([]*searchNode, 0)
|
||||||
|
for current := goal; current != nil; current = current.parent {
|
||||||
|
nodes = append(nodes, current)
|
||||||
|
}
|
||||||
|
for i, j := 0, len(nodes)-1; i < j; i, j = i+1, j-1 {
|
||||||
|
nodes[i], nodes[j] = nodes[j], nodes[i]
|
||||||
|
}
|
||||||
|
if len(nodes) < 2 {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
legs := make([]Leg, 0)
|
||||||
|
runStart := nodes[0]
|
||||||
|
for i := 1; i < len(nodes); i++ {
|
||||||
|
prev := nodes[i-1]
|
||||||
|
curr := nodes[i]
|
||||||
|
if curr.stopTime.TripId != prev.stopTime.TripId {
|
||||||
|
legs = append(legs, NewLeg(runStart.stopTime, prev.stopTime))
|
||||||
|
runStart = curr
|
||||||
|
}
|
||||||
|
}
|
||||||
|
legs = append(legs, NewLeg(runStart.stopTime, nodes[len(nodes)-1].stopTime))
|
||||||
|
return &Route{Legs: legs}
|
||||||
|
}
|
||||||
|
|
||||||
|
func routeSignature(route *Route) string {
|
||||||
|
if route == nil {
|
||||||
|
return ""
|
||||||
|
}
|
||||||
|
if len(route.Legs) == 0 {
|
||||||
|
return ""
|
||||||
|
}
|
||||||
|
start := int(route.StartTime())
|
||||||
|
duration := route.Duration()
|
||||||
|
return fmt.Sprintf("%d:%d", start, duration)
|
||||||
}
|
}
|
||||||
|
|
||||||
func NewLeg(fromStop, toStop *types.StopTime) Leg {
|
func NewLeg(fromStop, toStop *types.StopTime) Leg {
|
||||||
|
trip, ok := toStop.Stop.Trips[toStop.TripId]
|
||||||
|
if !ok {
|
||||||
|
log.Printf("trip %s not found for stop %s", toStop.TripId, toStop.Stop.StopName)
|
||||||
|
return Leg{
|
||||||
|
From: fromStop,
|
||||||
|
To: toStop,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
agencyName := ""
|
||||||
|
if trip.Agency != nil {
|
||||||
|
agencyName = trip.Agency.AgencyName
|
||||||
|
}
|
||||||
return Leg{
|
return Leg{
|
||||||
From: fromStop,
|
From: fromStop,
|
||||||
To: toStop,
|
To: toStop,
|
||||||
|
Trip: &JSONTrip{
|
||||||
|
TripId: trip.TripId,
|
||||||
|
RouteId: trip.RouteId,
|
||||||
|
AgencyName: agencyName,
|
||||||
|
TripHeadsign: trip.TripHeadsign,
|
||||||
|
TripShortName: trip.TripShortName,
|
||||||
|
},
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -1,18 +1,16 @@
|
|||||||
package main
|
package main
|
||||||
|
|
||||||
import "time"
|
|
||||||
|
|
||||||
type pqItem struct {
|
type pqItem struct {
|
||||||
Stop string
|
node *searchNode
|
||||||
Cost time.Time
|
priority float64
|
||||||
index int
|
index int
|
||||||
}
|
}
|
||||||
|
|
||||||
type priorityQueue []*pqItem
|
type priorityQueue []*pqItem
|
||||||
|
|
||||||
func (pq priorityQueue) Len() int { return len(pq) }
|
func (pq priorityQueue) Len() int { return len(pq) }
|
||||||
|
|
||||||
func (pq priorityQueue) Less(i, j int) bool { return pq[i].Cost.Before(pq[j].Cost) }
|
func (pq priorityQueue) Less(i, j int) bool { return pq[i].priority < pq[j].priority }
|
||||||
|
|
||||||
func (pq priorityQueue) Swap(i, j int) {
|
func (pq priorityQueue) Swap(i, j int) {
|
||||||
pq[i], pq[j] = pq[j], pq[i]
|
pq[i], pq[j] = pq[j], pq[i]
|
||||||
|
|||||||
33
frontend/package-lock.json
generated
33
frontend/package-lock.json
generated
@@ -8,8 +8,10 @@
|
|||||||
"name": "frontend",
|
"name": "frontend",
|
||||||
"version": "0.0.0",
|
"version": "0.0.0",
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
|
"leaflet": "^1.9.4",
|
||||||
"react": "^19.2.0",
|
"react": "^19.2.0",
|
||||||
"react-dom": "^19.2.0",
|
"react-dom": "^19.2.0",
|
||||||
|
"react-leaflet": "^5.0.0",
|
||||||
"swr": "^2.3.6"
|
"swr": "^2.3.6"
|
||||||
},
|
},
|
||||||
"devDependencies": {
|
"devDependencies": {
|
||||||
@@ -1007,6 +1009,17 @@
|
|||||||
"@jridgewell/sourcemap-codec": "^1.4.14"
|
"@jridgewell/sourcemap-codec": "^1.4.14"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"node_modules/@react-leaflet/core": {
|
||||||
|
"version": "3.0.0",
|
||||||
|
"resolved": "https://registry.npmjs.org/@react-leaflet/core/-/core-3.0.0.tgz",
|
||||||
|
"integrity": "sha512-3EWmekh4Nz+pGcr+xjf0KNyYfC3U2JjnkWsh0zcqaexYqmmB5ZhH37kz41JXGmKzpaMZCnPofBBm64i+YrEvGQ==",
|
||||||
|
"license": "Hippocratic-2.1",
|
||||||
|
"peerDependencies": {
|
||||||
|
"leaflet": "^1.9.0",
|
||||||
|
"react": "^19.0.0",
|
||||||
|
"react-dom": "^19.0.0"
|
||||||
|
}
|
||||||
|
},
|
||||||
"node_modules/@rolldown/pluginutils": {
|
"node_modules/@rolldown/pluginutils": {
|
||||||
"version": "1.0.0-beta.47",
|
"version": "1.0.0-beta.47",
|
||||||
"resolved": "https://registry.npmjs.org/@rolldown/pluginutils/-/pluginutils-1.0.0-beta.47.tgz",
|
"resolved": "https://registry.npmjs.org/@rolldown/pluginutils/-/pluginutils-1.0.0-beta.47.tgz",
|
||||||
@@ -2253,6 +2266,12 @@
|
|||||||
"json-buffer": "3.0.1"
|
"json-buffer": "3.0.1"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"node_modules/leaflet": {
|
||||||
|
"version": "1.9.4",
|
||||||
|
"resolved": "https://registry.npmjs.org/leaflet/-/leaflet-1.9.4.tgz",
|
||||||
|
"integrity": "sha512-nxS1ynzJOmOlHp+iL3FyWqK89GtNL8U8rvlMOsQdTTssxZwCXh8N2NB3GDQOL+YR3XnWyZAxwQixURb+FA74PA==",
|
||||||
|
"license": "BSD-2-Clause"
|
||||||
|
},
|
||||||
"node_modules/levn": {
|
"node_modules/levn": {
|
||||||
"version": "0.4.1",
|
"version": "0.4.1",
|
||||||
"resolved": "https://registry.npmjs.org/levn/-/levn-0.4.1.tgz",
|
"resolved": "https://registry.npmjs.org/levn/-/levn-0.4.1.tgz",
|
||||||
@@ -2526,6 +2545,20 @@
|
|||||||
"react": "^19.2.0"
|
"react": "^19.2.0"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"node_modules/react-leaflet": {
|
||||||
|
"version": "5.0.0",
|
||||||
|
"resolved": "https://registry.npmjs.org/react-leaflet/-/react-leaflet-5.0.0.tgz",
|
||||||
|
"integrity": "sha512-CWbTpr5vcHw5bt9i4zSlPEVQdTVcML390TjeDG0cK59z1ylexpqC6M1PJFjV8jD7CF+ACBFsLIDs6DRMoLEofw==",
|
||||||
|
"license": "Hippocratic-2.1",
|
||||||
|
"dependencies": {
|
||||||
|
"@react-leaflet/core": "^3.0.0"
|
||||||
|
},
|
||||||
|
"peerDependencies": {
|
||||||
|
"leaflet": "^1.9.0",
|
||||||
|
"react": "^19.0.0",
|
||||||
|
"react-dom": "^19.0.0"
|
||||||
|
}
|
||||||
|
},
|
||||||
"node_modules/react-refresh": {
|
"node_modules/react-refresh": {
|
||||||
"version": "0.18.0",
|
"version": "0.18.0",
|
||||||
"resolved": "https://registry.npmjs.org/react-refresh/-/react-refresh-0.18.0.tgz",
|
"resolved": "https://registry.npmjs.org/react-refresh/-/react-refresh-0.18.0.tgz",
|
||||||
|
|||||||
@@ -10,8 +10,10 @@
|
|||||||
"preview": "vite preview"
|
"preview": "vite preview"
|
||||||
},
|
},
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
|
"leaflet": "^1.9.4",
|
||||||
"react": "^19.2.0",
|
"react": "^19.2.0",
|
||||||
"react-dom": "^19.2.0",
|
"react-dom": "^19.2.0",
|
||||||
|
"react-leaflet": "^5.0.0",
|
||||||
"swr": "^2.3.6"
|
"swr": "^2.3.6"
|
||||||
},
|
},
|
||||||
"devDependencies": {
|
"devDependencies": {
|
||||||
|
|||||||
@@ -4,6 +4,8 @@
|
|||||||
margin: 0 auto;
|
margin: 0 auto;
|
||||||
padding: 2rem;
|
padding: 2rem;
|
||||||
font-family: Arial, sans-serif;
|
font-family: Arial, sans-serif;
|
||||||
|
background-color: white;
|
||||||
|
color: black;
|
||||||
}
|
}
|
||||||
|
|
||||||
/* Form Layout */
|
/* Form Layout */
|
||||||
@@ -119,7 +121,7 @@ button:disabled {
|
|||||||
padding: 1rem;
|
padding: 1rem;
|
||||||
border: 1px solid #ddd;
|
border: 1px solid #ddd;
|
||||||
border-radius: 8px;
|
border-radius: 8px;
|
||||||
background-color: #f9f9f9;
|
background-color: white;
|
||||||
}
|
}
|
||||||
|
|
||||||
.leg h3 {
|
.leg h3 {
|
||||||
@@ -180,7 +182,7 @@ button:disabled {
|
|||||||
|
|
||||||
/* Stops List */
|
/* Stops List */
|
||||||
.stops-list {
|
.stops-list {
|
||||||
background-color: #ecf0f1;
|
background-color: white;
|
||||||
border-radius: 4px;
|
border-radius: 4px;
|
||||||
padding: 0.5rem;
|
padding: 0.5rem;
|
||||||
margin: 0.5rem 0;
|
margin: 0.5rem 0;
|
||||||
@@ -198,6 +200,15 @@ button:disabled {
|
|||||||
border-bottom: none;
|
border-bottom: none;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.stop-item.clickable {
|
||||||
|
cursor: pointer;
|
||||||
|
}
|
||||||
|
|
||||||
|
.stop-item.past {
|
||||||
|
color: #999;
|
||||||
|
cursor: not-allowed;
|
||||||
|
}
|
||||||
|
|
||||||
/* Time Input */
|
/* Time Input */
|
||||||
input[type="datetime-local"] {
|
input[type="datetime-local"] {
|
||||||
padding: 0.5rem;
|
padding: 0.5rem;
|
||||||
@@ -216,20 +227,42 @@ select {
|
|||||||
box-sizing: border-box;
|
box-sizing: border-box;
|
||||||
}
|
}
|
||||||
|
|
||||||
/* Route Selector */
|
/* Mode Switch */
|
||||||
.route-selector {
|
.mode-switch {
|
||||||
display: flex;
|
display: flex;
|
||||||
gap: 0.5rem;
|
gap: 0.5rem;
|
||||||
margin-bottom: 1rem;
|
margin-bottom: 2rem;
|
||||||
justify-content: center;
|
justify-content: center;
|
||||||
}
|
}
|
||||||
|
|
||||||
.route-selector button {
|
.mode-switch button.active {
|
||||||
padding: 0.5rem 1rem;
|
|
||||||
font-size: 0.9rem;
|
|
||||||
}
|
|
||||||
|
|
||||||
.route-selector button.active {
|
|
||||||
background-color: #535bf2;
|
background-color: #535bf2;
|
||||||
font-weight: bold;
|
font-weight: bold;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/* Explorer */
|
||||||
|
.explorer {
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
gap: 1rem;
|
||||||
|
align-items: center;
|
||||||
|
}
|
||||||
|
|
||||||
|
.trips {
|
||||||
|
width: 100%;
|
||||||
|
max-width: 800px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.trip {
|
||||||
|
margin-bottom: 2rem;
|
||||||
|
padding: 1rem;
|
||||||
|
border: 1px solid #ddd;
|
||||||
|
border-radius: 8px;
|
||||||
|
background-color: white;
|
||||||
|
}
|
||||||
|
|
||||||
|
.trip h3 {
|
||||||
|
margin-top: 0;
|
||||||
|
color: #333;
|
||||||
|
font-size: 1.1rem;
|
||||||
|
}
|
||||||
|
|||||||
@@ -1,6 +1,8 @@
|
|||||||
import { useState, useMemo } from "react";
|
import { useState, useMemo, useEffect } from "react";
|
||||||
import useSWR from "swr";
|
import useSWR from "swr";
|
||||||
import "./App.css";
|
import "./App.css";
|
||||||
|
import { MapContainer, TileLayer, Marker, Popup, Polyline, CircleMarker, useMap } from 'react-leaflet';
|
||||||
|
import 'leaflet/dist/leaflet.css';
|
||||||
|
|
||||||
function StopSelector({ stops, onChange, placeholder, inputId }) {
|
function StopSelector({ stops, onChange, placeholder, inputId }) {
|
||||||
const [inputValue, setInputValue] = useState("");
|
const [inputValue, setInputValue] = useState("");
|
||||||
@@ -49,12 +51,66 @@ function StopSelector({ stops, onChange, placeholder, inputId }) {
|
|||||||
|
|
||||||
const fetcher = (url) => fetch(url).then(res => res.ok ? res.json() : Promise.reject(res.statusText));
|
const fetcher = (url) => fetch(url).then(res => res.ok ? res.json() : Promise.reject(res.statusText));
|
||||||
|
|
||||||
const JsonView = ({ data }) => { (
|
const JsonView = ({ data }) => {
|
||||||
<pre className="json-view">{JSON.stringify(data, null, 2)}</pre>
|
return (
|
||||||
); };
|
<pre className="json-view">{JSON.stringify(data, null, 2)}</pre>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
const MapComponent = ({ stops }) => {
|
||||||
|
if (!stops || stops.length === 0) return null;
|
||||||
|
const center = [stops[0].stop_lat, stops[0].stop_lon];
|
||||||
|
const positions = stops.map(stop => [stop.stop_lat, stop.stop_lon]);
|
||||||
|
return (
|
||||||
|
<MapContainer center={center} zoom={10} style={{ height: '400px', width: '100%' }}>
|
||||||
|
<FitBounds stops={stops} />
|
||||||
|
<TileLayer
|
||||||
|
url="https://{s}.tile.openstreetmap.org/{z}/{x}/{y}.png"
|
||||||
|
attribution='© <a href="https://www.openstreetmap.org/copyright">OpenStreetMap</a> contributors'
|
||||||
|
/>
|
||||||
|
{stops.map((stop, index) => (
|
||||||
|
<CircleMarker key={stop.stop_id} center={[stop.stop_lat, stop.stop_lon]} radius={5} color="blue" fillColor="blue" fillOpacity={0.5}>
|
||||||
|
<Popup>{stop.stop_name}</Popup>
|
||||||
|
</CircleMarker>
|
||||||
|
))}
|
||||||
|
<Polyline positions={positions} color="blue" />
|
||||||
|
</MapContainer>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
const FitBounds = ({ stops }) => {
|
||||||
|
const map = useMap();
|
||||||
|
useEffect(() => {
|
||||||
|
if (stops && stops.length > 1) {
|
||||||
|
const bounds = stops.reduce((acc, stop) => {
|
||||||
|
return [
|
||||||
|
[Math.min(acc[0][0], stop.stop_lat), Math.min(acc[0][1], stop.stop_lon)],
|
||||||
|
[Math.max(acc[1][0], stop.stop_lat), Math.max(acc[1][1], stop.stop_lon)]
|
||||||
|
];
|
||||||
|
}, [[stops[0].stop_lat, stops[0].stop_lon], [stops[0].stop_lat, stops[0].stop_lon]]);
|
||||||
|
map.fitBounds(bounds, { padding: [20, 20] });
|
||||||
|
}
|
||||||
|
}, [map, stops]);
|
||||||
|
return null;
|
||||||
|
};
|
||||||
|
|
||||||
|
const asTimeString = (secondsAfterMidnight) => {
|
||||||
|
|
||||||
|
const hours = Math.floor(secondsAfterMidnight / 3600) % 24;
|
||||||
|
const minutes = Math.floor((secondsAfterMidnight % 3600) / 60);
|
||||||
|
return `${hours.toString().padStart(2, '0')}:${minutes.toString().padStart(2, '0')}`;
|
||||||
|
}
|
||||||
|
|
||||||
|
const secondsToDateTime = (secondsAfterMidnight) => {
|
||||||
|
const now = new Date();
|
||||||
|
const date = new Date(now.getFullYear(), now.getMonth(), now.getDate());
|
||||||
|
date.setSeconds(secondsAfterMidnight);
|
||||||
|
return date.toISOString();
|
||||||
|
}
|
||||||
|
|
||||||
function App() {
|
function App() {
|
||||||
const { data: stops = [], error: stopsError } = useSWR('/api/stops', fetcher);
|
const { data: stops = [], error: stopsError } = useSWR('/api/stops', fetcher);
|
||||||
|
const [mode, setMode] = useState("plan");
|
||||||
const [from, setFrom] = useState({
|
const [from, setFrom] = useState({
|
||||||
id: "740000030",
|
id: "740000030",
|
||||||
name: "Falun Centralstation",
|
name: "Falun Centralstation",
|
||||||
@@ -65,142 +121,220 @@ function App() {
|
|||||||
});
|
});
|
||||||
const [time, setTime] = useState(new Date().toISOString());
|
const [time, setTime] = useState(new Date().toISOString());
|
||||||
const [num, setNum] = useState(3);
|
const [num, setNum] = useState(3);
|
||||||
const [routeKey, setRouteKey] = useState(null);
|
|
||||||
const { data: routes = [], error: routesError, isLoading } = useSWR(routeKey, fetcher);
|
const { data: routes = [], error: routesError, isLoading } = useSWR(from.id && to.id ? `/api/route?from=${from.id}&to=${to.id}&when=${encodeURIComponent(time)}&num=${num}` : null, (url => fetcher(url).then(data => data.map((item, idx) => ({ ...item, key: idx })))), { revalidateOnFocus: false });
|
||||||
const [selectedRouteIndex, setSelectedRouteIndex] = useState(0);
|
const [selectedRouteIndex, setSelectedRouteIndex] = useState(0);
|
||||||
const [error, setError] = useState("");
|
const [error, setError] = useState("");
|
||||||
|
|
||||||
|
const routeStops = useMemo(() => {
|
||||||
|
if (!routes[selectedRouteIndex]?.legs) return [];
|
||||||
|
const stopIds = new Set();
|
||||||
|
routes[selectedRouteIndex].legs.forEach(leg => {
|
||||||
|
leg.trip?.stops?.forEach(stopId => stopIds.add(stopId));
|
||||||
|
});
|
||||||
|
return Array.from(stopIds).map(id => stops.find(s => s.stop_id === id)).filter(Boolean);
|
||||||
|
}, [routes, selectedRouteIndex, stops]);
|
||||||
|
|
||||||
|
const [exploreStop, setExploreStop] = useState({
|
||||||
|
id: "740000030",
|
||||||
|
name: "Falun Centralstation",
|
||||||
|
});
|
||||||
|
const [exploreTime, setExploreTime] = useState(new Date().toISOString());
|
||||||
|
const { data: trips = [], error: tripsError, isLoading: tripsLoading } = useSWR(mode === "explore" && exploreStop.id ? `/api/trips?from=${exploreStop.id}&when=${encodeURIComponent(exploreTime)}` : null, fetcher, { revalidateOnFocus: false });
|
||||||
|
|
||||||
const findRoute = () => {
|
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;
|
||||||
}
|
}
|
||||||
setError("");
|
setError("");
|
||||||
setRouteKey(`/api/route?from=${from.id}&to=${to.id}&when=${encodeURIComponent(time)}&num=${num}`);
|
// setRouteKey(`/api/route?from=${from.id}&to=${to.id}&when=${encodeURIComponent(time)}&num=${num}`);
|
||||||
setSelectedRouteIndex(0);
|
setSelectedRouteIndex(0);
|
||||||
};
|
};
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className="App">
|
<div className="App">
|
||||||
<h1>GTFS Route Planner</h1>
|
<h1>GTFS Route Planner</h1>
|
||||||
<div className="planner">
|
<div className="mode-switch">
|
||||||
<div className="selects">
|
<button onClick={() => setMode("plan")} className={mode === "plan" ? "active" : ""} type="button">Plan Route</button>
|
||||||
<label htmlFor="from-stop">
|
<button onClick={() => setMode("explore")} className={mode === "explore" ? "active" : ""} type="button">Explore Trips</button>
|
||||||
From: ({from.id})
|
|
||||||
<StopSelector
|
|
||||||
stops={stops}
|
|
||||||
onChange={setFrom}
|
|
||||||
placeholder="Search for origin stop"
|
|
||||||
inputId="from-stop"
|
|
||||||
/>
|
|
||||||
</label>
|
|
||||||
<label htmlFor="to-stop">
|
|
||||||
To: ({to.id})
|
|
||||||
<StopSelector
|
|
||||||
stops={stops}
|
|
||||||
onChange={setTo}
|
|
||||||
placeholder="Search for destination stop"
|
|
||||||
inputId="to-stop"
|
|
||||||
/>
|
|
||||||
</label>
|
|
||||||
<label htmlFor="time">
|
|
||||||
Time:
|
|
||||||
<input
|
|
||||||
id="time"
|
|
||||||
type="datetime-local"
|
|
||||||
value={time}
|
|
||||||
onChange={(e) => setTime(e.target.value)}
|
|
||||||
/>
|
|
||||||
</label>
|
|
||||||
<label htmlFor="num">
|
|
||||||
Number of routes:
|
|
||||||
<select
|
|
||||||
id="num"
|
|
||||||
value={num}
|
|
||||||
onChange={(e) => setNum(parseInt(e.target.value))}
|
|
||||||
>
|
|
||||||
<option value={1}>1</option>
|
|
||||||
<option value={2}>2</option>
|
|
||||||
<option value={3}>3</option>
|
|
||||||
<option value={5}>5</option>
|
|
||||||
<option value={10}>10</option>
|
|
||||||
</select>
|
|
||||||
</label>
|
|
||||||
<button onClick={findRoute} disabled={isLoading} type="button">
|
|
||||||
{isLoading ? "Finding route..." : "Find Route"}
|
|
||||||
</button>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
{(error || stopsError || routesError) && <p className="error">{error || (stopsError ? "Failed to load stops: " + stopsError.message : "Failed to find route: " + routesError)}</p>}
|
|
||||||
{routes.length > 0 && (
|
|
||||||
<div className="routes">
|
|
||||||
<h2 className="route-title">
|
|
||||||
Routes from {from.name} to {to.name}
|
|
||||||
</h2>
|
|
||||||
{/* {routes.length > 1 && (
|
|
||||||
<div className="route-selector">
|
|
||||||
{routes.map((_, index) => (
|
|
||||||
<button
|
|
||||||
key={index}
|
|
||||||
onClick={() => setSelectedRouteIndex(index)}
|
|
||||||
className={selectedRouteIndex === index ? "active" : ""}
|
|
||||||
type="button"
|
|
||||||
>
|
|
||||||
Option {index + 1}
|
|
||||||
</button>
|
|
||||||
))}
|
|
||||||
</div>
|
|
||||||
)} */}
|
|
||||||
<JsonView data={routes} />
|
|
||||||
{/* {routes[selectedRouteIndex]?.legs &&
|
|
||||||
routes[selectedRouteIndex].legs.length > 0 && (
|
|
||||||
<div className="route">
|
|
||||||
{routes[selectedRouteIndex].legs.map((leg, index) => {
|
|
||||||
const trip = leg.trip;
|
|
||||||
return (
|
|
||||||
<div key={leg.trip?.trip_id || index} className="leg">
|
|
||||||
<h3 className="leg-title">
|
|
||||||
Leg {index + 1}:{" "}
|
|
||||||
{trip?.trip_short_name ?? trip.route_id} by{" "}
|
|
||||||
{leg.agency?.agency_name}
|
|
||||||
</h3>
|
|
||||||
<p className="leg-stops">
|
|
||||||
From: {leg.from?.stop_name} To: {leg.to?.stop_name}
|
|
||||||
</p>
|
|
||||||
<p className="leg-time">
|
|
||||||
Departure:{" "}
|
|
||||||
{new Date(leg.departure_time).toLocaleString()}
|
|
||||||
</p>
|
|
||||||
<p className="leg-time">
|
|
||||||
Arrival: {new Date(leg.arrival_time).toLocaleString()}
|
|
||||||
</p>
|
|
||||||
<ul className="stops-list">
|
|
||||||
{leg.stops?.map((stopId) => {
|
|
||||||
const stop = stops.find(
|
|
||||||
(s) => s.stop_id === stopId,
|
|
||||||
);
|
|
||||||
return (
|
|
||||||
<li key={stopId} className="stop-item">
|
|
||||||
{stop?.stop_name || stopId}
|
|
||||||
</li>
|
|
||||||
);
|
|
||||||
})}
|
|
||||||
</ul>
|
|
||||||
{/* {index < routes[selectedRouteIndex].legs.length - 1 && (
|
|
||||||
<p className="transfer">
|
|
||||||
Transfer at{" "}
|
|
||||||
{stops.find((s) => s.stop_id === leg.to)?.stop_name ||
|
|
||||||
leg.to}
|
|
||||||
</p>
|
|
||||||
)} }
|
|
||||||
</div>
|
|
||||||
);
|
|
||||||
})}
|
|
||||||
</div>
|
|
||||||
)}*/}
|
|
||||||
</div>
|
|
||||||
)}
|
|
||||||
</div>
|
</div>
|
||||||
|
{mode === "plan" && (
|
||||||
|
<div className="planner">
|
||||||
|
<div className="selects">
|
||||||
|
<label htmlFor="from-stop">
|
||||||
|
From: ({from.id})
|
||||||
|
<StopSelector
|
||||||
|
stops={stops}
|
||||||
|
onChange={setFrom}
|
||||||
|
placeholder="Search for origin stop"
|
||||||
|
inputId="from-stop"
|
||||||
|
/>
|
||||||
|
</label>
|
||||||
|
<label htmlFor="to-stop">
|
||||||
|
To: ({to.id})
|
||||||
|
<StopSelector
|
||||||
|
stops={stops}
|
||||||
|
onChange={setTo}
|
||||||
|
placeholder="Search for destination stop"
|
||||||
|
inputId="to-stop"
|
||||||
|
/>
|
||||||
|
</label>
|
||||||
|
<label htmlFor="time">
|
||||||
|
Time:
|
||||||
|
<input
|
||||||
|
id="time"
|
||||||
|
type="datetime-local"
|
||||||
|
value={time}
|
||||||
|
onChange={(e) => setTime(e.target.value)}
|
||||||
|
/>
|
||||||
|
</label>
|
||||||
|
<label htmlFor="num">
|
||||||
|
Number of routes:
|
||||||
|
<select
|
||||||
|
id="num"
|
||||||
|
value={num}
|
||||||
|
onChange={(e) => setNum(parseInt(e.target.value))}
|
||||||
|
>
|
||||||
|
<option value={1}>1</option>
|
||||||
|
<option value={2}>2</option>
|
||||||
|
<option value={3}>3</option>
|
||||||
|
<option value={5}>5</option>
|
||||||
|
<option value={10}>10</option>
|
||||||
|
</select>
|
||||||
|
</label>
|
||||||
|
<button onClick={findRoute} disabled={isLoading} type="button">
|
||||||
|
{isLoading ? "Finding route..." : "Find Route"}
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{(error || stopsError || routesError) && <p className="error">{error || (stopsError ? "Failed to load stops: " + stopsError.message : "Failed to find route: " + routesError)}</p>}
|
||||||
|
{routes.length > 0 && (
|
||||||
|
<div className="routes">
|
||||||
|
<h2 className="route-title">
|
||||||
|
Routes from {from.name} to {to.name}
|
||||||
|
</h2>
|
||||||
|
{routes.length > 1 && (
|
||||||
|
<div className="route-selector">
|
||||||
|
{routes.map((_, index) => (
|
||||||
|
<button
|
||||||
|
key={index}
|
||||||
|
onClick={() => setSelectedRouteIndex(index)}
|
||||||
|
className={selectedRouteIndex === index ? "active" : ""}
|
||||||
|
type="button"
|
||||||
|
>
|
||||||
|
Option {index + 1}
|
||||||
|
</button>
|
||||||
|
))}
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
|
||||||
|
{routes[selectedRouteIndex]?.legs &&
|
||||||
|
routes[selectedRouteIndex].legs.length > 0 && (
|
||||||
|
<div className="route">
|
||||||
|
{routes[selectedRouteIndex].legs.map((leg, index) => {
|
||||||
|
|
||||||
|
const trip = leg.trip;
|
||||||
|
console.log(leg)
|
||||||
|
return (
|
||||||
|
<div key={leg.trip?.trip_id || index} className="leg">
|
||||||
|
<h3 className="leg-title">
|
||||||
|
Leg {index + 1}:{" "}
|
||||||
|
{trip?.trip_short_name ?? trip.route_id} by{" "}
|
||||||
|
{trip.agency_name}
|
||||||
|
</h3>
|
||||||
|
<p className="leg-stops">
|
||||||
|
From: {leg.start?.stop.stop_name} To: {leg.end?.stop?.stop_name}
|
||||||
|
</p>
|
||||||
|
<p className="leg-time">
|
||||||
|
Departure:{" "}
|
||||||
|
{asTimeString(leg.start.departure_time)}
|
||||||
|
</p>
|
||||||
|
<p className="leg-time">
|
||||||
|
Arrival: {asTimeString(leg.end.arrival_time)}
|
||||||
|
</p>
|
||||||
|
<ul className="stops-list">
|
||||||
|
{trip.stops?.map((stopId, idx) => {
|
||||||
|
const stop = stops.find(
|
||||||
|
(s) => s.stop_id === stopId,
|
||||||
|
);
|
||||||
|
const isStart = index === 0 && idx === 0;
|
||||||
|
const isEnd = index === routes[selectedRouteIndex].legs.length - 1 && idx === trip.stops.length - 1;
|
||||||
|
const isCurrent = isStart || isEnd;
|
||||||
|
return (
|
||||||
|
<li key={stopId} className={`stop-item ${isCurrent ? 'current' : ''}`}>
|
||||||
|
{stop?.stop_name || stopId}
|
||||||
|
</li>
|
||||||
|
);
|
||||||
|
})}
|
||||||
|
</ul>
|
||||||
|
{index < routes[selectedRouteIndex].legs.length - 1 && (
|
||||||
|
<p className="transfer">
|
||||||
|
Transfer at{" "}
|
||||||
|
{stops.find((s) => s.stop_id === leg.to)?.stop_name ||
|
||||||
|
leg.to}
|
||||||
|
</p>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
})}
|
||||||
|
<MapComponent stops={routeStops} />
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
{mode === "explore" && (
|
||||||
|
<div className="explorer">
|
||||||
|
<div className="selects">
|
||||||
|
<label htmlFor="explore-stop">
|
||||||
|
Start Stop: ({exploreStop.id})
|
||||||
|
<StopSelector
|
||||||
|
stops={stops}
|
||||||
|
onChange={setExploreStop}
|
||||||
|
placeholder="Search for start stop"
|
||||||
|
inputId="explore-stop"
|
||||||
|
/>
|
||||||
|
</label>
|
||||||
|
<label htmlFor="explore-time">
|
||||||
|
Time:
|
||||||
|
<input
|
||||||
|
id="explore-time"
|
||||||
|
type="datetime-local"
|
||||||
|
value={exploreTime}
|
||||||
|
onChange={(e) => setExploreTime(e.target.value)}
|
||||||
|
/>
|
||||||
|
</label>
|
||||||
|
</div>
|
||||||
|
{(tripsError || stopsError) && <p className="error">{tripsError ? "Failed to load trips: " + tripsError : "Failed to load stops: " + stopsError}</p>}
|
||||||
|
{tripsLoading && <p>Loading trips...</p>}
|
||||||
|
{trips.length > 0 && (
|
||||||
|
<div className="trips">
|
||||||
|
<h2>Trips from {exploreStop.name}</h2>
|
||||||
|
{trips.slice(0, 10).map((trip, index) => {
|
||||||
|
const tripStops = trip.stops.map(s => stops.find(st => st.stop_id === s.stop_id)).filter(Boolean);
|
||||||
|
return (
|
||||||
|
<div key={trip.trip_id} className="trip">
|
||||||
|
<h3>{trip.headsign || trip.short_name} ({trip.route_id}) by {trip.agency_name}</h3>
|
||||||
|
<ul className="stops-list">
|
||||||
|
{trip.stops.map((stop, idx) => (
|
||||||
|
<li key={idx} className={`stop-item clickable ${idx === 0 ? 'current' : ''}`} onClick={() => {
|
||||||
|
setExploreStop({ id: stop.stop_id, name: stop.stop_name });
|
||||||
|
setExploreTime(secondsToDateTime(stop.departure_time));
|
||||||
|
}}>
|
||||||
|
{asTimeString(stop.arrival_time)} - {stop.stop_name}
|
||||||
|
</li>
|
||||||
|
))}
|
||||||
|
</ul>
|
||||||
|
<MapComponent stops={tripStops} />
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
})}
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -136,8 +136,8 @@ 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),
|
Trips: make([]*types.Trip, 0),
|
||||||
RouteID: record[0],
|
RouteId: record[0],
|
||||||
AgencyID: record[1],
|
AgencyId: record[1],
|
||||||
RouteShortName: record[2],
|
RouteShortName: record[2],
|
||||||
RouteLongName: record[3],
|
RouteLongName: record[3],
|
||||||
RouteType: routeType,
|
RouteType: routeType,
|
||||||
|
|||||||
@@ -39,10 +39,10 @@ func LoadTripData(path string) (*TripData, error) {
|
|||||||
})
|
})
|
||||||
case "routes":
|
case "routes":
|
||||||
err = ParseRoutes(f, func(r types.Route) {
|
err = ParseRoutes(f, func(r types.Route) {
|
||||||
tp.Routes[r.RouteID] = &r
|
tp.Routes[r.RouteId] = &r
|
||||||
if ag, ok := tp.Agencies[r.AgencyID]; ok {
|
if ag, ok := tp.Agencies[r.AgencyId]; ok {
|
||||||
r.Agency = ag
|
r.SetAgency(ag)
|
||||||
ag.AddRoute(&r)
|
// ag.AddRoute(&r)
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
case "stops":
|
case "stops":
|
||||||
@@ -58,10 +58,10 @@ func LoadTripData(path string) (*TripData, error) {
|
|||||||
} else {
|
} else {
|
||||||
log.Printf("route %s not found", trip.RouteId)
|
log.Printf("route %s not found", trip.RouteId)
|
||||||
}
|
}
|
||||||
if agency, ok := tp.Agencies[trip.AgencyID]; ok {
|
if agency, ok := tp.Agencies[trip.AgencyId]; ok {
|
||||||
trip.Agency = agency
|
trip.Agency = agency
|
||||||
} else {
|
} else {
|
||||||
log.Printf("agency %s not found", trip.AgencyID)
|
log.Printf("agency %s not found", trip.AgencyId)
|
||||||
}
|
}
|
||||||
tp.Trips[trip.TripId] = &trip
|
tp.Trips[trip.TripId] = &trip
|
||||||
})
|
})
|
||||||
@@ -88,7 +88,7 @@ func LoadTripData(path string) (*TripData, error) {
|
|||||||
//transfers = append(transfers, tr)
|
//transfers = append(transfers, tr)
|
||||||
stop, ok := tp.Stops[tr.FromStopId]
|
stop, ok := tp.Stops[tr.FromStopId]
|
||||||
if ok {
|
if ok {
|
||||||
stop.AddTransfer(&tr)
|
stop.AddTransfer(&tr, tp.Stops[tr.ToStopId])
|
||||||
} else {
|
} else {
|
||||||
log.Printf("stop %s not found for transfer", tr.FromStopId)
|
log.Printf("stop %s not found for transfer", tr.FromStopId)
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -6,12 +6,12 @@ type Agency struct {
|
|||||||
AgencyURL string `json:"agency_url" csv:"agency_url"`
|
AgencyURL string `json:"agency_url" csv:"agency_url"`
|
||||||
AgencyTimezone string `json:"agency_timezone" csv:"agency_timezone"`
|
AgencyTimezone string `json:"agency_timezone" csv:"agency_timezone"`
|
||||||
AgencyLang string `json:"agency_lang" csv:"agency_lang"`
|
AgencyLang string `json:"agency_lang" csv:"agency_lang"`
|
||||||
Routes map[string]*Route
|
//Routes map[string]*Route
|
||||||
}
|
}
|
||||||
|
|
||||||
func (a *Agency) AddRoute(route *Route) {
|
// func (a *Agency) AddRoute(route *Route) {
|
||||||
if a.Routes == nil {
|
// if a.Routes == nil {
|
||||||
a.Routes = make(map[string]*Route)
|
// a.Routes = make(map[string]*Route)
|
||||||
}
|
// }
|
||||||
a.Routes[route.RouteID] = route
|
// a.Routes[route.RouteID] = route
|
||||||
}
|
// }
|
||||||
|
|||||||
@@ -1,10 +1,10 @@
|
|||||||
package types
|
package types
|
||||||
|
|
||||||
type Route struct {
|
type Route struct {
|
||||||
Agency *Agency `json:"agency" csv:"agency"`
|
Agency *Agency `json:"agency" csv:"-"`
|
||||||
Trips []*Trip `json:"trips" csv:"trips"`
|
Trips []*Trip `json:"trips" csv:"trips"`
|
||||||
RouteID string `json:"route_id" csv:"route_id"`
|
RouteId string `json:"route_id" csv:"route_id"`
|
||||||
AgencyID string `json:"agency_id" csv:"agency_id"`
|
AgencyId string `json:"agency_id" csv:"agency_id"`
|
||||||
RouteShortName string `json:"route_short_name" csv:"route_short_name"`
|
RouteShortName string `json:"route_short_name" csv:"route_short_name"`
|
||||||
RouteLongName string `json:"route_long_name" csv:"route_long_name"`
|
RouteLongName string `json:"route_long_name" csv:"route_long_name"`
|
||||||
RouteType int `json:"route_type" csv:"route_type"`
|
RouteType int `json:"route_type" csv:"route_type"`
|
||||||
|
|||||||
@@ -16,7 +16,8 @@ type Stop struct {
|
|||||||
Transfers []*Transfer `json:"-" csv:"transfers"`
|
Transfers []*Transfer `json:"-" csv:"transfers"`
|
||||||
}
|
}
|
||||||
|
|
||||||
func (s *Stop) AddTransfer(transfer *Transfer) {
|
func (s *Stop) AddTransfer(transfer *Transfer, toStop *Stop) {
|
||||||
|
transfer.ToStop = toStop
|
||||||
if s.Transfers == nil {
|
if s.Transfers == nil {
|
||||||
s.Transfers = make([]*Transfer, 0)
|
s.Transfers = make([]*Transfer, 0)
|
||||||
}
|
}
|
||||||
@@ -24,7 +25,6 @@ func (s *Stop) AddTransfer(transfer *Transfer) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
func (s *Stop) AddTrip(trip *Trip) {
|
func (s *Stop) AddTrip(trip *Trip) {
|
||||||
|
|
||||||
s.Trips[trip.TripId] = trip
|
s.Trips[trip.TripId] = trip
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -39,7 +39,7 @@ func (s *Stop) GetTripsAfter(when time.Time) iter.Seq[*TripWithDepartureTime] {
|
|||||||
for _, trip := range s.Trips {
|
for _, trip := range s.Trips {
|
||||||
|
|
||||||
for _, stop := range trip.Stops {
|
for _, stop := range trip.Stops {
|
||||||
if stop.StopId == s.StopId && stop.ArrivalTime >= startAfterMidnight {
|
if stop.StopId == s.StopId && stop.ArrivalTime >= startAfterMidnight && stop.PickupType == 0 {
|
||||||
|
|
||||||
if !yield(&TripWithDepartureTime{Trip: trip, DepartureTime: stop.DepartureTime}) {
|
if !yield(&TripWithDepartureTime{Trip: trip, DepartureTime: stop.DepartureTime}) {
|
||||||
return
|
return
|
||||||
@@ -86,21 +86,44 @@ func (s *Stop) GetUpcomingStops(start *StopTime) iter.Seq[*StopTime] {
|
|||||||
|
|
||||||
func (s *Stop) GetStopsAfter(when time.Time) iter.Seq2[*StopTime, *StopTime] {
|
func (s *Stop) GetStopsAfter(when time.Time) iter.Seq2[*StopTime, *StopTime] {
|
||||||
startAfterMidnight := AsSecondsAfterMidnight(when)
|
startAfterMidnight := AsSecondsAfterMidnight(when)
|
||||||
|
var first *StopTime
|
||||||
return func(yield func(start, stop *StopTime) bool) {
|
return func(yield func(start, stop *StopTime) bool) {
|
||||||
for _, trip := range s.Trips {
|
for _, trip := range s.Trips {
|
||||||
found := false
|
found := -1
|
||||||
var start *StopTime
|
var start *StopTime
|
||||||
for _, stop := range trip.Stops {
|
for _, stop := range trip.Stops {
|
||||||
if stop.StopId == s.StopId && stop.ArrivalTime >= startAfterMidnight {
|
if stop.StopId == s.StopId && stop.ArrivalTime >= startAfterMidnight {
|
||||||
found = true
|
found = stop.StopSequence
|
||||||
start = stop
|
start = stop
|
||||||
|
if first == nil || start.ArrivalTime < first.ArrivalTime {
|
||||||
|
first = start
|
||||||
|
}
|
||||||
}
|
}
|
||||||
if found {
|
if found != -1 && stop.StopSequence > found && stop.PickupType == 0 {
|
||||||
if !yield(start, stop) {
|
if !yield(start, stop) {
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
}
|
||||||
|
if first == nil {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
for _, transfer := range s.Transfers {
|
||||||
|
if transfer.FromStopId == s.StopId {
|
||||||
|
|
||||||
|
if !yield(first, &StopTime{
|
||||||
|
Stop: transfer.ToStop,
|
||||||
|
TripId: "transfer",
|
||||||
|
ArrivalTime: startAfterMidnight + SecondsAfterMidnight(transfer.MinTransferTime),
|
||||||
|
DepartureTime: startAfterMidnight + SecondsAfterMidnight(transfer.MinTransferTime),
|
||||||
|
StopId: transfer.ToStopId,
|
||||||
|
}) {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,6 +1,7 @@
|
|||||||
package types
|
package types
|
||||||
|
|
||||||
type Transfer struct {
|
type Transfer struct {
|
||||||
|
ToStop *Stop `json:"-" csv:"-"`
|
||||||
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"`
|
||||||
|
|||||||
@@ -42,7 +42,28 @@ func (t *Trip) AddStopTime(stopTime *StopTime) {
|
|||||||
if t.Stops == nil {
|
if t.Stops == nil {
|
||||||
t.Stops = make([]*StopTime, 0)
|
t.Stops = make([]*StopTime, 0)
|
||||||
}
|
}
|
||||||
t.Stops = append(t.Stops, stopTime)
|
// Find the index to insert based on StopSequence
|
||||||
|
idx := 0
|
||||||
|
for i, st := range t.Stops {
|
||||||
|
if stopTime.StopSequence < st.StopSequence {
|
||||||
|
idx = i
|
||||||
|
break
|
||||||
|
}
|
||||||
|
idx = i + 1
|
||||||
|
}
|
||||||
|
// Insert at the correct position
|
||||||
|
t.Stops = append(t.Stops[:idx], append([]*StopTime{stopTime}, t.Stops[idx:]...)...)
|
||||||
|
// Adjust times if necessary for next-day continuation
|
||||||
|
for i := idx; i < len(t.Stops)-1; i++ {
|
||||||
|
curr := t.Stops[i]
|
||||||
|
next := t.Stops[i+1]
|
||||||
|
if next.ArrivalTime < curr.ArrivalTime {
|
||||||
|
next.ArrivalTime += 86400
|
||||||
|
}
|
||||||
|
if next.DepartureTime < curr.DepartureTime {
|
||||||
|
next.DepartureTime += 86400
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
func (t *Trip) Has(stop *Stop) (*StopTime, bool) {
|
func (t *Trip) Has(stop *Stop) (*StopTime, bool) {
|
||||||
|
|||||||
Reference in New Issue
Block a user