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}