diff --git a/cmd/planner/astar.go b/cmd/planner/astar.go deleted file mode 100644 index 770c4c1..0000000 --- a/cmd/planner/astar.go +++ /dev/null @@ -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} -// } diff --git a/cmd/planner/csa.go b/cmd/planner/csa.go deleted file mode 100644 index 8b5fac6..0000000 --- a/cmd/planner/csa.go +++ /dev/null @@ -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 -// } diff --git a/cmd/planner/main.go b/cmd/planner/main.go index 1e830a7..8f6f2a5 100644 --- a/cmd/planner/main.go +++ b/cmd/planner/main.go @@ -26,8 +26,17 @@ type TripDetail struct { Stops []string `json:"stops"` } +type JSONTrip struct { + TripId string `json:"trip_id"` + RouteId string `json:"route_id"` + AgencyName string `json:"agency_name"` + TripHeadsign string `json:"trip_headsign"` + TripShortName string `json:"trip_short_name"` +} + type Leg struct { From *types.StopTime `json:"start"` + Trip *JSONTrip `json:"trip"` To *types.StopTime `json:"end"` } @@ -101,6 +110,64 @@ func main() { json.NewEncoder(w).Encode(stopList) }) + http.HandleFunc("/api/trips", func(w http.ResponseWriter, r *http.Request) { + from := r.URL.Query().Get("from") + whenStr := r.URL.Query().Get("when") + if from == "" { + w.WriteHeader(http.StatusBadRequest) + json.NewEncoder(w).Encode(map[string]string{"error": "from parameter required"}) + return + } + stop, ok := tp.Stops[from] + if !ok { + w.WriteHeader(http.StatusNotFound) + json.NewEncoder(w).Encode(map[string]string{"error": "stop not found"}) + return + } + when := time.Now() + if whenStr != "" { + var err error + when, err = time.Parse(time.RFC3339, whenStr) + if err != nil { + w.WriteHeader(http.StatusBadRequest) + json.NewEncoder(w).Encode(map[string]string{"error": "invalid when parameter"}) + return + } + } + trips := []map[string]interface{}{} + for trip := range stop.GetTripsAfter(when) { + // Find the index of the stop in the trip + startIdx := 0 + for i, st := range trip.Stops { + if st.StopId == from { + startIdx = i + break + } + } + tripData := map[string]interface{}{ + "trip_id": trip.TripId, + "headsign": trip.TripHeadsign, + "short_name": trip.TripShortName, + "route_id": trip.RouteID, + "agency_id": trip.AgencyID, + "agency_name": trip.Route.Agency.AgencyName, + "stops": []map[string]interface{}{}, + } + for _, st := range trip.Stops[startIdx:] { + tripData["stops"] = append(tripData["stops"].([]map[string]interface{}), map[string]interface{}{ + "stop_id": st.StopId, + "stop_name": st.Stop.StopName, + "location": []float64{st.Stop.StopLat, st.Stop.StopLon}, + "arrival_time": st.ArrivalTime, + "departure_time": st.DepartureTime, + }) + } + trips = append(trips, tripData) + } + w.Header().Set("Content-Type", "application/json") + json.NewEncoder(w).Encode(trips) + }) + http.HandleFunc("/api/route", func(w http.ResponseWriter, r *http.Request) { from := r.URL.Query().Get("from") @@ -125,7 +192,7 @@ func main() { return } log.Printf("using num %v", num) - w.WriteHeader(http.StatusOK) + log.Printf("start time %v", when) route, err := tp.FindRoute(from, to, when) if err != nil { @@ -133,6 +200,7 @@ func main() { json.NewEncoder(w).Encode(map[string]string{"error": "no route found"}) return } + w.WriteHeader(http.StatusOK) w.Header().Set("Content-Type", "application/json") json.NewEncoder(w).Encode(route) }) diff --git a/cmd/planner/main_test.go b/cmd/planner/main_test.go index 128bacf..05e2d8a 100644 --- a/cmd/planner/main_test.go +++ b/cmd/planner/main_test.go @@ -5,6 +5,7 @@ import ( "time" "git.tornberg.me/go-gtfs/pkg/reader" + "git.tornberg.me/go-gtfs/pkg/types" ) var tripData *reader.TripData @@ -23,35 +24,68 @@ func TestFindRouteToStockholm(t *testing.T) { //tp.Preprocess() - route, err := tp.FindRoute("740000030", "740000001", time.Now().Add(time.Hour*-16)) + routes, err := tp.FindRoute("740000030", "740000001", time.Now()) if err != nil { t.Fatalf("Error finding route: %v", err) } - if route == nil { + if len(routes) == 0 { t.Fatal("No route found from Falun Centralstation to Stockholm Centralstation") } - if len(route.Legs) < 1 { + if len(routes[0].Legs) < 1 { t.Fatal("Route has no legs") } } +func TestFindRouteWithRequiredChanges(t *testing.T) { + tp := NewTripPlanner(tripData) + //tp.Preprocess() + routes, err := tp.FindRoute("740000030", "740010947", time.Now().Add(time.Hour*-4)) + if err != nil { + t.Fatalf("Error finding route: %v", err) + } + if len(routes) == 0 { + t.Fatal("No route found from Falun Centralstation to Uppsala Centralstation") + } + if len(routes[0].Legs) < 2 { + t.Fatal("Route has less than 2 legs, expected at least one transfer") + } +} + func TestFindRouteToMalmo(t *testing.T) { tp := NewTripPlanner(tripData) //tp.Preprocess() - route, err := tp.FindRoute("740000030", "740000003", time.Now().Add(time.Hour*-16)) + routes, err := tp.FindRoute("740000030", "740000003", time.Now().Add(time.Hour*-8)) if err != nil { t.Fatalf("Error finding route: %v", err) } - if route == nil { + if len(routes) == 0 { t.Fatal("No route found from Falun Centralstation to Malmö Centralstation") } - if len(route.Legs) < 1 { + if len(routes[0].Legs) < 1 { t.Fatal("Route has no legs") } } + +func TestDepartuesAfter(t *testing.T) { + if hosjo, ok := tripData.Stops["740025287"]; ok { + trips := hosjo.GetTripsAfter(time.Now()) + p := 3 + for trip := range trips { + t.Logf("Trip %s (%s):", trip.TripShortName, trip.TripHeadsign) + for stop := range trip.GetDirectPossibleDestinations(hosjo, time.Now()) { + t.Logf("- Stop %s at %s", stop.Stop.StopName, types.AsTime(stop.DepartureTime)) + } + p-- + if p == 0 { + break + } + } + + } +} diff --git a/cmd/planner/planner.go b/cmd/planner/planner.go index af897c6..297d46d 100644 --- a/cmd/planner/planner.go +++ b/cmd/planner/planner.go @@ -4,7 +4,6 @@ import ( "fmt" "log" "slices" - "sort" "time" "git.tornberg.me/go-gtfs/pkg/reader" @@ -41,41 +40,41 @@ const ( func NewTripPlanner(data *reader.TripData) *TripPlanner { return &TripPlanner{ TripData: data, - graph: make(map[string][]Edge), + //graph: make(map[string][]Edge), } } // Preprocess builds the routing graph and precomputes routes func (tp *TripPlanner) Preprocess() error { - if hosjo, ok := tp.Stops["740025287"]; ok { - trips := hosjo.GetTripsAfter(time.Now()) - for trip := range trips { - log.Printf("Trip %s (%s):", trip.TripShortName, trip.TripHeadsign) - for stop := range trip.GetDirectPossibleDestinations(hosjo, time.Now()) { - log.Printf("- Stop %s at %s", stop.Stop.StopName, types.AsTime(stop.DepartureTime)) - } - } + // if hosjo, ok := tp.Stops["740025287"]; ok { + // trips := hosjo.GetTripsAfter(time.Now()) + // for trip := range trips { + // log.Printf("Trip %s (%s):", trip.TripShortName, trip.TripHeadsign) + // for stop := range trip.GetDirectPossibleDestinations(hosjo, time.Now()) { + // log.Printf("- Stop %s at %s", stop.Stop.StopName, types.AsTime(stop.DepartureTime)) + // } + // } - } + // } - // Build graph with trip edges - for tripID, trip := range tp.Trips { - sts := trip.Stops - sort.Slice(sts, func(i, j int) bool { - return sts[i].StopSequence < sts[j].StopSequence - }) - for i := 0; i < len(sts)-1; i++ { - from := sts[i].StopId - to := sts[i+1].StopId - departure := sts[i].DepartureTime - arrival := sts[i+1].DepartureTime - timeDiff := arrival - departure - if timeDiff > 0 { - tp.graph[from] = append(tp.graph[from], Edge{To: to, TripID: tripID, Time: timeDiff, DepartureTime: departure}) - } - } - } + // // Build graph with trip edges + // for tripID, trip := range tp.Trips { + // sts := trip.Stops + // sort.Slice(sts, func(i, j int) bool { + // return sts[i].StopSequence < sts[j].StopSequence + // }) + // for i := 0; i < len(sts)-1; i++ { + // from := sts[i].StopId + // to := sts[i+1].StopId + // departure := sts[i].DepartureTime + // arrival := sts[i+1].DepartureTime + // timeDiff := arrival - departure + // if timeDiff > 0 { + // tp.graph[from] = append(tp.graph[from], Edge{To: to, TripID: tripID, Time: timeDiff, DepartureTime: departure}) + // } + // } + // } // // Add transfer edges // for _, tr := range tp.transfers { @@ -100,8 +99,8 @@ type History struct { TravelTime types.SecondsAfterMidnight } -func NewHistory(st *types.StopTime, distanceToEnd float64, travelTime types.SecondsAfterMidnight) *History { - return &History{ +func NewHistory(st *types.StopTime, distanceToEnd float64, travelTime types.SecondsAfterMidnight) History { + return History{ StopTime: st, DistanceToEnd: distanceToEnd, TravelTime: travelTime, @@ -109,7 +108,7 @@ func NewHistory(st *types.StopTime, distanceToEnd float64, travelTime types.Seco } // FindRoutes finds the best routes (up to num) between two stops starting at the given time -func (tp *TripPlanner) FindRoute(from, to string, when time.Time) (*Route, error) { +func (tp *TripPlanner) FindRoute(from, to string, when time.Time) ([]*Route, error) { fromStop := tp.GetStop(from) toStop := tp.GetStop(to) @@ -117,28 +116,61 @@ func (tp *TripPlanner) FindRoute(from, to string, when time.Time) (*Route, error if fromStop == nil || toStop == nil { return nil, fmt.Errorf("invalid from or to stop") } + routes := make([]*Route, 0) + usedRouteIds := make(map[string]struct{}) + + for trip := range fromStop.GetTripsAfter(when) { + if _, used := usedRouteIds[trip.RouteId]; used { + continue + } + for i := len(trip.Stops) - 1; i >= 0; i-- { + stop := trip.Stops[i] + if stop.StopId == toStop.StopId { + usedRouteIds[trip.RouteId] = struct{}{} + routes = append(routes, &Route{ + Legs: []Leg{NewLeg(trip.Stops[0], trip.Stops[i])}, + }) + break + } else if stop.StopId == fromStop.StopId { + break + } else if stop.PickupType == 0 { + distance := stop.Stop.HaversineDistance(toStop) + + } + } + + } - possibleNextStops := make([]*types.StopTime, 0) - var startTime *types.StopTime for start, stop := range fromStop.GetStopsAfter(when) { if stop.StopId == toStop.StopId { - return &Route{ + routes = append(routes, &Route{ Legs: []Leg{NewLeg(start, stop)}, - }, nil + }) } else if from != stop.StopId { - startTime = start - possibleNextStops = append(possibleNextStops, stop) - } - } - slices.SortFunc(possibleNextStops, byDistanceTo(*toStop)) - for _, nextStop := range possibleNextStops { - route, err := tp.findRoute(*nextStop, toStop, *NewHistory(startTime, startTime.Stop.HaversineDistance(toStop), types.AsSecondsAfterMidnight(when)), *NewHistory(nextStop, nextStop.Stop.HaversineDistance(toStop), nextStop.ArrivalTime-startTime.ArrivalTime)) - if err == nil && route != nil { - return route, nil - } - } + // startTime = start + route, err := tp.findRoute(*start, toStop, NewHistory(start, start.Stop.HaversineDistance(toStop), 0), NewHistory(stop, stop.Stop.HaversineDistance(toStop), stop.ArrivalTime-start.DepartureTime)) - return nil, fmt.Errorf("no route found") + if err == nil && route != nil { + routes = append(routes, route) + } + } + } + // slices.SortFunc(possibleNextStops, byDistanceTo(*toStop)) + // for _, nextStop := range possibleNextStops { + // route, err := tp.findRoute(*nextStop, toStop, *NewHistory(startTime, startTime.Stop.HaversineDistance(toStop), types.AsSecondsAfterMidnight(when)), *NewHistory(nextStop, nextStop.Stop.HaversineDistance(toStop), nextStop.ArrivalTime-startTime.ArrivalTime)) + // if err == nil && route != nil { + // return route, nil + // } + // } + slices.SortFunc(routes, func(a, b *Route) int { + transfersA := len(a.Legs) - 1 + transfersB := len(b.Legs) - 1 + if transfersA != transfersB { + return transfersA - transfersB + } + return a.Duration() - b.Duration() - (transfersA-transfersB)*int(transferPenalty.Seconds()) + }) + return routes[:min(len(routes), 10)], nil } func byDistanceTo(end types.Stop) func(a, b *types.StopTime) int { @@ -149,10 +181,38 @@ func byDistanceTo(end types.Stop) func(a, b *types.StopTime) int { } } +func isInCorrectDirection(from, possible, end *types.Stop) bool { + if from.StopId == end.StopId || possible.StopId == end.StopId { + return true + } + if from.StopId == possible.StopId { + return false + } + startToEndLat := end.StopLat - from.StopLat + startToEndLon := end.StopLon - from.StopLon + startToPossibleLat := possible.StopLat - from.StopLat + startToPossibleLon := possible.StopLon - from.StopLon + dotProduct := startToEndLat*startToPossibleLat + startToEndLon*startToPossibleLon + return dotProduct > -0.4 && dotProduct < 0.4 +} + func shouldTryStop(end *types.Stop, visited ...History) func(possible *types.StopTime) bool { lastDistance := visited[len(visited)-1].Stop.HaversineDistance(end) return func(possible *types.StopTime) bool { + if end.StopId == possible.StopId { + return true + } + if possible.DepartureTime > visited[len(visited)-1].DepartureTime+types.SecondsAfterMidnight(maxWaitBetweenTrips.Seconds()) { + return false + } + if possible.DropOffType == 1 { + return false + } + // if !isInCorrectDirection(visited[len(visited)-1].Stop, possible.Stop, end) { + // return false + // } distance := possible.Stop.HaversineDistance(end) + for _, v := range visited { if v.DistanceToEnd <= distance*1.2 { return false @@ -187,7 +247,7 @@ func (tp *TripPlanner) findRoute(start types.StopTime, end *types.Stop, changes tries := 15 for _, nextStop := range possibleNextStops { - route, err := tp.findRoute(*nextStop, end, append(changes, *NewHistory(nextStop, nextStop.Stop.HaversineDistance(end), nextStop.ArrivalTime-start.ArrivalTime))...) + route, err := tp.findRoute(*nextStop, end, append(changes, NewHistory(nextStop, nextStop.Stop.HaversineDistance(end), nextStop.ArrivalTime-start.ArrivalTime))...) if err == nil && route != nil { return route, nil } @@ -213,9 +273,24 @@ func CreateLegs(stops []History, finalStop *types.StopTime) []Leg { } func NewLeg(fromStop, toStop *types.StopTime) Leg { + trip, ok := toStop.Stop.Trips[toStop.TripId] + if !ok { + log.Printf("trip %s not found for stop %s", toStop.TripId, toStop.Stop.StopName) + return Leg{ + From: fromStop, + To: toStop, + } + } return Leg{ From: fromStop, To: toStop, + Trip: &JSONTrip{ + TripId: trip.TripId, + RouteId: trip.RouteID, + AgencyName: trip.Agency.AgencyName, + TripHeadsign: trip.TripHeadsign, + TripShortName: trip.TripShortName, + }, } } diff --git a/frontend/package-lock.json b/frontend/package-lock.json index 49d9e2c..b86d2b4 100644 --- a/frontend/package-lock.json +++ b/frontend/package-lock.json @@ -8,8 +8,10 @@ "name": "frontend", "version": "0.0.0", "dependencies": { + "leaflet": "^1.9.4", "react": "^19.2.0", "react-dom": "^19.2.0", + "react-leaflet": "^5.0.0", "swr": "^2.3.6" }, "devDependencies": { @@ -1007,6 +1009,17 @@ "@jridgewell/sourcemap-codec": "^1.4.14" } }, + "node_modules/@react-leaflet/core": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/@react-leaflet/core/-/core-3.0.0.tgz", + "integrity": "sha512-3EWmekh4Nz+pGcr+xjf0KNyYfC3U2JjnkWsh0zcqaexYqmmB5ZhH37kz41JXGmKzpaMZCnPofBBm64i+YrEvGQ==", + "license": "Hippocratic-2.1", + "peerDependencies": { + "leaflet": "^1.9.0", + "react": "^19.0.0", + "react-dom": "^19.0.0" + } + }, "node_modules/@rolldown/pluginutils": { "version": "1.0.0-beta.47", "resolved": "https://registry.npmjs.org/@rolldown/pluginutils/-/pluginutils-1.0.0-beta.47.tgz", @@ -2253,6 +2266,12 @@ "json-buffer": "3.0.1" } }, + "node_modules/leaflet": { + "version": "1.9.4", + "resolved": "https://registry.npmjs.org/leaflet/-/leaflet-1.9.4.tgz", + "integrity": "sha512-nxS1ynzJOmOlHp+iL3FyWqK89GtNL8U8rvlMOsQdTTssxZwCXh8N2NB3GDQOL+YR3XnWyZAxwQixURb+FA74PA==", + "license": "BSD-2-Clause" + }, "node_modules/levn": { "version": "0.4.1", "resolved": "https://registry.npmjs.org/levn/-/levn-0.4.1.tgz", @@ -2526,6 +2545,20 @@ "react": "^19.2.0" } }, + "node_modules/react-leaflet": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/react-leaflet/-/react-leaflet-5.0.0.tgz", + "integrity": "sha512-CWbTpr5vcHw5bt9i4zSlPEVQdTVcML390TjeDG0cK59z1ylexpqC6M1PJFjV8jD7CF+ACBFsLIDs6DRMoLEofw==", + "license": "Hippocratic-2.1", + "dependencies": { + "@react-leaflet/core": "^3.0.0" + }, + "peerDependencies": { + "leaflet": "^1.9.0", + "react": "^19.0.0", + "react-dom": "^19.0.0" + } + }, "node_modules/react-refresh": { "version": "0.18.0", "resolved": "https://registry.npmjs.org/react-refresh/-/react-refresh-0.18.0.tgz", diff --git a/frontend/package.json b/frontend/package.json index 0db66fc..64776a7 100644 --- a/frontend/package.json +++ b/frontend/package.json @@ -10,8 +10,10 @@ "preview": "vite preview" }, "dependencies": { + "leaflet": "^1.9.4", "react": "^19.2.0", "react-dom": "^19.2.0", + "react-leaflet": "^5.0.0", "swr": "^2.3.6" }, "devDependencies": { diff --git a/frontend/src/App.css b/frontend/src/App.css index 2b2c389..3cce2dd 100644 --- a/frontend/src/App.css +++ b/frontend/src/App.css @@ -4,6 +4,8 @@ margin: 0 auto; padding: 2rem; font-family: Arial, sans-serif; + background-color: white; + color: black; } /* Form Layout */ @@ -119,7 +121,7 @@ button:disabled { padding: 1rem; border: 1px solid #ddd; border-radius: 8px; - background-color: #f9f9f9; + background-color: white; } .leg h3 { @@ -180,7 +182,7 @@ button:disabled { /* Stops List */ .stops-list { - background-color: #ecf0f1; + background-color: white; border-radius: 4px; padding: 0.5rem; margin: 0.5rem 0; @@ -198,6 +200,15 @@ button:disabled { border-bottom: none; } +.stop-item.clickable { + cursor: pointer; +} + +.stop-item.past { + color: #999; + cursor: not-allowed; +} + /* Time Input */ input[type="datetime-local"] { padding: 0.5rem; @@ -216,20 +227,42 @@ select { box-sizing: border-box; } -/* Route Selector */ -.route-selector { +/* Mode Switch */ +.mode-switch { display: flex; gap: 0.5rem; - margin-bottom: 1rem; + margin-bottom: 2rem; justify-content: center; } -.route-selector button { - padding: 0.5rem 1rem; - font-size: 0.9rem; -} - -.route-selector button.active { +.mode-switch button.active { background-color: #535bf2; font-weight: bold; } + +/* Explorer */ +.explorer { + display: flex; + flex-direction: column; + gap: 1rem; + align-items: center; +} + +.trips { + width: 100%; + max-width: 800px; +} + +.trip { + margin-bottom: 2rem; + padding: 1rem; + border: 1px solid #ddd; + border-radius: 8px; + background-color: white; +} + +.trip h3 { + margin-top: 0; + color: #333; + font-size: 1.1rem; +} diff --git a/frontend/src/App.jsx b/frontend/src/App.jsx index c2c4573..d79c84b 100644 --- a/frontend/src/App.jsx +++ b/frontend/src/App.jsx @@ -1,6 +1,8 @@ -import { useState, useMemo } from "react"; +import { useState, useMemo, useEffect } from "react"; import useSWR from "swr"; import "./App.css"; +import { MapContainer, TileLayer, Marker, Popup, Polyline, CircleMarker, useMap } from 'react-leaflet'; +import 'leaflet/dist/leaflet.css'; function StopSelector({ stops, onChange, placeholder, inputId }) { const [inputValue, setInputValue] = useState(""); @@ -49,12 +51,66 @@ function StopSelector({ stops, onChange, placeholder, inputId }) { const fetcher = (url) => fetch(url).then(res => res.ok ? res.json() : Promise.reject(res.statusText)); -const JsonView = ({ data }) => { ( -
{JSON.stringify(data, null, 2)}
-); };
+const JsonView = ({ data }) => {
+ return (
+ {JSON.stringify(data, null, 2)}
+ );
+};
+
+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 (
+ {error || (stopsError ? "Failed to load stops: " + stopsError.message : "Failed to find route: " + routesError)}
} - {routes.length > 0 && ( -- From: {leg.from?.stop_name} To: {leg.to?.stop_name} -
-- Departure:{" "} - {new Date(leg.departure_time).toLocaleString()} -
-- Arrival: {new Date(leg.arrival_time).toLocaleString()} -
-- Transfer at{" "} - {stops.find((s) => s.stop_id === leg.to)?.stop_name || - leg.to} -
- )} } -{error || (stopsError ? "Failed to load stops: " + stopsError.message : "Failed to find route: " + routesError)}
} + {routes.length > 0 && ( ++ From: {leg.start?.stop.stop_name} To: {leg.end?.stop?.stop_name} +
++ Departure:{" "} + {asTimeString(leg.start.departure_time)} +
++ Arrival: {asTimeString(leg.end.arrival_time)} +
++ Transfer at{" "} + {stops.find((s) => s.stop_id === leg.to)?.stop_name || + leg.to} +
+ )} +{tripsError ? "Failed to load trips: " + tripsError : "Failed to load stops: " + stopsError}
} + {tripsLoading &&Loading trips...
} + {trips.length > 0 && ( +