polyline6_test.go

  1package geo
  2
  3import (
  4	"encoding/json"
  5	"os"
  6	"strings"
  7	"testing"
  8)
  9
 10func TestEncode(t *testing.T) {
 11	// Test encoding Maplibre format points [lon, lat] to polyline6
 12	// Using known test case from Google polyline algorithm
 13	points := [][]float64{
 14		{-120.2, 38.5},   // lon, lat
 15		{-120.95, 40.7},
 16		{-126.453, 43.252},
 17	}
 18	
 19	encoded, err := Encode(points)
 20	if err != nil {
 21		t.Fatalf("encode error: %v", err)
 22	}
 23	
 24	// The polyline6 encoding of [38.5, -120.2], [40.7, -120.95], [43.252, -126.453]
 25	// (after swapping [lon, lat] -> [lat, lon])
 26	// Note: float64 precision produces different encoding than float32
 27	expected := "_izlhA~rlgdF_{geC~ywl@_kwzCn`{nI"
 28	if string(encoded) != expected {
 29		t.Errorf("expected %q, got %q", expected, encoded)
 30	}
 31}
 32
 33func TestDecode(t *testing.T) {
 34	// Test decoding polyline6 to Maplibre format points [lon, lat]
 35	// This is the polyline6 encoding of [38.5, -120.2], [40.7, -120.95], [43.252, -126.453]
 36	encoded := Polyline6("_izlhAxrlgdFa{geC~ywl@{jwzCz`{nI")
 37	
 38	points, err := Decode(encoded)
 39	if err != nil {
 40		t.Fatalf("decode error: %v", err)
 41	}
 42	
 43	expected := [][]float64{
 44		{-120.2, 38.5},
 45		{-120.95, 40.7},
 46		{-126.453, 43.252},
 47	}
 48	
 49	if len(points) != len(expected) {
 50		t.Fatalf("expected %d points, got %d", len(expected), len(points))
 51	}
 52	
 53	for i, pt := range points {
 54		if len(pt) != 2 {
 55			t.Errorf("point %d: expected 2 coordinates, got %d", i, len(pt))
 56			continue
 57		}
 58		// Allow small floating point differences due to encoding
 59		if abs(pt[0]-expected[i][0]) > 0.0001 || abs(pt[1]-expected[i][1]) > 0.0001 {
 60			t.Errorf("point %d: expected [%f, %f], got [%f, %f]", 
 61				i, expected[i][0], expected[i][1], pt[0], pt[1])
 62		}
 63	}
 64}
 65
 66func TestDecodeManual(t *testing.T) {
 67	// Test decoding polyline6 to Maplibre format points [lon, lat]
 68	// This is the polyline6 encoding of [38.5, -120.2], [40.7, -120.95], [43.252, -126.453]
 69	encoded := Polyline6("_izlhAxrlgdFa{geC~ywl@{jwzCz`{nI")
 70
 71	points, err := DecodeManual(encoded)
 72	if err != nil {
 73		t.Fatalf("decode error: %v", err)
 74	}
 75
 76	expected := [][]float64{
 77		{-120.2, 38.5},
 78		{-120.95, 40.7},
 79		{-126.453, 43.252},
 80	}
 81
 82	if len(points) != len(expected) {
 83		t.Fatalf("expected %d points, got %d", len(expected), len(points))
 84	}
 85
 86	for i, pt := range points {
 87		if len(pt) != 2 {
 88			t.Errorf("point %d: expected 2 coordinates, got %d", i, len(pt))
 89			continue
 90		}
 91		// Allow small floating point differences due to encoding
 92		if abs(pt[0]-expected[i][0]) > 0.0001 || abs(pt[1]-expected[i][1]) > 0.0001 {
 93			t.Errorf("point %d: expected [%f, %f], got [%f, %f]",
 94				i, expected[i][0], expected[i][1], pt[0], pt[1])
 95		}
 96	}
 97}
 98
 99func TestDecodeManual_Empty(t *testing.T) {
100	points, err := DecodeManual("")
101	if err != nil {
102		t.Fatalf("decode error: %v", err)
103	}
104	if len(points) != 0 {
105		t.Errorf("expected empty points for empty polyline, got %d points", len(points))
106	}
107}
108
109func TestDecodeManual_Invalid(t *testing.T) {
110	_, err := DecodeManual("invalid!@#$")
111	if err == nil {
112		t.Error("expected error for invalid polyline")
113	}
114}
115
116func TestDecodeManual_RoundTrip(t *testing.T) {
117	original := [][]float64{
118		{-74.006, 40.7128},   // NYC
119		{-118.2437, 34.0522}, // LA
120		{-87.6298, 41.8781},  // Chicago
121	}
122
123	encoded, err := Encode(original)
124	if err != nil {
125		t.Fatalf("encode error: %v", err)
126	}
127
128	decoded, err := DecodeManual(encoded)
129	if err != nil {
130		t.Fatalf("decode error: %v", err)
131	}
132
133	if len(decoded) != len(original) {
134		t.Fatalf("expected %d points, got %d", len(original), len(decoded))
135	}
136
137	for i, pt := range decoded {
138		if abs(pt[0]-original[i][0]) > 0.00001 || abs(pt[1]-original[i][1]) > 0.00001 {
139			t.Errorf("point %d: expected [%f, %f], got [%f, %f]",
140				i, original[i][0], original[i][1], pt[0], pt[1])
141		}
142	}
143}
144
145func TestDecodeManual_MatchesLibraryDecode(t *testing.T) {
146	testCases := []Polyline6{
147		"_izlhAxrlgdFa{geC~ywl@{jwzCz`{nI",
148		"", // empty
149	}
150
151	// Add test cases from real data files
152	polylineData, err := os.ReadFile("testdata/test.polyline")
153	if err == nil {
154		testCases = append(testCases, Polyline6(strings.TrimSpace(string(polylineData))))
155	}
156
157	for _, encoded := range testCases {
158		if encoded == "" {
159			// Test empty case
160			manual, err := DecodeManual(encoded)
161			if err != nil {
162				t.Errorf("DecodeManual empty error: %v", err)
163				continue
164			}
165			lib, err := Decode(encoded)
166			if err != nil {
167				t.Errorf("Decode empty error: %v", err)
168				continue
169			}
170			if len(manual) != len(lib) {
171				t.Errorf("empty case: length mismatch")
172			}
173			continue
174		}
175
176		manual, err := DecodeManual(encoded)
177		if err != nil {
178			t.Errorf("DecodeManual error for %q: %v", string(encoded)[:min(20, len(encoded))], err)
179			continue
180		}
181
182		lib, err := Decode(encoded)
183		if err != nil {
184			t.Errorf("Decode error for %q: %v", string(encoded)[:min(20, len(encoded))], err)
185			continue
186		}
187
188		if len(manual) != len(lib) {
189			t.Errorf("point count mismatch: manual=%d, lib=%d", len(manual), len(lib))
190			continue
191		}
192
193		const epsilon = 0.00001
194		for i := 0; i < len(manual); i++ {
195			lonDiff := absFloat64(manual[i][0] - lib[i][0])
196			latDiff := absFloat64(manual[i][1] - lib[i][1])
197			if lonDiff > epsilon || latDiff > epsilon {
198				t.Errorf("point %d mismatch: manual=[%f, %f], lib=[%f, %f]",
199					i, manual[i][0], manual[i][1], lib[i][0], lib[i][1])
200			}
201		}
202	}
203}
204
205func TestEncodeDecode_RoundTrip(t *testing.T) {
206	original := [][]float64{
207		{-74.006, 40.7128},   // NYC
208		{-118.2437, 34.0522}, // LA
209		{-87.6298, 41.8781},  // Chicago
210	}
211	
212	encoded, err := Encode(original)
213	if err != nil {
214		t.Fatalf("encode error: %v", err)
215	}
216	
217	decoded, err := Decode(encoded)
218	if err != nil {
219		t.Fatalf("decode error: %v", err)
220	}
221	
222	if len(decoded) != len(original) {
223		t.Fatalf("expected %d points, got %d", len(original), len(decoded))
224	}
225	
226	for i, pt := range decoded {
227		if abs(pt[0]-original[i][0]) > 0.00001 || abs(pt[1]-original[i][1]) > 0.00001 {
228			t.Errorf("point %d: expected [%f, %f], got [%f, %f]", 
229				i, original[i][0], original[i][1], pt[0], pt[1])
230		}
231	}
232}
233
234func TestEncode_Empty(t *testing.T) {
235	points := [][]float64{}
236	encoded, err := Encode(points)
237	if err != nil {
238		t.Fatalf("encode error: %v", err)
239	}
240	if encoded != "" {
241		t.Errorf("expected empty polyline for empty points, got %q", encoded)
242	}
243}
244
245func TestDecode_Empty(t *testing.T) {
246	points, err := Decode("")
247	if err != nil {
248		t.Fatalf("decode error: %v", err)
249	}
250	if len(points) != 0 {
251		t.Errorf("expected empty points for empty polyline, got %d points", len(points))
252	}
253}
254
255func TestDecode_Invalid(t *testing.T) {
256	_, err := Decode("invalid!@#$")
257	if err == nil {
258		t.Error("expected error for invalid polyline")
259	}
260}
261
262func TestPolyline6_TypeDefinition(t *testing.T) {
263	// Test that Polyline6 is a string type
264	var p Polyline6 = "test_polyline"
265	if string(p) != "test_polyline" {
266		t.Errorf("expected Polyline6 to hold string value")
267	}
268}
269
270func TestPolyline6_JSONRoundTrip(t *testing.T) {
271	// Test JSON marshaling/unmarshaling with escaping
272	// Polyline string containing backslash: _p~iF~ps\U
273	// When stored in Go string: should be stored as-is with backslash
274	// When marshaled to JSON: backslash should be escaped to \\
275	
276	original := Polyline6("_p~iF~ps\\U")
277	
278	// Marshal to JSON
279	data, err := json.Marshal(original)
280	if err != nil {
281		t.Fatalf("marshal error: %v", err)
282	}
283	
284	// Should be quoted and escaped (backslash becomes \\ in JSON)
285	expected := `"_p~iF~ps\\\\U"`
286	if string(data) != expected {
287		t.Errorf("expected %s, got %s", expected, string(data))
288	}
289	
290	// Unmarshal back
291	var decoded Polyline6
292	if err := json.Unmarshal(data, &decoded); err != nil {
293		t.Fatalf("unmarshal error: %v", err)
294	}
295	
296	if decoded != original {
297		t.Errorf("expected %s, got %s", original, decoded)
298	}
299}
300
301func TestPolyline6_Unmarshal_EscapedBackslashes(t *testing.T) {
302	// JSON often has escaped backslashes like \\ which should become \ in the string
303	// Test that \\\\ in JSON becomes \\ in the Go string
304	jsonInput := `"test\\\\path"` // JSON string with escaped backslashes
305	
306	var p Polyline6
307	if err := json.Unmarshal([]byte(jsonInput), &p); err != nil {
308		t.Fatalf("unmarshal error: %v", err)
309	}
310	
311	expected := "test\\path" // Unescaped: single backslash
312	if string(p) != expected {
313		t.Errorf("expected %q, got %q", expected, string(p))
314	}
315}
316
317func TestPolyline6_Marshal_EscapeBackslashes(t *testing.T) {
318	// Go string with backslash should become escaped in JSON
319	p := Polyline6("test\\path") // Go string with backslash
320	
321	data, err := json.Marshal(p)
322	if err != nil {
323		t.Fatalf("marshal error: %v", err)
324	}
325	
326	// Should be properly JSON escaped
327	expected := `"test\\\\path"`
328	if string(data) != expected {
329		t.Errorf("expected %s, got %s", expected, string(data))
330	}
331}
332
333func TestPolyline6_EmptyString(t *testing.T) {
334	// Test empty string handling
335	original := Polyline6("")
336
337	// Marshal to JSON
338	data, err := json.Marshal(original)
339	if err != nil {
340		t.Fatalf("marshal error: %v", err)
341	}
342
343	// Should be quoted empty string
344	expected := `""`
345	if string(data) != expected {
346		t.Errorf("expected %s, got %s", expected, string(data))
347	}
348
349	// Unmarshal back
350	var decoded Polyline6
351	if err := json.Unmarshal(data, &decoded); err != nil {
352		t.Fatalf("unmarshal error: %v", err)
353	}
354
355	if decoded != original {
356		t.Errorf("expected %q, got %q", original, decoded)
357	}
358}
359
360func TestPolyline6_OnlyBackslashes(t *testing.T) {
361	// Test string containing only backslashes
362	// Single backslash in Go string
363	original := Polyline6("\\")
364
365	// Marshal to JSON
366	data, err := json.Marshal(original)
367	if err != nil {
368		t.Fatalf("marshal error: %v", err)
369	}
370
371	// Should be escaped as double backslash in JSON
372	expected := `"\\\\"`
373	if string(data) != expected {
374		t.Errorf("expected %s, got %s", expected, string(data))
375	}
376
377	// Unmarshal back
378	var decoded Polyline6
379	if err := json.Unmarshal(data, &decoded); err != nil {
380		t.Fatalf("unmarshal error: %v", err)
381	}
382
383	if decoded != original {
384		t.Errorf("expected %q, got %q", original, decoded)
385	}
386}
387
388func TestPolyline6_MultipleBackslashes(t *testing.T) {
389	// Test string containing multiple backslashes
390	// Double backslash in Go string
391	original := Polyline6("\\\\")
392
393	// Marshal to JSON
394	data, err := json.Marshal(original)
395	if err != nil {
396		t.Fatalf("marshal error: %v", err)
397	}
398
399	// Should be escaped as quadruple backslash in JSON
400	expected := `"\\\\\\\\"`
401	if string(data) != expected {
402		t.Errorf("expected %s, got %s", expected, string(data))
403	}
404
405	// Unmarshal back
406	var decoded Polyline6
407	if err := json.Unmarshal(data, &decoded); err != nil {
408		t.Fatalf("unmarshal error: %v", err)
409	}
410
411	if decoded != original {
412		t.Errorf("expected %q, got %q", original, decoded)
413	}
414}
415
416func TestCombine(t *testing.T) {
417	// Two polylines that should connect
418	// First: points A -> B -> C
419	firstPoints := [][]float64{
420		{0.0, 0.0},
421		{1.0, 1.0},
422		{2.0, 2.0},
423	}
424	// Second: points C -> D -> E (C is repeated at start)
425	secondPoints := [][]float64{
426		{2.0, 2.0},
427		{3.0, 3.0},
428		{4.0, 4.0},
429	}
430
431	first, _ := Encode(firstPoints)
432	second, _ := Encode(secondPoints)
433
434	combined, err := Combine(first, second)
435	if err != nil {
436		t.Fatalf("combine error: %v", err)
437	}
438
439	// Decode and verify - should have 5 points (A,B,C,D,E) without duplicate C
440	decoded, err := Decode(combined)
441	if err != nil {
442		t.Fatalf("decode error: %v", err)
443	}
444
445	if len(decoded) != 5 {
446		t.Errorf("expected 5 points, got %d", len(decoded))
447	}
448
449	// Check all expected points
450	expected := [][]float64{
451		{0.0, 0.0},
452		{1.0, 1.0},
453		{2.0, 2.0},
454		{3.0, 3.0},
455		{4.0, 4.0},
456	}
457
458	for i, pt := range decoded {
459		if abs(pt[0]-expected[i][0]) > 0.0001 || abs(pt[1]-expected[i][1]) > 0.0001 {
460			t.Errorf("point %d: expected [%f, %f], got [%f, %f]",
461				i, expected[i][0], expected[i][1], pt[0], pt[1])
462		}
463	}
464}
465
466func TestCombine_NoOverlap(t *testing.T) {
467	// Two polylines that don't share endpoints
468	firstPoints := [][]float64{
469		{0.0, 0.0},
470		{1.0, 1.0},
471	}
472	secondPoints := [][]float64{
473		{5.0, 5.0},
474		{6.0, 6.0},
475	}
476
477	first, _ := Encode(firstPoints)
478	second, _ := Encode(secondPoints)
479
480	combined, err := Combine(first, second)
481	if err != nil {
482		t.Fatalf("combine error: %v", err)
483	}
484
485	decoded, err := Decode(combined)
486	if err != nil {
487		t.Fatalf("decode error: %v", err)
488	}
489
490	// Should have all 4 points
491	if len(decoded) != 4 {
492		t.Errorf("expected 4 points, got %d", len(decoded))
493	}
494}
495
496func TestCombine_EmptyFirst(t *testing.T) {
497	secondPoints := [][]float64{
498		{0.0, 0.0},
499		{1.0, 1.0},
500	}
501
502	second, _ := Encode(secondPoints)
503
504	combined, err := Combine("", second)
505	if err != nil {
506		t.Fatalf("combine error: %v", err)
507	}
508
509	decoded, err := Decode(combined)
510	if err != nil {
511		t.Fatalf("decode error: %v", err)
512	}
513
514	if len(decoded) != 2 {
515		t.Errorf("expected 2 points, got %d", len(decoded))
516	}
517}
518
519func TestCombine_EmptySecond(t *testing.T) {
520	firstPoints := [][]float64{
521		{0.0, 0.0},
522		{1.0, 1.0},
523	}
524
525	first, _ := Encode(firstPoints)
526
527	combined, err := Combine(first, "")
528	if err != nil {
529		t.Fatalf("combine error: %v", err)
530	}
531
532	decoded, err := Decode(combined)
533	if err != nil {
534		t.Fatalf("decode error: %v", err)
535	}
536
537	if len(decoded) != 2 {
538		t.Errorf("expected 2 points, got %d", len(decoded))
539	}
540}
541
542func TestCombine_BothEmpty(t *testing.T) {
543	combined, err := Combine("", "")
544	if err != nil {
545		t.Fatalf("combine error: %v", err)
546	}
547
548	if combined != "" {
549		t.Errorf("expected empty polyline, got %q", combined)
550	}
551}
552
553func TestCombine_MultipleOverlappingPoints(t *testing.T) {
554	// First: A -> B -> C -> D
555	firstPoints := [][]float64{
556		{0.0, 0.0},
557		{1.0, 1.0},
558		{2.0, 2.0},
559		{3.0, 3.0},
560	}
561	// Second: C -> D -> E (shares C and D at end of first)
562	secondPoints := [][]float64{
563		{2.0, 2.0},
564		{3.0, 3.0},
565		{4.0, 4.0},
566	}
567
568	first, _ := Encode(firstPoints)
569	second, _ := Encode(secondPoints)
570
571	combined, err := Combine(first, second)
572	if err != nil {
573		t.Fatalf("combine error: %v", err)
574	}
575
576	decoded, err := Decode(combined)
577	if err != nil {
578		t.Fatalf("decode error: %v", err)
579	}
580
581	// Should deduplicate C, keep D (not duplicate), then E = 5 points
582	if len(decoded) != 5 {
583		t.Errorf("expected 5 points, got %d", len(decoded))
584	}
585
586	expected := [][]float64{
587		{0.0, 0.0},
588		{1.0, 1.0},
589		{2.0, 2.0},
590		{3.0, 3.0},
591		{4.0, 4.0},
592	}
593
594	for i, pt := range decoded {
595		if abs(pt[0]-expected[i][0]) > 0.0001 || abs(pt[1]-expected[i][1]) > 0.0001 {
596			t.Errorf("point %d: expected [%f, %f], got [%f, %f]",
597				i, expected[i][0], expected[i][1], pt[0], pt[1])
598		}
599	}
600}
601
602func TestToGeoJSON(t *testing.T) {
603	// Create a simple polyline
604	points := [][]float64{
605		{0.0, 0.0},
606		{1.0, 1.0},
607		{2.0, 2.0},
608	}
609	polyline, _ := Encode(points)
610
611	geojson, err := ToGeoJSON(polyline)
612	if err != nil {
613		t.Fatalf("to geojson error: %v", err)
614	}
615
616	// Check structure
617	if geojson.Type != "Feature" {
618		t.Errorf("expected type Feature, got %s", geojson.Type)
619	}
620
621	if geojson.Geometry.Type != "LineString" {
622		t.Errorf("expected geometry type LineString, got %s", geojson.Geometry.Type)
623	}
624
625	// Check coordinates are in [lon, lat] format
626	if len(geojson.Geometry.Coordinates) != 3 {
627		t.Errorf("expected 3 coordinates, got %d", len(geojson.Geometry.Coordinates))
628	}
629
630	for i, coord := range geojson.Geometry.Coordinates {
631		if len(coord) != 2 {
632			t.Errorf("coordinate %d: expected 2 values, got %d", i, len(coord))
633			continue
634		}
635		if abs(coord[0]-points[i][0]) > 0.0001 || abs(coord[1]-points[i][1]) > 0.0001 {
636			t.Errorf("coordinate %d: expected [%f, %f], got [%f, %f]",
637				i, points[i][0], points[i][1], coord[0], coord[1])
638		}
639	}
640
641	// Check properties exist
642	if geojson.Properties == nil {
643		t.Error("expected properties to be initialized")
644	}
645}
646
647func TestToGeoJSON_WithProperties(t *testing.T) {
648	points := [][]float64{
649		{0.0, 0.0},
650		{1.0, 1.0},
651	}
652	polyline, _ := Encode(points)
653
654	geojson, err := ToGeoJSON(polyline,
655		WithProperty("name", "Test Route"),
656		WithProperty("distance", 100),
657	)
658	if err != nil {
659		t.Fatalf("to geojson error: %v", err)
660	}
661
662	// Check properties
663	name, ok := geojson.Properties["name"].(string)
664	if !ok || name != "Test Route" {
665		t.Errorf("expected name='Test Route', got %v", geojson.Properties["name"])
666	}
667
668	distance, ok := geojson.Properties["distance"].(int)
669	if !ok || distance != 100 {
670		t.Errorf("expected distance=100, got %v", geojson.Properties["distance"])
671	}
672}
673
674func TestToGeoJSON_Empty(t *testing.T) {
675	geojson, err := ToGeoJSON("")
676	if err != nil {
677		t.Fatalf("to geojson error: %v", err)
678	}
679
680	if len(geojson.Geometry.Coordinates) != 0 {
681		t.Errorf("expected empty coordinates, got %d", len(geojson.Geometry.Coordinates))
682	}
683}
684
685func TestToGeoJSON_JSONMarshal(t *testing.T) {
686	points := [][]float64{
687		{-74.006, 40.7128},
688		{-118.2437, 34.0522},
689	}
690	polyline, _ := Encode(points)
691
692	geojson, err := ToGeoJSON(polyline, WithProperty("name", "NYC to LA"))
693	if err != nil {
694		t.Fatalf("to geojson error: %v", err)
695	}
696
697	// Marshal to JSON and verify structure
698	data, err := json.Marshal(geojson)
699	if err != nil {
700		t.Fatalf("marshal error: %v", err)
701	}
702
703	// Unmarshal to verify
704	var result map[string]interface{}
705	if err := json.Unmarshal(data, &result); err != nil {
706		t.Fatalf("unmarshal error: %v", err)
707	}
708
709	if result["type"] != "Feature" {
710		t.Errorf("expected type Feature, got %v", result["type"])
711	}
712
713	geometry, ok := result["geometry"].(map[string]interface{})
714	if !ok {
715		t.Fatal("geometry not found or invalid")
716	}
717
718	if geometry["type"] != "LineString" {
719		t.Errorf("expected geometry type LineString, got %v", geometry["type"])
720	}
721
722	coords, ok := geometry["coordinates"].([]interface{})
723	if !ok || len(coords) != 2 {
724		t.Errorf("expected 2 coordinates, got %v", geometry["coordinates"])
725	}
726}
727
728func TestDecode_RealStadiaAPIData(t *testing.T) {
729	// Read the polyline from testdata
730	polylineData, err := os.ReadFile("testdata/test.polyline")
731	if err != nil {
732		t.Fatalf("failed to read test.polyline: %v", err)
733	}
734	
735	// Read expected coordinates from JSON
736	jsonData, err := os.ReadFile("testdata/test.json")
737	if err != nil {
738		t.Fatalf("failed to read test.json: %v", err)
739	}
740	
741	var expectedPoints [][]float64
742	if err := json.Unmarshal(jsonData, &expectedPoints); err != nil {
743		t.Fatalf("failed to unmarshal test.json: %v", err)
744	}
745	
746	// The polyline file contains the unescaped polyline6 string
747	encoded := Polyline6(strings.TrimSpace(string(polylineData)))
748	
749	// Decode the polyline
750	decoded, err := Decode(encoded)
751	if err != nil {
752		t.Fatalf("decode error: %v", err)
753	}
754	
755	// Verify we got the expected number of points
756	if len(decoded) != len(expectedPoints) {
757		t.Errorf("expected %d points, got %d", len(expectedPoints), len(decoded))
758	}
759	
760	// Verify each point matches within tolerance
761	// polyline6 has ~1e-6 degree precision, but accumulated floating-point errors
762	// in large datasets require slightly looser tolerance
763	const epsilon = 0.00001 // 1e-5 tolerance for real-world data
764	for i := 0; i < min(len(decoded), len(expectedPoints)); i++ {
765		lonDiff := float64(decoded[i][0]) - expectedPoints[i][0]
766		latDiff := float64(decoded[i][1]) - expectedPoints[i][1]
767		
768		if absFloat64(lonDiff) > epsilon || absFloat64(latDiff) > epsilon {
769			t.Errorf("point %d: expected [%f, %f], got [%f, %f] (diff: [%f, %f])", 
770				i, expectedPoints[i][0], expectedPoints[i][1], 
771				decoded[i][0], decoded[i][1],
772				lonDiff, latDiff)
773		}
774	}
775	
776	// Verify first and last points specifically
777	if len(decoded) > 0 && len(expectedPoints) > 0 {
778		firstLonDiff := float64(decoded[0][0]) - expectedPoints[0][0]
779		firstLatDiff := float64(decoded[0][1]) - expectedPoints[0][1]
780		if absFloat64(firstLonDiff) > epsilon || absFloat64(firstLatDiff) > epsilon {
781			t.Errorf("first point mismatch: expected [%f, %f], got [%f, %f]",
782				expectedPoints[0][0], expectedPoints[0][1],
783				decoded[0][0], decoded[0][1])
784		}
785		
786		lastIdx := len(decoded) - 1
787		lastLonDiff := float64(decoded[lastIdx][0]) - expectedPoints[lastIdx][0]
788		lastLatDiff := float64(decoded[lastIdx][1]) - expectedPoints[lastIdx][1]
789		if absFloat64(lastLonDiff) > epsilon || absFloat64(lastLatDiff) > epsilon {
790			t.Errorf("last point mismatch: expected [%f, %f], got [%f, %f]",
791				expectedPoints[lastIdx][0], expectedPoints[lastIdx][1],
792				decoded[lastIdx][0], decoded[lastIdx][1])
793		}
794	}
795}
796
797func absFloat64(f float64) float64 {
798	if f < 0 {
799		return -f
800	}
801	return f
802}
803
804// TestDecode_TruncatedPolyline tests decoding a polyline that was truncated
805// (missing the last byte). This reproduces a bug where the routing API
806// returned a truncated polyline, causing "decode error: empty" errors.
807// The Decode function now handles this gracefully by trimming trailing bytes.
808func TestDecode_TruncatedPolyline(t *testing.T) {
809	// Read the polyline file (which is truncated - missing last byte)
810	polylineData, err := os.ReadFile("testdata/empty-error.polyline6")
811	if err != nil {
812		t.Fatalf("failed to read polyline file: %v", err)
813	}
814	encoded := Polyline6(strings.TrimSpace(string(polylineData)))
815
816	// Read the expected points (JSON has 9607 points, but polyline is truncated)
817	expectedData, err := os.ReadFile("testdata/empty-error.json")
818	if err != nil {
819		t.Fatalf("failed to read expected points file: %v", err)
820	}
821	var expectedPoints [][]float64
822	if err := json.Unmarshal(expectedData, &expectedPoints); err != nil {
823		t.Fatalf("failed to unmarshal expected points: %v", err)
824	}
825
826	t.Logf("Polyline length: %d characters", len(encoded))
827	t.Logf("Expected points: %d", len(expectedPoints))
828	t.Logf("First 100 chars: %s", string(encoded)[:min(100, len(encoded))])
829
830	// The polyline is truncated (missing last byte '?'), so we expect only 9606 points
831	// The Decode function should handle this gracefully
832	decoded, err := Decode(encoded)
833	if err != nil {
834		t.Fatalf("decode error: %v\nPolyline should be decodable with graceful truncation", err)
835	}
836
837	t.Logf("Decoded points: %d (expected 9606 due to truncation)", len(decoded))
838
839	// Verify we got 9606 points (truncated polyline is missing the last point)
840	if len(decoded) != 9606 {
841		t.Errorf("expected 9606 points (truncated), got %d", len(decoded))
842	}
843
844	// Verify first point matches
845	if len(decoded) > 0 && len(expectedPoints) > 0 {
846		tolerance := 0.0001
847		lonDiff := absFloat64(decoded[0][0] - expectedPoints[0][0])
848		latDiff := absFloat64(decoded[0][1] - expectedPoints[0][1])
849		if lonDiff > tolerance || latDiff > tolerance {
850			t.Errorf("first point mismatch: expected [%f, %f], got [%f, %f]",
851				expectedPoints[0][0], expectedPoints[0][1],
852				decoded[0][0], decoded[0][1])
853		}
854	}
855
856	// Verify last decoded point matches expected second-to-last point
857	// (since the polyline is truncated and missing the very last point)
858	if len(decoded) > 0 && len(expectedPoints) >= 2 {
859		lastIdx := len(decoded) - 1
860		expSecondToLastIdx := len(expectedPoints) - 2 // Second to last in expected
861		tolerance := 0.0001
862		lonDiff := absFloat64(decoded[lastIdx][0] - expectedPoints[expSecondToLastIdx][0])
863		latDiff := absFloat64(decoded[lastIdx][1] - expectedPoints[expSecondToLastIdx][1])
864		if lonDiff > tolerance || latDiff > tolerance {
865			t.Errorf("last point mismatch: expected [%f, %f] (second-to-last in expected), got [%f, %f]",
866				expectedPoints[expSecondToLastIdx][0], expectedPoints[expSecondToLastIdx][1],
867				decoded[lastIdx][0], decoded[lastIdx][1])
868		}
869	}
870}