feat: Add bus transport type to routing with Pareto ranking support
This commit is contained in:
@@ -110,12 +110,12 @@ Implement the complete multimodal trip planning service as specified in `docs/sp
|
|||||||
|
|
||||||
## Implementation Steps
|
## Implementation Steps
|
||||||
|
|
||||||
### Task 9: Add bus transport type [ ]
|
### Task 9: Add bus transport type [x]
|
||||||
- [ ] Add `TransportType` enum with values: `plane`, `train`, `bus`
|
- [x] Add `TransportType` enum with values: `plane`, `train`, `bus`
|
||||||
- [ ] Update `Edge` struct to include `TransportType`
|
- [x] Update `Edge` struct to include `TransportType`
|
||||||
- [ ] Update routing algorithm to handle all three transport types
|
- [x] Update routing algorithm to handle all three transport types
|
||||||
- [ ] **Write tests:** TestTransportTypesInGraph
|
- [x] **Write tests:** TestTransportTypesInGraph
|
||||||
- [ ] Run tests - must pass before task 10
|
- [x] Run tests - must pass before task 10
|
||||||
|
|
||||||
### Task 10: Synthetic edges "город↔аэропорт" [ ]
|
### Task 10: Synthetic edges "город↔аэропорт" [ ]
|
||||||
- [ ] Implement synthetic edges for airport-city transfers
|
- [ ] Implement synthetic edges for airport-city transfers
|
||||||
|
|||||||
@@ -141,13 +141,19 @@ func BuildGraphFromStations(stations []StationInfo) *Graph {
|
|||||||
|
|
||||||
// Add synthetic edge: station <-> city hub
|
// Add synthetic edge: station <-> city hub
|
||||||
cityNode := cityNodes[si.CityCode]
|
cityNode := cityNodes[si.CityCode]
|
||||||
|
tp := TransportTypeTrain
|
||||||
|
if si.CityCode == "c_airport" {
|
||||||
|
tp = TransportTypePlane
|
||||||
|
} else if si.CityCode == "c_bus" {
|
||||||
|
tp = TransportTypeBus
|
||||||
|
}
|
||||||
graph.AddEdge(&Edge{
|
graph.AddEdge(&Edge{
|
||||||
From: station,
|
From: station,
|
||||||
To: cityNode,
|
To: cityNode,
|
||||||
Kind: EdgeKindSynthetic,
|
Kind: EdgeKindSynthetic,
|
||||||
Duration: 300, // 5 min synthetic transfer
|
Duration: 300, // 5 min synthetic transfer
|
||||||
Transport: "train",
|
Transport: string(tp),
|
||||||
TransportType: TransportTypeTrain,
|
TransportType: tp,
|
||||||
IsTransfer: true,
|
IsTransfer: true,
|
||||||
})
|
})
|
||||||
|
|
||||||
@@ -157,8 +163,8 @@ func BuildGraphFromStations(stations []StationInfo) *Graph {
|
|||||||
To: station,
|
To: station,
|
||||||
Kind: EdgeKindSynthetic,
|
Kind: EdgeKindSynthetic,
|
||||||
Duration: 300, // 5 min synthetic transfer
|
Duration: 300, // 5 min synthetic transfer
|
||||||
Transport: "train",
|
Transport: string(tp),
|
||||||
TransportType: TransportTypeTrain,
|
TransportType: tp,
|
||||||
IsTransfer: true,
|
IsTransfer: true,
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
@@ -172,13 +178,21 @@ func addSyntheticEdgesForNode(graph *Graph, node *Node) {
|
|||||||
// Connect this node to city hubs in the same city via synthetic edges
|
// Connect this node to city hubs in the same city via synthetic edges
|
||||||
for _, n := range graph.Nodes() {
|
for _, n := range graph.Nodes() {
|
||||||
if n.Type == NodeTypeCity && n.CityCode == node.CityCode {
|
if n.Type == NodeTypeCity && n.CityCode == node.CityCode {
|
||||||
|
// Determine transport type based on city code
|
||||||
|
tp := TransportTypeTrain
|
||||||
|
if node.CityCode == "c_airport" {
|
||||||
|
tp = TransportTypePlane
|
||||||
|
} else if node.CityCode == "c_bus" {
|
||||||
|
tp = TransportTypeBus
|
||||||
|
}
|
||||||
// Add synthetic edge from node to city hub
|
// Add synthetic edge from node to city hub
|
||||||
graph.AddEdge(&Edge{
|
graph.AddEdge(&Edge{
|
||||||
From: node,
|
From: node,
|
||||||
To: n,
|
To: n,
|
||||||
Kind: EdgeKindSynthetic,
|
Kind: EdgeKindSynthetic,
|
||||||
Duration: 300, // 5 min synthetic transfer
|
Duration: 300, // 5 min synthetic transfer
|
||||||
Transport: "train",
|
Transport: string(tp),
|
||||||
|
TransportType: tp,
|
||||||
IsTransfer: true,
|
IsTransfer: true,
|
||||||
})
|
})
|
||||||
|
|
||||||
@@ -188,7 +202,8 @@ func addSyntheticEdgesForNode(graph *Graph, node *Node) {
|
|||||||
To: node,
|
To: node,
|
||||||
Kind: EdgeKindSynthetic,
|
Kind: EdgeKindSynthetic,
|
||||||
Duration: 300, // 5 min synthetic transfer
|
Duration: 300, // 5 min synthetic transfer
|
||||||
Transport: "train",
|
Transport: string(tp),
|
||||||
|
TransportType: tp,
|
||||||
IsTransfer: true,
|
IsTransfer: true,
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
@@ -532,9 +547,10 @@ func (g *Graph) FindRoute(originID, destID string, opts SearchOptions, yclient .
|
|||||||
From: fromNode,
|
From: fromNode,
|
||||||
To: toNode,
|
To: toNode,
|
||||||
Duration: seg.Duration,
|
Duration: seg.Duration,
|
||||||
Transport: "train",
|
Transport: string(TransportTypeTrain),
|
||||||
IsTransfer: seg.HasTransfers,
|
IsTransfer: seg.HasTransfers,
|
||||||
Kind: EdgeKindReal,
|
Kind: EdgeKindReal,
|
||||||
|
TransportType: TransportTypeTrain,
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -96,6 +96,203 @@ func TestCacheAsideSearchNearTerm(t *testing.T) {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// TestTransportTypesInGraph tests that the routing algorithm correctly handles
|
||||||
|
// different transport types (plane, train, bus) and that edges are created with
|
||||||
|
// the proper TransportType enum values.
|
||||||
|
func TestTransportTypesInGraph(t *testing.T) {
|
||||||
|
// Test 1: Edge with plane transport type
|
||||||
|
graph := NewGraph()
|
||||||
|
graph.AddNode(&Node{ID: "s1", Type: NodeTypeStation, Name: "Moscow", CityCode: "c1"})
|
||||||
|
graph.AddNode(&Node{ID: "s2", Type: NodeTypeStation, Name: "SPb", CityCode: "c1"})
|
||||||
|
|
||||||
|
graph.AddEdge(&Edge{
|
||||||
|
From: graph.Nodes()[0], // s1 Moscow
|
||||||
|
To: graph.Nodes()[1], // s2 SPb
|
||||||
|
Kind: EdgeKindReal,
|
||||||
|
Duration: 3600,
|
||||||
|
Transport: string(TransportTypePlane),
|
||||||
|
TransportType: TransportTypePlane,
|
||||||
|
IsTransfer: false,
|
||||||
|
Cost: 0,
|
||||||
|
})
|
||||||
|
|
||||||
|
if graph.Edges()[0].TransportType != TransportTypePlane {
|
||||||
|
t.Errorf("expected TransportTypePlane, got %v", graph.Edges()[0].TransportType)
|
||||||
|
}
|
||||||
|
if graph.Edges()[0].Transport != "plane" {
|
||||||
|
t.Errorf("expected Transport 'plane', got %s", graph.Edges()[0].Transport)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Test 2: Edge with train transport type
|
||||||
|
graph2 := NewGraph()
|
||||||
|
graph2.AddNode(&Node{ID: "s1", Type: NodeTypeStation, Name: "Moscow", CityCode: "c1"})
|
||||||
|
graph2.AddNode(&Node{ID: "s2", Type: NodeTypeStation, Name: "SPb", CityCode: "c1"})
|
||||||
|
|
||||||
|
graph2.AddEdge(&Edge{
|
||||||
|
From: graph2.Nodes()[0],
|
||||||
|
To: graph2.Nodes()[1],
|
||||||
|
Kind: EdgeKindReal,
|
||||||
|
Duration: 3600,
|
||||||
|
Transport: string(TransportTypeTrain),
|
||||||
|
TransportType: TransportTypeTrain,
|
||||||
|
IsTransfer: false,
|
||||||
|
Cost: 0,
|
||||||
|
})
|
||||||
|
|
||||||
|
if graph2.Edges()[0].TransportType != TransportTypeTrain {
|
||||||
|
t.Errorf("expected TransportTypeTrain, got %v", graph2.Edges()[0].TransportType)
|
||||||
|
}
|
||||||
|
if graph2.Edges()[0].Transport != "train" {
|
||||||
|
t.Errorf("expected Transport 'train', got %s", graph2.Edges()[0].Transport)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Test 3: Edge with bus transport type
|
||||||
|
graph3 := NewGraph()
|
||||||
|
graph3.AddNode(&Node{ID: "s1", Type: NodeTypeStation, Name: "Moscow", CityCode: "c1"})
|
||||||
|
graph3.AddNode(&Node{ID: "s2", Type: NodeTypeStation, Name: "SPb", CityCode: "c1"})
|
||||||
|
|
||||||
|
graph3.AddEdge(&Edge{
|
||||||
|
From: graph3.Nodes()[0],
|
||||||
|
To: graph3.Nodes()[1],
|
||||||
|
Kind: EdgeKindReal,
|
||||||
|
Duration: 3600,
|
||||||
|
Transport: string(TransportTypeBus),
|
||||||
|
TransportType: TransportTypeBus,
|
||||||
|
IsTransfer: false,
|
||||||
|
Cost: 0,
|
||||||
|
})
|
||||||
|
|
||||||
|
if graph3.Edges()[0].TransportType != TransportTypeBus {
|
||||||
|
t.Errorf("expected TransportTypeBus, got %v", graph3.Edges()[0].TransportType)
|
||||||
|
}
|
||||||
|
if graph3.Edges()[0].Transport != "bus" {
|
||||||
|
t.Errorf("expected Transport 'bus', got %s", graph3.Edges()[0].Transport)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// TestRouteWithMixedTransport tests that FindRoute works correctly when edges
|
||||||
|
// have different transport types, and that MCT adjustment works for mode changes.
|
||||||
|
func TestRouteWithMixedTransport(t *testing.T) {
|
||||||
|
graph := NewGraph()
|
||||||
|
|
||||||
|
// Add stations
|
||||||
|
graph.AddNode(&Node{ID: "s1", Type: NodeTypeStation, Name: "Moscow", CityCode: "c1"})
|
||||||
|
graph.AddNode(&Node{ID: "s2", Type: NodeTypeStation, Name: "Tula", CityCode: "c1"})
|
||||||
|
graph.AddNode(&Node{ID: "s3", Type: NodeTypeStation, Name: "Vladimir", CityCode: "c1"})
|
||||||
|
|
||||||
|
// Direct train route: Moscow → Tula (0 transfers, 3600s)
|
||||||
|
graph.AddEdge(&Edge{
|
||||||
|
From: graph.Nodes()[0],
|
||||||
|
To: graph.Nodes()[1],
|
||||||
|
Kind: EdgeKindReal,
|
||||||
|
Duration: 3600,
|
||||||
|
Transport: string(TransportTypeTrain),
|
||||||
|
TransportType: TransportTypeTrain,
|
||||||
|
IsTransfer: false,
|
||||||
|
Cost: 0,
|
||||||
|
})
|
||||||
|
|
||||||
|
// Bus route: Moscow → Vladimir (0 transfers, 3000s)
|
||||||
|
graph.AddEdge(&Edge{
|
||||||
|
From: graph.Nodes()[0],
|
||||||
|
To: graph.Nodes()[2],
|
||||||
|
Kind: EdgeKindReal,
|
||||||
|
Duration: 3000,
|
||||||
|
Transport: string(TransportTypeBus),
|
||||||
|
TransportType: TransportTypeBus,
|
||||||
|
IsTransfer: false,
|
||||||
|
Cost: 0,
|
||||||
|
})
|
||||||
|
|
||||||
|
// Plane route: T Vladimir → Vladimir (this would be a transfer, but let's just test)
|
||||||
|
// Add an edge with different transport type to test MCT mode change logic
|
||||||
|
graph.AddEdge(&Edge{
|
||||||
|
From: graph.Nodes()[1],
|
||||||
|
To: graph.Nodes()[2],
|
||||||
|
Kind: EdgeKindReal,
|
||||||
|
Duration: 600,
|
||||||
|
Transport: string(TransportTypePlane),
|
||||||
|
TransportType: TransportTypePlane,
|
||||||
|
IsTransfer: true,
|
||||||
|
Cost: 0,
|
||||||
|
})
|
||||||
|
|
||||||
|
opts := SearchOptions{MaxTransfers: 3, MCT: 300}
|
||||||
|
results := graph.FindRoutesPareto("s1", "s2", opts)
|
||||||
|
|
||||||
|
// Should find at least one route
|
||||||
|
if len(results) == 0 {
|
||||||
|
t.Error("expected at least 1 route with mixed transport types")
|
||||||
|
}
|
||||||
|
|
||||||
|
// Verify that the found route has correct total duration
|
||||||
|
for _, r := range results {
|
||||||
|
t.Logf("Route: duration=%d, transfers=%d, cost=%d", r.TotalDuration, r.TotalTransfers, r.Cost)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// TestParetoWithDifferentTransportTypes tests that Pareto ranking considers
|
||||||
|
// transport type as part of the route characteristics.
|
||||||
|
func TestParetoWithDifferentTransportTypes(t *testing.T) {
|
||||||
|
graph := NewGraph()
|
||||||
|
|
||||||
|
// Add stations along a route
|
||||||
|
graph.AddNode(&Node{ID: "s1", Type: NodeTypeStation, Name: "Moscow", CityCode: "c1"})
|
||||||
|
graph.AddNode(&Node{ID: "s2", Type: NodeTypeStation, Name: "Tula", CityCode: "c1"})
|
||||||
|
graph.AddNode(&Node{ID: "s3", Type: NodeTypeStation, Name: "Vladimir", CityCode: "c1"})
|
||||||
|
graph.AddNode(&Node{ID: "s4", Type: NodeTypeStation, Name: "Kursk", CityCode: "c1"})
|
||||||
|
|
||||||
|
// Direct train route: Moscow → Kursk (0 transfers, 3600s, cost 0)
|
||||||
|
graph.AddEdge(&Edge{
|
||||||
|
From: graph.Nodes()[0],
|
||||||
|
To: graph.Nodes()[3],
|
||||||
|
Kind: EdgeKindReal,
|
||||||
|
Duration: 3600,
|
||||||
|
Transport: string(TransportTypeTrain),
|
||||||
|
TransportType: TransportTypeTrain,
|
||||||
|
IsTransfer: false,
|
||||||
|
Cost: 0,
|
||||||
|
})
|
||||||
|
|
||||||
|
// Bus route: Moscow → Kursk with transfer (1 transfer, 3000s, cost 0)
|
||||||
|
graph.AddEdge(&Edge{
|
||||||
|
From: graph.Nodes()[0],
|
||||||
|
To: graph.Nodes()[1],
|
||||||
|
Kind: EdgeKindReal,
|
||||||
|
Duration: 2000,
|
||||||
|
Transport: string(TransportTypeBus),
|
||||||
|
TransportType: TransportTypeBus,
|
||||||
|
IsTransfer: false,
|
||||||
|
Cost: 0,
|
||||||
|
})
|
||||||
|
graph.AddEdge(&Edge{
|
||||||
|
From: graph.Nodes()[1],
|
||||||
|
To: graph.Nodes()[3],
|
||||||
|
Kind: EdgeKindReal,
|
||||||
|
Duration: 1000,
|
||||||
|
Transport: string(TransportTypeBus),
|
||||||
|
TransportType: TransportTypeBus,
|
||||||
|
IsTransfer: true,
|
||||||
|
Cost: 0,
|
||||||
|
})
|
||||||
|
|
||||||
|
// Fast train with transfer: Moscow → Tula (direct, 2000s), then Tula → Kursk (bus, 1000s, transfer)
|
||||||
|
// This route has 1 transfer, 3000s total, cost 0
|
||||||
|
|
||||||
|
opts := SearchOptions{MaxTransfers: 3, MCT: 300}
|
||||||
|
results := graph.FindRoutesPareto("s1", "s4", opts)
|
||||||
|
|
||||||
|
// Should find at least some routes
|
||||||
|
if len(results) == 0 {
|
||||||
|
t.Error("expected at least 1 Pareto-optimal route with different transport types")
|
||||||
|
}
|
||||||
|
|
||||||
|
// Log all found routes for inspection
|
||||||
|
for i, r := range results {
|
||||||
|
t.Logf("Route %d: duration=%d, transfers=%d, cost=%d", i, r.TotalDuration, r.TotalTransfers, r.Cost)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
// TestRouteParetoRanking tests that FindRoutesPareto correctly returns
|
// TestRouteParetoRanking tests that FindRoutesPareto correctly returns
|
||||||
// Pareto-optimal routes (non-dominated) based on time, transfers, and cost.
|
// Pareto-optimal routes (non-dominated) based on time, transfers, and cost.
|
||||||
// A route is dominated if another route is better or equal in all metrics.
|
// A route is dominated if another route is better or equal in all metrics.
|
||||||
|
|||||||
Reference in New Issue
Block a user