type_test.go
1package waypoint
2
3import (
4 "os"
5 "strings"
6 "testing"
7)
8
9func hasGraphHopperKey() bool {
10 return os.Getenv("GRAPHHOPPER_API_KEY") != ""
11}
12
13func TestToRoutingRequest_EmptyWaypoints(t *testing.T) {
14 reqs, err := ToRoutingRequest()
15 if err != nil {
16 t.Fatalf("unexpected error: %v", err)
17 }
18 if len(reqs) != 0 {
19 t.Errorf("expected empty map, got %d entries", len(reqs))
20 }
21}
22
23func TestToRoutingRequest_SingleWaypoint(t *testing.T) {
24 wp := Waypoint{
25 ID: "wp1",
26 Point: []float64{10.0, 20.0},
27 From: map[string]Route{},
28 }
29 reqs, err := ToRoutingRequest(wp)
30 if err != nil {
31 t.Fatalf("unexpected error: %v", err)
32 }
33 if len(reqs) != 0 {
34 t.Errorf("expected empty map for single waypoint, got %d entries", len(reqs))
35 }
36}
37
38func TestToRoutingRequest_TwoWaypoints_NoExistingRoute(t *testing.T) {
39 if !hasGraphHopperKey() {
40 t.Skip("GRAPHHOPPER_API_KEY not set")
41 }
42
43 wp1 := Waypoint{
44 ID: "wp1",
45 Point: []float64{10.0, 20.0},
46 From: map[string]Route{},
47 }
48 wp2 := Waypoint{
49 ID: "wp2",
50 Point: []float64{30.0, 40.0},
51 From: map[string]Route{
52 "wp1": {}, // No ID means route not calculated yet
53 },
54 }
55
56 reqs, err := ToRoutingRequest(wp1, wp2)
57 if err != nil {
58 t.Fatalf("unexpected error: %v", err)
59 }
60
61 if len(reqs) != 1 {
62 t.Fatalf("expected 1 request, got %d", len(reqs))
63 }
64 if reqs[0].WaypointID != "wp2" {
65 t.Errorf("expected request for wp2, got %s", reqs[0].WaypointID)
66 }
67 if reqs[0].RouteKey != "wp1" {
68 t.Errorf("expected route key 'wp1', got %s", reqs[0].RouteKey)
69 }
70}
71
72func TestToRoutingRequest_TwoWaypoints_ExistingRoute(t *testing.T) {
73 wp1 := Waypoint{
74 ID: "wp1",
75 Point: []float64{10.0, 20.0},
76 From: map[string]Route{},
77 }
78 wp2 := Waypoint{
79 ID: "wp2",
80 Point: []float64{30.0, 40.0},
81 From: map[string]Route{
82 "wp1": {ID: "existing-hash", Distance: 100.0, Time: 60.0},
83 },
84 }
85
86 reqs, err := ToRoutingRequest(wp1, wp2)
87 if err != nil {
88 t.Fatalf("unexpected error: %v", err)
89 }
90
91 if len(reqs) != 0 {
92 t.Errorf("expected no requests when route already exists, got %d entries", len(reqs))
93 }
94}
95
96func TestToRoutingRequest_WithAdventureConstraint(t *testing.T) {
97 if !hasGraphHopperKey() {
98 t.Skip("GRAPHHOPPER_API_KEY not set")
99 }
100
101 wp1 := Waypoint{
102 ID: "wp1",
103 Point: []float64{10.0, 20.0},
104 From: map[string]Route{},
105 }
106 wp2 := Waypoint{
107 ID: "wp2",
108 Point: []float64{30.0, 40.0},
109 From: map[string]Route{
110 "wp1:adv(80)": {}, // High adventure value
111 },
112 }
113
114 reqs, err := ToRoutingRequest(wp1, wp2)
115 if err != nil {
116 t.Fatalf("unexpected error: %v", err)
117 }
118
119 if len(reqs) != 1 {
120 t.Fatalf("expected 1 request, got %d", len(reqs))
121 }
122 if reqs[0].WaypointID != "wp2" {
123 t.Errorf("expected request for wp2, got %s", reqs[0].WaypointID)
124 }
125 if reqs[0].RouteKey != "wp1:adv(80)" {
126 t.Errorf("expected route key 'wp1:adv(80)', got %s", reqs[0].RouteKey)
127 }
128}
129
130func TestToRoutingRequest_WithMaxSpeedConstraint(t *testing.T) {
131 if !hasGraphHopperKey() {
132 t.Skip("GRAPHHOPPER_API_KEY not set")
133 }
134
135 wp1 := Waypoint{
136 ID: "wp1",
137 Point: []float64{10.0, 20.0},
138 From: map[string]Route{},
139 }
140 wp2 := Waypoint{
141 ID: "wp2",
142 Point: []float64{30.0, 40.0},
143 From: map[string]Route{
144 "wp1:max(80)": {}, // Max speed 80 kph
145 },
146 }
147
148 reqs, err := ToRoutingRequest(wp1, wp2)
149 if err != nil {
150 t.Fatalf("unexpected error: %v", err)
151 }
152
153 if len(reqs) != 1 {
154 t.Fatalf("expected 1 destination with requests, got %d", len(reqs))
155 }
156}
157
158func TestToRoutingRequest_WithMultipleConstraints(t *testing.T) {
159 if !hasGraphHopperKey() {
160 t.Skip("GRAPHHOPPER_API_KEY not set")
161 }
162
163 wp1 := Waypoint{
164 ID: "wp1",
165 Point: []float64{10.0, 20.0},
166 From: map[string]Route{},
167 }
168 wp2 := Waypoint{
169 ID: "wp2",
170 Point: []float64{30.0, 40.0},
171 From: map[string]Route{
172 "wp1:adv(60):max(100)": {}, // Adventure 60, max speed 100
173 },
174 }
175
176 reqs, err := ToRoutingRequest(wp1, wp2)
177 if err != nil {
178 t.Fatalf("unexpected error: %v", err)
179 }
180
181 if len(reqs) != 1 {
182 t.Fatalf("expected 1 destination with requests, got %d", len(reqs))
183 }
184}
185
186func TestToRoutingRequest_MultipleRoutesToSameDestination(t *testing.T) {
187 if !hasGraphHopperKey() {
188 t.Skip("GRAPHHOPPER_API_KEY not set")
189 }
190
191 wp1 := Waypoint{
192 ID: "wp1",
193 Point: []float64{10.0, 20.0},
194 From: map[string]Route{},
195 }
196 wp2 := Waypoint{
197 ID: "wp2",
198 Point: []float64{30.0, 40.0},
199 From: map[string]Route{},
200 }
201 wp3 := Waypoint{
202 ID: "wp3",
203 Point: []float64{50.0, 60.0},
204 From: map[string]Route{
205 "wp1": {},
206 "wp2:adv(80)": {},
207 "wp2:adv(20)": {}, // Different constraint, same source
208 },
209 }
210
211 reqs, err := ToRoutingRequest(wp1, wp2, wp3)
212 if err != nil {
213 t.Fatalf("unexpected error: %v", err)
214 }
215
216 if len(reqs) != 3 {
217 t.Fatalf("expected 3 requests, got %d", len(reqs))
218 }
219 // Count requests for wp3
220 wp3Count := 0
221 for _, r := range reqs {
222 if r.WaypointID == "wp3" {
223 wp3Count++
224 }
225 }
226 if wp3Count != 3 {
227 t.Errorf("expected 3 requests for wp3, got %d", wp3Count)
228 }
229}
230
231func TestToRoutingRequest_MissingSourceWaypoint(t *testing.T) {
232 wp1 := Waypoint{
233 ID: "wp1",
234 Point: []float64{10.0, 20.0},
235 From: map[string]Route{},
236 }
237 wp2 := Waypoint{
238 ID: "wp2",
239 Point: []float64{30.0, 40.0},
240 From: map[string]Route{
241 "nonexistent": {}, // Source doesn't exist
242 },
243 }
244
245 _, err := ToRoutingRequest(wp1, wp2)
246 if err == nil {
247 t.Fatal("expected error for non-existent source waypoint")
248 }
249
250 if !strings.Contains(err.Error(), "wp2") || !strings.Contains(err.Error(), "non-existent source") {
251 t.Errorf("expected error to mention waypoint ID and non-existent source, got: %v", err)
252 }
253}
254
255func TestToRoutingRequest_InvalidRouteKey(t *testing.T) {
256 wp1 := Waypoint{
257 ID: "wp1",
258 Point: []float64{10.0, 20.0},
259 From: map[string]Route{},
260 }
261 wp2 := Waypoint{
262 ID: "wp2",
263 Point: []float64{30.0, 40.0},
264 From: map[string]Route{
265 "wp1:invalid": {}, // Invalid constraint format
266 },
267 }
268
269 _, err := ToRoutingRequest(wp1, wp2)
270 if err == nil {
271 t.Fatal("expected error for invalid route key")
272 }
273
274 if !strings.Contains(err.Error(), "wp2") {
275 t.Errorf("expected error to mention waypoint ID, got: %v", err)
276 }
277}
278
279func TestToRoutingRequest_InvalidAdventureValue(t *testing.T) {
280 wp1 := Waypoint{
281 ID: "wp1",
282 Point: []float64{10.0, 20.0},
283 From: map[string]Route{},
284 }
285 wp2 := Waypoint{
286 ID: "wp2",
287 Point: []float64{30.0, 40.0},
288 From: map[string]Route{
289 "wp1:adv(notanumber)": {},
290 },
291 }
292
293 _, err := ToRoutingRequest(wp1, wp2)
294 if err == nil {
295 t.Fatal("expected error for invalid adventure value")
296 }
297
298 if !strings.Contains(err.Error(), "wp2") || !strings.Contains(err.Error(), "invalid adventure value") {
299 t.Errorf("expected error to mention waypoint ID and adventure value, got: %v", err)
300 }
301}
302
303func TestToRoutingRequest_InvalidMaxSpeedValue(t *testing.T) {
304 if !hasGraphHopperKey() {
305 t.Skip("GRAPHHOPPER_API_KEY not set")
306 }
307
308 wp1 := Waypoint{
309 ID: "wp1",
310 Point: []float64{10.0, 20.0},
311 From: map[string]Route{},
312 }
313 wp2 := Waypoint{
314 ID: "wp2",
315 Point: []float64{30.0, 40.0},
316 From: map[string]Route{
317 "wp1:max(notanumber)": {},
318 },
319 }
320
321 _, err := ToRoutingRequest(wp1, wp2)
322 if err == nil {
323 t.Fatal("expected error for invalid maxspeed value")
324 }
325
326 if !strings.Contains(err.Error(), "wp2") || !strings.Contains(err.Error(), "invalid maxspeed value") {
327 t.Errorf("expected error to mention waypoint ID and maxspeed value, got: %v", err)
328 }
329}
330
331func TestToRoutingRequest_InvalidPointCoordinates(t *testing.T) {
332 wp1 := Waypoint{
333 ID: "wp1",
334 Point: []float64{10.0}, // Only one coordinate - invalid
335 From: map[string]Route{},
336 }
337 wp2 := Waypoint{
338 ID: "wp2",
339 Point: []float64{30.0, 40.0},
340 From: map[string]Route{
341 "wp1": {},
342 },
343 }
344
345 _, err := ToRoutingRequest(wp1, wp2)
346 if err == nil {
347 t.Fatal("expected error for invalid point coordinates")
348 }
349
350 if !strings.Contains(err.Error(), "wp1") || !strings.Contains(err.Error(), "invalid point") {
351 t.Errorf("expected error to mention waypoint ID and invalid point, got: %v", err)
352 }
353}
354
355func TestToRoutingRequest_EmptyPointCoordinates(t *testing.T) {
356 wp1 := Waypoint{
357 ID: "wp1",
358 Point: []float64{}, // Empty - invalid
359 From: map[string]Route{},
360 }
361 wp2 := Waypoint{
362 ID: "wp2",
363 Point: []float64{30.0, 40.0},
364 From: map[string]Route{
365 "wp1": {},
366 },
367 }
368
369 _, err := ToRoutingRequest(wp1, wp2)
370 if err == nil {
371 t.Fatal("expected error for empty point coordinates")
372 }
373}
374
375func TestToRoutingRequest_NonRoutingConstraintsIgnored(t *testing.T) {
376 if !hasGraphHopperKey() {
377 t.Skip("GRAPHHOPPER_API_KEY not set")
378 }
379
380 wp1 := Waypoint{
381 ID: "wp1",
382 Point: []float64{10.0, 20.0},
383 From: map[string]Route{},
384 }
385 wp2 := Waypoint{
386 ID: "wp2",
387 Point: []float64{30.0, 40.0},
388 Cons: []string{"dkm(200)", "bbt(120)"}, // Non-routing constraints
389 From: map[string]Route{
390 "wp1:dkm(300):bbt(180)": {}, // These should be ignored for routing
391 },
392 }
393
394 reqs, err := ToRoutingRequest(wp1, wp2)
395 if err != nil {
396 t.Fatalf("unexpected error: %v", err)
397 }
398
399 if len(reqs) != 1 {
400 t.Fatalf("expected 1 request, got %d", len(reqs))
401 }
402 // Request should be created successfully even with non-routing constraints
403 if reqs[0].WaypointID != "wp2" {
404 t.Errorf("expected request for wp2, got %s", reqs[0].WaypointID)
405 }
406}
407
408// Test ToRouteKey
409func TestToRouteKey_NoConstraints(t *testing.T) {
410 from := Waypoint{ID: "wp1", Point: []float64{10.0, 20.0}}
411 to := Waypoint{ID: "wp2", Point: []float64{30.0, 40.0}}
412
413 key, err := ToRouteKey(from, to, false)
414 if err != nil {
415 t.Fatalf("unexpected error: %v", err)
416 }
417
418 if key != "wp1" {
419 t.Errorf("expected key 'wp1', got %s", key)
420 }
421}
422
423func TestToRouteKey_WithAdventure(t *testing.T) {
424 from := Waypoint{ID: "wp1", Point: []float64{10.0, 20.0}}
425 to := Waypoint{
426 ID: "wp2",
427 Point: []float64{30.0, 40.0},
428 Cons: []string{"adv(80)"},
429 }
430
431 key, err := ToRouteKey(from, to, false)
432 if err != nil {
433 t.Fatalf("unexpected error: %v", err)
434 }
435
436 expected := "wp1:adv(80)"
437 if key != expected {
438 t.Errorf("expected key '%s', got %s", expected, key)
439 }
440}
441
442func TestToRouteKey_WithMaxSpeed(t *testing.T) {
443 from := Waypoint{ID: "wp1", Point: []float64{10.0, 20.0}}
444 to := Waypoint{
445 ID: "wp2",
446 Point: []float64{30.0, 40.0},
447 Cons: []string{"max(100)"},
448 }
449
450 key, err := ToRouteKey(from, to, false)
451 if err != nil {
452 t.Fatalf("unexpected error: %v", err)
453 }
454
455 expected := "wp1:max(100)"
456 if key != expected {
457 t.Errorf("expected key '%s', got %s", expected, key)
458 }
459}
460
461func TestToRouteKey_WithWaypointLocationType(t *testing.T) {
462 from := Waypoint{ID: "wp1", Point: []float64{10.0, 20.0}}
463 to := Waypoint{
464 ID: "wp2",
465 Point: []float64{30.0, 40.0},
466 Cons: []string{"wlt(v)"},
467 }
468
469 key, err := ToRouteKey(from, to, false)
470 if err != nil {
471 t.Fatalf("unexpected error: %v", err)
472 }
473
474 expected := "wp1:wlt(v)"
475 if key != expected {
476 t.Errorf("expected key '%s', got %s", expected, key)
477 }
478}
479
480func TestToRouteKey_MultipleConstraints(t *testing.T) {
481 from := Waypoint{ID: "wp1", Point: []float64{10.0, 20.0}}
482 to := Waypoint{
483 ID: "wp2",
484 Point: []float64{30.0, 40.0},
485 Cons: []string{"adv(80)", "max(100)", "wlt(t)"},
486 }
487
488 key, err := ToRouteKey(from, to, false)
489 if err != nil {
490 t.Fatalf("unexpected error: %v", err)
491 }
492
493 // Should contain all three constraints
494 if !strings.Contains(key, "wp1") {
495 t.Error("key should start with source waypoint ID")
496 }
497 if !strings.Contains(key, "adv(80)") {
498 t.Error("key should contain adventure constraint")
499 }
500 if !strings.Contains(key, "max(100)") {
501 t.Error("key should contain maxspeed constraint")
502 }
503 if !strings.Contains(key, "wlt(t)") {
504 t.Error("key should contain waypoint location type constraint")
505 }
506}
507
508func TestToRouteKey_NonRoutingConstraintsIgnored(t *testing.T) {
509 from := Waypoint{ID: "wp1", Point: []float64{10.0, 20.0}}
510 to := Waypoint{
511 ID: "wp2",
512 Point: []float64{30.0, 40.0},
513 Cons: []string{"dkm(200)", "bbt(120)", "dat(0800)", "abt(1800)"},
514 }
515
516 key, err := ToRouteKey(from, to, false)
517 if err != nil {
518 t.Fatalf("unexpected error: %v", err)
519 }
520
521 // Should only contain source ID, non-routing constraints should be ignored
522 if key != "wp1" {
523 t.Errorf("expected key 'wp1' (non-routing constraints ignored), got %s", key)
524 }
525}
526
527func TestToRouteKey_EmptyFromID(t *testing.T) {
528 from := Waypoint{ID: "", Point: []float64{10.0, 20.0}}
529 to := Waypoint{ID: "wp2", Point: []float64{30.0, 40.0}}
530
531 _, err := ToRouteKey(from, to, false)
532 if err == nil {
533 t.Fatal("expected error for empty from ID")
534 }
535}
536
537func TestToRouteKey_InvalidConstraint(t *testing.T) {
538 from := Waypoint{ID: "wp1", Point: []float64{10.0, 20.0}}
539 to := Waypoint{
540 ID: "wp2",
541 Point: []float64{30.0, 40.0},
542 Cons: []string{"invalid"},
543 }
544
545 _, err := ToRouteKey(from, to, false)
546 if err == nil {
547 t.Fatal("expected error for invalid constraint")
548 }
549}
550
551// Test FromRouteKey
552func TestFromRouteKey_NoConstraints(t *testing.T) {
553 fromID, cons, err := FromRouteKey("wp1")
554 if err != nil {
555 t.Fatalf("unexpected error: %v", err)
556 }
557
558 if fromID != "wp1" {
559 t.Errorf("expected fromID 'wp1', got %s", fromID)
560 }
561
562 if len(cons) != 0 {
563 t.Errorf("expected 0 constraints, got %d", len(cons))
564 }
565}
566
567func TestFromRouteKey_WithConstraints(t *testing.T) {
568 fromID, cons, err := FromRouteKey("wp1:adv(80):max(100)")
569 if err != nil {
570 t.Fatalf("unexpected error: %v", err)
571 }
572
573 if fromID != "wp1" {
574 t.Errorf("expected fromID 'wp1', got %s", fromID)
575 }
576
577 if len(cons) != 2 {
578 t.Fatalf("expected 2 constraints, got %d", len(cons))
579 }
580
581 // Check first constraint
582 if cons[0].Type != Adventure || cons[0].Value != "80" {
583 t.Errorf("expected adventure constraint with value 80, got %v", cons[0])
584 }
585
586 // Check second constraint
587 if cons[1].Type != MaxSpeed || cons[1].Value != "100" {
588 t.Errorf("expected maxspeed constraint with value 100, got %v", cons[1])
589 }
590}
591
592func TestFromRouteKey_InvalidConstraint(t *testing.T) {
593 _, _, err := FromRouteKey("wp1:invalid")
594 if err == nil {
595 t.Fatal("expected error for invalid constraint format")
596 }
597}
598
599// Test ConstraintFromString
600func TestConstraintFromString_ValidConstraints(t *testing.T) {
601 tests := []struct {
602 input string
603 expected ConstraintType
604 value string
605 }{
606 {"adv(80)", Adventure, "80"},
607 {"max(100)", MaxSpeed, "100"},
608 {"dkm(200)", DailyKMs, "200"},
609 {"bbd(150)", BreakBeforeDist, "150"},
610 {"bbt(120)", BreakBeforeTime, "120"},
611 {"dat(0800)", DepartAfterTime, "0800"},
612 {"abt(1800)", ArriveBefore, "1800"},
613 {"wlt(v)", WaypointLocationType, "v"},
614 }
615
616 for _, test := range tests {
617 c, err := ConstraintFromString(test.input)
618 if err != nil {
619 t.Errorf("unexpected error for %s: %v", test.input, err)
620 continue
621 }
622 if c.Type != test.expected {
623 t.Errorf("expected type %v for %s, got %v", test.expected, test.input, c.Type)
624 }
625 if c.Value != test.value {
626 t.Errorf("expected value %s for %s, got %s", test.value, test.input, c.Value)
627 }
628 }
629}
630
631func TestConstraintFromString_InvalidFormat(t *testing.T) {
632 invalidInputs := []string{
633 "adv80",
634 "adv(80",
635 "unknown(value)",
636 "",
637 "adv",
638 }
639
640 for _, input := range invalidInputs {
641 _, err := ConstraintFromString(input)
642 if err == nil {
643 t.Errorf("expected error for input: %s", input)
644 }
645 }
646}
647
648// Test ParseConstraints
649func TestParseConstraints(t *testing.T) {
650 inputs := []string{
651 "adv(80)",
652 "max(100)",
653 "invalid",
654 "dkm(200)",
655 }
656
657 cons := ParseConstraints(inputs)
658
659 // Should parse 3 valid constraints, skip the invalid one
660 if len(cons) != 3 {
661 t.Errorf("expected 3 constraints (skipping invalid), got %d", len(cons))
662 }
663
664 // Verify the types
665 expectedTypes := []ConstraintType{Adventure, MaxSpeed, DailyKMs}
666 for i, expected := range expectedTypes {
667 if cons[i].Type != expected {
668 t.Errorf("expected constraint %d to be %v, got %v", i, expected, cons[i].Type)
669 }
670 }
671}
672
673func TestParseConstraints_Empty(t *testing.T) {
674 cons := ParseConstraints([]string{})
675 if len(cons) != 0 {
676 t.Errorf("expected empty constraints, got %d", len(cons))
677 }
678}
679
680// Test wpToLocation
681func TestWpToLocation_Default(t *testing.T) {
682 wp := Waypoint{
683 ID: "wp1",
684 Point: []float64{10.5, 20.5},
685 }
686
687 loc, err := wpToLocation(wp)
688 if err != nil {
689 t.Fatalf("unexpected error: %v", err)
690 }
691
692 // PointLngLat is [2]float64 where index 0 = lon, index 1 = lat
693 if loc[0] != 10.5 || loc[1] != 20.5 {
694 t.Errorf("expected coordinates (lon=10.5, lat=20.5), got (lon=%f, lat=%f)", loc[0], loc[1])
695 }
696}
697
698// Note: TestWpToLocation_WithLocationType removed because Graphhopper doesn't support
699// location types (break, through, via, etc.) like Valhalla did. The wlt() constraint
700// is still parsed for route key purposes but doesn't affect the location structure.
701
702func TestWpToLocation_InvalidPoint(t *testing.T) {
703 tests := []struct {
704 name string
705 point []float64
706 }{
707 {"empty", []float64{}},
708 {"one coordinate", []float64{10.0}},
709 }
710
711 for _, test := range tests {
712 t.Run(test.name, func(t *testing.T) {
713 wp := Waypoint{
714 ID: "wp1",
715 Point: test.point,
716 }
717
718 _, err := wpToLocation(wp)
719 if err == nil {
720 t.Fatal("expected error for invalid point coordinates")
721 }
722
723 if !strings.Contains(err.Error(), "wp1") || !strings.Contains(err.Error(), "invalid point") {
724 t.Errorf("expected error to mention waypoint ID and invalid point, got: %v", err)
725 }
726 })
727 }
728}