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}