calculate_test.go
1package waypoint
2
3import (
4 "fmt"
5 "testing"
6)
7
8func TestNoRouteSentinel(t *testing.T) {
9 // Verify NoRouteSentinel has expected values
10 if NoRouteSentinel.ID != NoRouteID {
11 t.Errorf("NoRouteSentinel.ID = %q, want %q", NoRouteSentinel.ID, NoRouteID)
12 }
13 if NoRouteSentinel.Distance != 0 {
14 t.Errorf("NoRouteSentinel.Distance = %f, want 0", NoRouteSentinel.Distance)
15 }
16 if NoRouteSentinel.Time != 0 {
17 t.Errorf("NoRouteSentinel.Time = %f, want 0", NoRouteSentinel.Time)
18 }
19 if len(NoRouteSentinel.Bbox) != 4 {
20 t.Fatalf("NoRouteSentinel.Bbox length = %d, want 4", len(NoRouteSentinel.Bbox))
21 }
22 for i, v := range NoRouteSentinel.Bbox {
23 if v != 0 {
24 t.Errorf("NoRouteSentinel.Bbox[%d] = %f, want 0", i, v)
25 }
26 }
27}
28
29func TestCalculateMissingRoutes_NoWaypoints(t *testing.T) {
30 // Empty waypoints should return nil, nil
31 results, err := CalculateMissingRoutes(nil, nil)
32 if err != nil {
33 t.Fatalf("unexpected error: %v", err)
34 }
35 if results != nil {
36 t.Error("expected nil results for empty waypoints")
37 }
38}
39
40func TestCalculateMissingRoutes_SingleWaypoint(t *testing.T) {
41 // Single waypoint - no routes possible (need at least 2)
42 waypoints := []Waypoint{
43 {ID: "wp1", Point: []float64{-120.0, 38.0}},
44 }
45 results, err := CalculateMissingRoutes(waypoints, nil)
46 if err != nil {
47 t.Fatalf("unexpected error: %v", err)
48 }
49 if results != nil {
50 t.Error("expected nil results for single waypoint")
51 }
52}
53
54func TestCalculatedRoute_Struct(t *testing.T) {
55 // Test the CalculatedRoute struct
56 cr := CalculatedRoute{
57 WaypointID: "wp1",
58 RouteKey: "wp0:adv(80)",
59 Route: Route{
60 ID: "abc123",
61 Distance: 100.0,
62 Time: 60.0,
63 Bbox: []float64{-120, 38, -119, 39},
64 },
65 Points: [][]float64{{-120.0, 38.0}, {-119.0, 39.0}},
66 Elevation: []float64{100.0, 150.0},
67 Error: nil,
68 }
69
70 if cr.WaypointID != "wp1" {
71 t.Error("WaypointID mismatch")
72 }
73 if cr.RouteKey != "wp0:adv(80)" {
74 t.Error("RouteKey mismatch")
75 }
76 if len(cr.Points) != 2 {
77 t.Errorf("Points length = %d, want 2", len(cr.Points))
78 }
79 if len(cr.Elevation) != 2 {
80 t.Errorf("Elevation length = %d, want 2", len(cr.Elevation))
81 }
82}
83
84func TestCalculatedRoute_SnappedPoints(t *testing.T) {
85 cr := CalculatedRoute{
86 WaypointID: "wp1",
87 RouteKey: "wp0:adv(80)",
88 Route: Route{ID: "abc123"},
89 Points: [][]float64{{-120.51, 38.51}, {-119.51, 39.51}},
90 SourceID: "wp0",
91 SourceOrig: []float64{-120.5, 38.5},
92 SourceSnap: []float64{-120.51, 38.51},
93 DestOrig: []float64{-119.5, 39.5},
94 DestSnap: []float64{-119.51, 39.51},
95 }
96 if cr.SourceID != "wp0" {
97 t.Error("SourceID mismatch")
98 }
99 if len(cr.SourceOrig) != 2 {
100 t.Error("SourceOrig should have 2 coordinates")
101 }
102 if len(cr.SourceSnap) != 2 {
103 t.Error("SourceSnap should have 2 coordinates")
104 }
105 if len(cr.DestOrig) != 2 {
106 t.Error("DestOrig should have 2 coordinates")
107 }
108 if len(cr.DestSnap) != 2 {
109 t.Error("DestSnap should have 2 coordinates")
110 }
111}
112
113func TestCalculateBbox(t *testing.T) {
114 points := [][]float64{
115 {-120.0, 38.0},
116 {-119.0, 39.0},
117 {-121.0, 37.0},
118 {-118.0, 40.0},
119 }
120
121 bbox := calculateBbox(points)
122
123 if len(bbox) != 4 {
124 t.Fatalf("expected bbox with 4 elements, got %d", len(bbox))
125 }
126
127 // [min_lon, min_lat, max_lon, max_lat]
128 if bbox[0] != -121.0 {
129 t.Errorf("min_lon = %f, want -121.0", bbox[0])
130 }
131 if bbox[1] != 37.0 {
132 t.Errorf("min_lat = %f, want 37.0", bbox[1])
133 }
134 if bbox[2] != -118.0 {
135 t.Errorf("max_lon = %f, want -118.0", bbox[2])
136 }
137 if bbox[3] != 40.0 {
138 t.Errorf("max_lat = %f, want 40.0", bbox[3])
139 }
140}
141
142func TestCalculateBbox_Empty(t *testing.T) {
143 bbox := calculateBbox([][]float64{})
144 expected := []float64{0, 0, 0, 0}
145 for i, v := range bbox {
146 if v != expected[i] {
147 t.Errorf("bbox[%d] = %f, want %f", i, v, expected[i])
148 }
149 }
150}
151
152func TestCalculateBbox_SinglePoint(t *testing.T) {
153 points := [][]float64{{-120.0, 38.0}}
154
155 bbox := calculateBbox(points)
156
157 if len(bbox) != 4 {
158 t.Fatalf("expected bbox with 4 elements, got %d", len(bbox))
159 }
160
161 // For single point, min and max are the same
162 if bbox[0] != -120.0 {
163 t.Errorf("min_lon = %f, want -120.0", bbox[0])
164 }
165 if bbox[1] != 38.0 {
166 t.Errorf("min_lat = %f, want 38.0", bbox[1])
167 }
168 if bbox[2] != -120.0 {
169 t.Errorf("max_lon = %f, want -120.0", bbox[2])
170 }
171 if bbox[3] != 38.0 {
172 t.Errorf("max_lat = %f, want 38.0", bbox[3])
173 }
174}
175
176func TestShouldSnapPoint(t *testing.T) {
177 tests := []struct {
178 name string
179 original []float64
180 snapped []float64
181 want bool
182 }{
183 {
184 name: "same point",
185 original: []float64{-120.0, 38.0},
186 snapped: []float64{-120.0, 38.0},
187 want: true,
188 },
189 {
190 name: "close points - 100m apart",
191 original: []float64{-120.0, 38.0},
192 snapped: []float64{-120.001, 38.001}, // roughly 140m
193 want: true,
194 },
195 {
196 name: "distant points - 2000m apart",
197 original: []float64{-120.0, 38.0},
198 snapped: []float64{-120.02, 38.02}, // roughly 2800m
199 want: false,
200 },
201 {
202 name: "edge case - exactly 1000m",
203 original: []float64{-120.0, 38.0},
204 snapped: []float64{-120.01, 38.003}, // roughly 1000m at this latitude
205 want: true, // <= 1000 is allowed
206 },
207 {
208 name: "invalid original",
209 original: []float64{-120.0},
210 snapped: []float64{-120.0, 38.0},
211 want: false,
212 },
213 {
214 name: "invalid snapped",
215 original: []float64{-120.0, 38.0},
216 snapped: []float64{-120.0},
217 want: false,
218 },
219 }
220
221 for _, tt := range tests {
222 t.Run(tt.name, func(t *testing.T) {
223 got := shouldSnapPoint(tt.original, tt.snapped)
224 if got != tt.want {
225 t.Errorf("shouldSnapPoint() = %v, want %v", got, tt.want)
226 }
227 })
228 }
229}
230
231func TestToRouteUpdates(t *testing.T) {
232 routes := []CalculatedRoute{
233 {
234 WaypointID: "wp2",
235 RouteKey: "wp1",
236 Route: Route{ID: "route123", Distance: 100},
237 DestOrig: []float64{-120.0, 38.0},
238 DestSnap: []float64{-120.0001, 38.0001}, // Very close, should snap
239 },
240 {
241 WaypointID: "wp3",
242 RouteKey: "wp2",
243 Route: Route{ID: NoRouteID, Distance: 0}, // No route
244 DestOrig: []float64{-120.0, 38.0},
245 DestSnap: []float64{-120.0, 38.0},
246 },
247 {
248 WaypointID: "wp4",
249 RouteKey: "wp3",
250 Route: Route{ID: "route456", Distance: 200},
251 DestOrig: []float64{-120.0, 38.0},
252 DestSnap: []float64{-120.1, 38.1}, // Far away, should not snap
253 },
254 {
255 WaypointID: "wp5",
256 RouteKey: "wp4",
257 Route: Route{ID: "route789", Distance: 150},
258 Error: fmt.Errorf("some error"), // Has error, should be skipped
259 },
260 }
261
262 updates := ToRouteUpdates(routes)
263
264 // Now includes routes with errors (4 total: wp2, wp3, wp4, wp5)
265 if len(updates) != 4 {
266 t.Errorf("expected 4 updates, got %d", len(updates))
267 }
268
269 // Check wp2 - should have NewPoint set
270 foundWp2 := false
271 for _, u := range updates {
272 if u.WaypointID == "wp2" {
273 foundWp2 = true
274 if u.NewPoint == nil {
275 t.Error("wp2 should have NewPoint set (close snap)")
276 }
277 }
278 }
279 if !foundWp2 {
280 t.Error("wp2 update not found")
281 }
282
283 // Check wp4 - should NOT have NewPoint set (far away)
284 for _, u := range updates {
285 if u.WaypointID == "wp4" {
286 if u.NewPoint != nil {
287 t.Error("wp4 should NOT have NewPoint set (far snap)")
288 }
289 }
290 }
291
292 // Check wp5 - should have ErrorCodes populated (even though it has an error)
293 foundWp5 := false
294 for _, u := range updates {
295 if u.WaypointID == "wp5" {
296 foundWp5 = true
297 // Error route should NOT have NewPoint set
298 if u.NewPoint != nil {
299 t.Error("wp5 should NOT have NewPoint set (has error)")
300 }
301 }
302 }
303 if !foundWp5 {
304 t.Error("wp5 update not found - routes with errors should be included")
305 }
306}
307
308func TestToSourcePointUpdates(t *testing.T) {
309 routes := []CalculatedRoute{
310 {
311 SourceID: "wp1",
312 SourceOrig: []float64{-120.0, 38.0},
313 SourceSnap: []float64{-120.0001, 38.0001}, // Close
314 Route: Route{ID: "route123"},
315 },
316 {
317 SourceID: "wp2",
318 SourceOrig: []float64{-120.0, 38.0},
319 SourceSnap: []float64{-120.1, 38.1}, // Far
320 Route: Route{ID: "route456"},
321 },
322 {
323 SourceID: "wp3",
324 SourceOrig: []float64{-120.0, 38.0},
325 SourceSnap: []float64{-120.0001, 38.0001},
326 Route: Route{ID: NoRouteID}, // No route
327 },
328 }
329
330 updates := ToSourcePointUpdates(routes)
331
332 if len(updates) != 1 {
333 t.Errorf("expected 1 source update, got %d", len(updates))
334 }
335
336 if _, ok := updates["wp1"]; !ok {
337 t.Error("wp1 should be in updates")
338 }
339
340 if _, ok := updates["wp2"]; ok {
341 t.Error("wp2 should NOT be in updates (far away)")
342 }
343
344 if _, ok := updates["wp3"]; ok {
345 t.Error("wp3 should NOT be in updates (no route)")
346 }
347}