blackopsrepl commited on
Commit
03e3b1b
·
0 Parent(s):

feat(app): add SolverForge deliveries tutorial app

Browse files

Introduce the runnable deliveries tutorial as a first buildable repository state. The commit adds the Rust 1.95 package manifest and lockfile, published SolverForge dependencies, solver policy, scaffold metadata, and the generated UI metadata needed by the app runtime.

The domain model teaches the list-variable routing shape through a Plan solution, Delivery problem facts, Vehicle planning entities, route shadow values, CVRP construction/k-opt hooks, and route-metric helpers. The scoring model covers assignment coverage, capacity, delivery time windows, unreachable legs, and travel-time soft cost.

The backend adds retained SolverForge jobs over SolverManager<Plan>, REST routes, DTO conversion, SSE bootstrap/live events, route geometry, insertion recommendations, and error mapping. The browser app uses stock solverforge-ui assets with local modules for plan normalization, draft/server previews, route lists, non-panning route highlighting, data tables, timelines, modals, and the visible API guide.

This commit deliberately includes app source, runtime static assets, config, and dependency metadata together so the unborn repository starts from a coherent, buildable application state. Later commits add external validation, Space tooling, and prose documentation as separate revert boundaries.

This view is limited to 50 files because it contains too many changes.   See raw diff
Files changed (50) hide show
  1. .gitignore +5 -0
  2. Cargo.lock +2746 -0
  3. Cargo.toml +37 -0
  4. solver.toml +76 -0
  5. solverforge.app.toml +62 -0
  6. src/api/dto.rs +230 -0
  7. src/api/dto/runtime.rs +83 -0
  8. src/api/dto/tests.rs +38 -0
  9. src/api/errors.rs +35 -0
  10. src/api/mod.rs +12 -0
  11. src/api/routes.rs +288 -0
  12. src/api/sse.rs +41 -0
  13. src/constraints/all_deliveries_assigned.rs +26 -0
  14. src/constraints/delivery_time_windows.rs +16 -0
  15. src/constraints/mod.rs +32 -0
  16. src/constraints/total_travel_time.rs +11 -0
  17. src/constraints/vehicle_capacity.rs +14 -0
  18. src/data/data_seed.rs +10 -0
  19. src/data/data_seed/entrypoints.rs +127 -0
  20. src/data/data_seed/firenze.rs +8 -0
  21. src/data/data_seed/firenze/depots.rs +64 -0
  22. src/data/data_seed/firenze/visits.rs +292 -0
  23. src/data/data_seed/firenze/visits_extra.rs +196 -0
  24. src/data/data_seed/hartford.rs +8 -0
  25. src/data/data_seed/hartford/depots.rs +64 -0
  26. src/data/data_seed/hartford/visits.rs +184 -0
  27. src/data/data_seed/hartford/visits_extra.rs +124 -0
  28. src/data/data_seed/philadelphia.rs +8 -0
  29. src/data/data_seed/philadelphia/depots.rs +64 -0
  30. src/data/data_seed/philadelphia/visits.rs +298 -0
  31. src/data/data_seed/philadelphia/visits_extra.rs +202 -0
  32. src/data/data_seed/tests.rs +123 -0
  33. src/data/data_seed/types.rs +48 -0
  34. src/data/mod.rs +3 -0
  35. src/domain/coord_value.rs +39 -0
  36. src/domain/delivery.rs +95 -0
  37. src/domain/mod.rs +42 -0
  38. src/domain/plan.rs +169 -0
  39. src/domain/plan_tests.rs +243 -0
  40. src/domain/preview.rs +105 -0
  41. src/domain/route_metrics/cvrp_hooks.rs +185 -0
  42. src/domain/route_metrics/helpers.rs +106 -0
  43. src/domain/route_metrics/insertions.rs +66 -0
  44. src/domain/route_metrics/metrics.rs +144 -0
  45. src/domain/route_metrics/mod.rs +26 -0
  46. src/domain/route_metrics/preparation.rs +184 -0
  47. src/domain/route_metrics/routes.rs +168 -0
  48. src/domain/route_metrics/scoring.rs +186 -0
  49. src/domain/route_metrics/tests.rs +92 -0
  50. src/domain/route_metrics/types.rs +121 -0
.gitignore ADDED
@@ -0,0 +1,5 @@
 
 
 
 
 
 
1
+ /target
2
+ .osm_cache/
3
+ **/*.rs.bk
4
+ test-results/
5
+ playwright-report/
Cargo.lock ADDED
@@ -0,0 +1,2746 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ # This file is automatically @generated by Cargo.
2
+ # It is not intended for manual editing.
3
+ version = 4
4
+
5
+ [[package]]
6
+ name = "aho-corasick"
7
+ version = "1.1.4"
8
+ source = "registry+https://github.com/rust-lang/crates.io-index"
9
+ checksum = "ddd31a130427c27518df266943a5308ed92d4b226cc639f5a8f1002816174301"
10
+ dependencies = [
11
+ "memchr",
12
+ ]
13
+
14
+ [[package]]
15
+ name = "anyhow"
16
+ version = "1.0.102"
17
+ source = "registry+https://github.com/rust-lang/crates.io-index"
18
+ checksum = "7f202df86484c868dbad7eaa557ef785d5c66295e41b460ef922eca0723b842c"
19
+
20
+ [[package]]
21
+ name = "arrayvec"
22
+ version = "0.7.6"
23
+ source = "registry+https://github.com/rust-lang/crates.io-index"
24
+ checksum = "7c02d123df017efcdfbd739ef81735b36c5ba83ec3c59c80a9d7ecc718f92e50"
25
+
26
+ [[package]]
27
+ name = "atomic-waker"
28
+ version = "1.1.2"
29
+ source = "registry+https://github.com/rust-lang/crates.io-index"
30
+ checksum = "1505bd5d3d116872e7271a6d4e16d81d0c8570876c8de68093a09ac269d8aac0"
31
+
32
+ [[package]]
33
+ name = "aws-lc-rs"
34
+ version = "1.16.3"
35
+ source = "registry+https://github.com/rust-lang/crates.io-index"
36
+ checksum = "0ec6fb3fe69024a75fa7e1bfb48aa6cf59706a101658ea01bfd33b2b248a038f"
37
+ dependencies = [
38
+ "aws-lc-sys",
39
+ "zeroize",
40
+ ]
41
+
42
+ [[package]]
43
+ name = "aws-lc-sys"
44
+ version = "0.40.0"
45
+ source = "registry+https://github.com/rust-lang/crates.io-index"
46
+ checksum = "f50037ee5e1e41e7b8f9d161680a725bd1626cb6f8c7e901f91f942850852fe7"
47
+ dependencies = [
48
+ "cc",
49
+ "cmake",
50
+ "dunce",
51
+ "fs_extra",
52
+ ]
53
+
54
+ [[package]]
55
+ name = "axum"
56
+ version = "0.8.9"
57
+ source = "registry+https://github.com/rust-lang/crates.io-index"
58
+ checksum = "31b698c5f9a010f6573133b09e0de5408834d0c82f8d7475a89fc1867a71cd90"
59
+ dependencies = [
60
+ "axum-core",
61
+ "bytes",
62
+ "form_urlencoded",
63
+ "futures-util",
64
+ "http",
65
+ "http-body",
66
+ "http-body-util",
67
+ "hyper",
68
+ "hyper-util",
69
+ "itoa",
70
+ "matchit",
71
+ "memchr",
72
+ "mime",
73
+ "percent-encoding",
74
+ "pin-project-lite",
75
+ "serde_core",
76
+ "serde_json",
77
+ "serde_path_to_error",
78
+ "serde_urlencoded",
79
+ "sync_wrapper",
80
+ "tokio",
81
+ "tower",
82
+ "tower-layer",
83
+ "tower-service",
84
+ "tracing",
85
+ ]
86
+
87
+ [[package]]
88
+ name = "axum-core"
89
+ version = "0.5.6"
90
+ source = "registry+https://github.com/rust-lang/crates.io-index"
91
+ checksum = "08c78f31d7b1291f7ee735c1c6780ccde7785daae9a9206026862dab7d8792d1"
92
+ dependencies = [
93
+ "bytes",
94
+ "futures-core",
95
+ "http",
96
+ "http-body",
97
+ "http-body-util",
98
+ "mime",
99
+ "pin-project-lite",
100
+ "sync_wrapper",
101
+ "tower-layer",
102
+ "tower-service",
103
+ "tracing",
104
+ ]
105
+
106
+ [[package]]
107
+ name = "base64"
108
+ version = "0.22.1"
109
+ source = "registry+https://github.com/rust-lang/crates.io-index"
110
+ checksum = "72b3254f16251a8381aa12e40e3c4d2f0199f8c6508fbecb9d91f575e0fbb8c6"
111
+
112
+ [[package]]
113
+ name = "bitflags"
114
+ version = "2.11.1"
115
+ source = "registry+https://github.com/rust-lang/crates.io-index"
116
+ checksum = "c4512299f36f043ab09a583e57bceb5a5aab7a73db1805848e8fef3c9e8c78b3"
117
+
118
+ [[package]]
119
+ name = "bumpalo"
120
+ version = "3.20.2"
121
+ source = "registry+https://github.com/rust-lang/crates.io-index"
122
+ checksum = "5d20789868f4b01b2f2caec9f5c4e0213b41e3e5702a50157d699ae31ced2fcb"
123
+
124
+ [[package]]
125
+ name = "bytes"
126
+ version = "1.11.1"
127
+ source = "registry+https://github.com/rust-lang/crates.io-index"
128
+ checksum = "1e748733b7cbc798e1434b6ac524f0c1ff2ab456fe201501e6497c8417a4fc33"
129
+
130
+ [[package]]
131
+ name = "cc"
132
+ version = "1.2.61"
133
+ source = "registry+https://github.com/rust-lang/crates.io-index"
134
+ checksum = "d16d90359e986641506914ba71350897565610e87ce0ad9e6f28569db3dd5c6d"
135
+ dependencies = [
136
+ "find-msvc-tools",
137
+ "jobserver",
138
+ "libc",
139
+ "shlex",
140
+ ]
141
+
142
+ [[package]]
143
+ name = "cesu8"
144
+ version = "1.1.0"
145
+ source = "registry+https://github.com/rust-lang/crates.io-index"
146
+ checksum = "6d43a04d8753f35258c91f8ec639f792891f748a1edbd759cf1dcea3382ad83c"
147
+
148
+ [[package]]
149
+ name = "cfg-if"
150
+ version = "1.0.4"
151
+ source = "registry+https://github.com/rust-lang/crates.io-index"
152
+ checksum = "9330f8b2ff13f34540b44e946ef35111825727b38d33286ef986142615121801"
153
+
154
+ [[package]]
155
+ name = "cfg_aliases"
156
+ version = "0.2.1"
157
+ source = "registry+https://github.com/rust-lang/crates.io-index"
158
+ checksum = "613afe47fcd5fac7ccf1db93babcb082c5994d996f20b8b159f2ad1658eb5724"
159
+
160
+ [[package]]
161
+ name = "chacha20"
162
+ version = "0.10.0"
163
+ source = "registry+https://github.com/rust-lang/crates.io-index"
164
+ checksum = "6f8d983286843e49675a4b7a2d174efe136dc93a18d69130dd18198a6c167601"
165
+ dependencies = [
166
+ "cfg-if",
167
+ "cpufeatures",
168
+ "rand_core 0.10.1",
169
+ ]
170
+
171
+ [[package]]
172
+ name = "cmake"
173
+ version = "0.1.58"
174
+ source = "registry+https://github.com/rust-lang/crates.io-index"
175
+ checksum = "c0f78a02292a74a88ac736019ab962ece0bc380e3f977bf72e376c5d78ff0678"
176
+ dependencies = [
177
+ "cc",
178
+ ]
179
+
180
+ [[package]]
181
+ name = "combine"
182
+ version = "4.6.7"
183
+ source = "registry+https://github.com/rust-lang/crates.io-index"
184
+ checksum = "ba5a308b75df32fe02788e748662718f03fde005016435c444eea572398219fd"
185
+ dependencies = [
186
+ "bytes",
187
+ "memchr",
188
+ ]
189
+
190
+ [[package]]
191
+ name = "core-foundation"
192
+ version = "0.9.4"
193
+ source = "registry+https://github.com/rust-lang/crates.io-index"
194
+ checksum = "91e195e091a93c46f7102ec7818a2aa394e1e1771c3ab4825963fa03e45afb8f"
195
+ dependencies = [
196
+ "core-foundation-sys",
197
+ "libc",
198
+ ]
199
+
200
+ [[package]]
201
+ name = "core-foundation"
202
+ version = "0.10.1"
203
+ source = "registry+https://github.com/rust-lang/crates.io-index"
204
+ checksum = "b2a6cd9ae233e7f62ba4e9353e81a88df7fc8a5987b8d445b4d90c879bd156f6"
205
+ dependencies = [
206
+ "core-foundation-sys",
207
+ "libc",
208
+ ]
209
+
210
+ [[package]]
211
+ name = "core-foundation-sys"
212
+ version = "0.8.7"
213
+ source = "registry+https://github.com/rust-lang/crates.io-index"
214
+ checksum = "773648b94d0e5d620f64f280777445740e61fe701025087ec8b57f45c791888b"
215
+
216
+ [[package]]
217
+ name = "cpufeatures"
218
+ version = "0.3.0"
219
+ source = "registry+https://github.com/rust-lang/crates.io-index"
220
+ checksum = "8b2a41393f66f16b0823bb79094d54ac5fbd34ab292ddafb9a0456ac9f87d201"
221
+ dependencies = [
222
+ "libc",
223
+ ]
224
+
225
+ [[package]]
226
+ name = "crossbeam-deque"
227
+ version = "0.8.6"
228
+ source = "registry+https://github.com/rust-lang/crates.io-index"
229
+ checksum = "9dd111b7b7f7d55b72c0a6ae361660ee5853c9af73f70c3c2ef6858b950e2e51"
230
+ dependencies = [
231
+ "crossbeam-epoch",
232
+ "crossbeam-utils",
233
+ ]
234
+
235
+ [[package]]
236
+ name = "crossbeam-epoch"
237
+ version = "0.9.18"
238
+ source = "registry+https://github.com/rust-lang/crates.io-index"
239
+ checksum = "5b82ac4a3c2ca9c3460964f020e1402edd5753411d7737aa39c3714ad1b5420e"
240
+ dependencies = [
241
+ "crossbeam-utils",
242
+ ]
243
+
244
+ [[package]]
245
+ name = "crossbeam-utils"
246
+ version = "0.8.21"
247
+ source = "registry+https://github.com/rust-lang/crates.io-index"
248
+ checksum = "d0a5c400df2834b80a4c3327b3aad3a4c4cd4de0629063962b03235697506a28"
249
+
250
+ [[package]]
251
+ name = "displaydoc"
252
+ version = "0.2.5"
253
+ source = "registry+https://github.com/rust-lang/crates.io-index"
254
+ checksum = "97369cbbc041bc366949bc74d34658d6cda5621039731c6310521892a3a20ae0"
255
+ dependencies = [
256
+ "proc-macro2",
257
+ "quote",
258
+ "syn",
259
+ ]
260
+
261
+ [[package]]
262
+ name = "dunce"
263
+ version = "1.0.5"
264
+ source = "registry+https://github.com/rust-lang/crates.io-index"
265
+ checksum = "92773504d58c093f6de2459af4af33faa518c13451eb8f2b5698ed3d36e7c813"
266
+
267
+ [[package]]
268
+ name = "either"
269
+ version = "1.15.0"
270
+ source = "registry+https://github.com/rust-lang/crates.io-index"
271
+ checksum = "48c757948c5ede0e46177b7add2e67155f70e33c07fea8284df6576da70b3719"
272
+
273
+ [[package]]
274
+ name = "encoding_rs"
275
+ version = "0.8.35"
276
+ source = "registry+https://github.com/rust-lang/crates.io-index"
277
+ checksum = "75030f3c4f45dafd7586dd6780965a8c7e8e285a5ecb86713e63a79c5b2766f3"
278
+ dependencies = [
279
+ "cfg-if",
280
+ ]
281
+
282
+ [[package]]
283
+ name = "equivalent"
284
+ version = "1.0.2"
285
+ source = "registry+https://github.com/rust-lang/crates.io-index"
286
+ checksum = "877a4ace8713b0bcf2a4e7eec82529c029f1d0619886d18145fea96c3ffe5c0f"
287
+
288
+ [[package]]
289
+ name = "errno"
290
+ version = "0.3.14"
291
+ source = "registry+https://github.com/rust-lang/crates.io-index"
292
+ checksum = "39cab71617ae0d63f51a36d69f866391735b51691dbda63cf6f96d042b63efeb"
293
+ dependencies = [
294
+ "libc",
295
+ "windows-sys 0.61.2",
296
+ ]
297
+
298
+ [[package]]
299
+ name = "find-msvc-tools"
300
+ version = "0.1.9"
301
+ source = "registry+https://github.com/rust-lang/crates.io-index"
302
+ checksum = "5baebc0774151f905a1a2cc41989300b1e6fbb29aff0ceffa1064fdd3088d582"
303
+
304
+ [[package]]
305
+ name = "fnv"
306
+ version = "1.0.7"
307
+ source = "registry+https://github.com/rust-lang/crates.io-index"
308
+ checksum = "3f9eec918d3f24069decb9af1554cad7c880e2da24a9afd88aca000531ab82c1"
309
+
310
+ [[package]]
311
+ name = "foldhash"
312
+ version = "0.1.5"
313
+ source = "registry+https://github.com/rust-lang/crates.io-index"
314
+ checksum = "d9c4f5dac5e15c24eb999c26181a6ca40b39fe946cbe4c263c7209467bc83af2"
315
+
316
+ [[package]]
317
+ name = "form_urlencoded"
318
+ version = "1.2.2"
319
+ source = "registry+https://github.com/rust-lang/crates.io-index"
320
+ checksum = "cb4cb245038516f5f85277875cdaa4f7d2c9a0fa0468de06ed190163b1581fcf"
321
+ dependencies = [
322
+ "percent-encoding",
323
+ ]
324
+
325
+ [[package]]
326
+ name = "fs_extra"
327
+ version = "1.3.0"
328
+ source = "registry+https://github.com/rust-lang/crates.io-index"
329
+ checksum = "42703706b716c37f96a77aea830392ad231f44c9e9a67872fa5548707e11b11c"
330
+
331
+ [[package]]
332
+ name = "futures-channel"
333
+ version = "0.3.32"
334
+ source = "registry+https://github.com/rust-lang/crates.io-index"
335
+ checksum = "07bbe89c50d7a535e539b8c17bc0b49bdb77747034daa8087407d655f3f7cc1d"
336
+ dependencies = [
337
+ "futures-core",
338
+ ]
339
+
340
+ [[package]]
341
+ name = "futures-core"
342
+ version = "0.3.32"
343
+ source = "registry+https://github.com/rust-lang/crates.io-index"
344
+ checksum = "7e3450815272ef58cec6d564423f6e755e25379b217b0bc688e295ba24df6b1d"
345
+
346
+ [[package]]
347
+ name = "futures-sink"
348
+ version = "0.3.32"
349
+ source = "registry+https://github.com/rust-lang/crates.io-index"
350
+ checksum = "c39754e157331b013978ec91992bde1ac089843443c49cbc7f46150b0fad0893"
351
+
352
+ [[package]]
353
+ name = "futures-task"
354
+ version = "0.3.32"
355
+ source = "registry+https://github.com/rust-lang/crates.io-index"
356
+ checksum = "037711b3d59c33004d3856fbdc83b99d4ff37a24768fa1be9ce3538a1cde4393"
357
+
358
+ [[package]]
359
+ name = "futures-util"
360
+ version = "0.3.32"
361
+ source = "registry+https://github.com/rust-lang/crates.io-index"
362
+ checksum = "389ca41296e6190b48053de0321d02a77f32f8a5d2461dd38762c0593805c6d6"
363
+ dependencies = [
364
+ "futures-core",
365
+ "futures-task",
366
+ "pin-project-lite",
367
+ "slab",
368
+ ]
369
+
370
+ [[package]]
371
+ name = "getrandom"
372
+ version = "0.2.17"
373
+ source = "registry+https://github.com/rust-lang/crates.io-index"
374
+ checksum = "ff2abc00be7fca6ebc474524697ae276ad847ad0a6b3faa4bcb027e9a4614ad0"
375
+ dependencies = [
376
+ "cfg-if",
377
+ "js-sys",
378
+ "libc",
379
+ "wasi",
380
+ "wasm-bindgen",
381
+ ]
382
+
383
+ [[package]]
384
+ name = "getrandom"
385
+ version = "0.3.4"
386
+ source = "registry+https://github.com/rust-lang/crates.io-index"
387
+ checksum = "899def5c37c4fd7b2664648c28120ecec138e4d395b459e5ca34f9cce2dd77fd"
388
+ dependencies = [
389
+ "cfg-if",
390
+ "js-sys",
391
+ "libc",
392
+ "r-efi 5.3.0",
393
+ "wasip2",
394
+ "wasm-bindgen",
395
+ ]
396
+
397
+ [[package]]
398
+ name = "getrandom"
399
+ version = "0.4.2"
400
+ source = "registry+https://github.com/rust-lang/crates.io-index"
401
+ checksum = "0de51e6874e94e7bf76d726fc5d13ba782deca734ff60d5bb2fb2607c7406555"
402
+ dependencies = [
403
+ "cfg-if",
404
+ "libc",
405
+ "r-efi 6.0.0",
406
+ "rand_core 0.10.1",
407
+ "wasip2",
408
+ "wasip3",
409
+ ]
410
+
411
+ [[package]]
412
+ name = "h2"
413
+ version = "0.4.13"
414
+ source = "registry+https://github.com/rust-lang/crates.io-index"
415
+ checksum = "2f44da3a8150a6703ed5d34e164b875fd14c2cdab9af1252a9a1020bde2bdc54"
416
+ dependencies = [
417
+ "atomic-waker",
418
+ "bytes",
419
+ "fnv",
420
+ "futures-core",
421
+ "futures-sink",
422
+ "http",
423
+ "indexmap",
424
+ "slab",
425
+ "tokio",
426
+ "tokio-util",
427
+ "tracing",
428
+ ]
429
+
430
+ [[package]]
431
+ name = "hashbrown"
432
+ version = "0.15.5"
433
+ source = "registry+https://github.com/rust-lang/crates.io-index"
434
+ checksum = "9229cfe53dfd69f0609a49f65461bd93001ea1ef889cd5529dd176593f5338a1"
435
+ dependencies = [
436
+ "foldhash",
437
+ ]
438
+
439
+ [[package]]
440
+ name = "hashbrown"
441
+ version = "0.17.0"
442
+ source = "registry+https://github.com/rust-lang/crates.io-index"
443
+ checksum = "4f467dd6dccf739c208452f8014c75c18bb8301b050ad1cfb27153803edb0f51"
444
+
445
+ [[package]]
446
+ name = "heck"
447
+ version = "0.5.0"
448
+ source = "registry+https://github.com/rust-lang/crates.io-index"
449
+ checksum = "2304e00983f87ffb38b55b444b5e3b60a884b5d30c0fca7d82fe33449bbe55ea"
450
+
451
+ [[package]]
452
+ name = "http"
453
+ version = "1.4.0"
454
+ source = "registry+https://github.com/rust-lang/crates.io-index"
455
+ checksum = "e3ba2a386d7f85a81f119ad7498ebe444d2e22c2af0b86b069416ace48b3311a"
456
+ dependencies = [
457
+ "bytes",
458
+ "itoa",
459
+ ]
460
+
461
+ [[package]]
462
+ name = "http-body"
463
+ version = "1.0.1"
464
+ source = "registry+https://github.com/rust-lang/crates.io-index"
465
+ checksum = "1efedce1fb8e6913f23e0c92de8e62cd5b772a67e7b3946df930a62566c93184"
466
+ dependencies = [
467
+ "bytes",
468
+ "http",
469
+ ]
470
+
471
+ [[package]]
472
+ name = "http-body-util"
473
+ version = "0.1.3"
474
+ source = "registry+https://github.com/rust-lang/crates.io-index"
475
+ checksum = "b021d93e26becf5dc7e1b75b1bed1fd93124b374ceb73f43d4d4eafec896a64a"
476
+ dependencies = [
477
+ "bytes",
478
+ "futures-core",
479
+ "http",
480
+ "http-body",
481
+ "pin-project-lite",
482
+ ]
483
+
484
+ [[package]]
485
+ name = "http-range-header"
486
+ version = "0.4.2"
487
+ source = "registry+https://github.com/rust-lang/crates.io-index"
488
+ checksum = "9171a2ea8a68358193d15dd5d70c1c10a2afc3e7e4c5bc92bc9f025cebd7359c"
489
+
490
+ [[package]]
491
+ name = "httparse"
492
+ version = "1.10.1"
493
+ source = "registry+https://github.com/rust-lang/crates.io-index"
494
+ checksum = "6dbf3de79e51f3d586ab4cb9d5c3e2c14aa28ed23d180cf89b4df0454a69cc87"
495
+
496
+ [[package]]
497
+ name = "httpdate"
498
+ version = "1.0.3"
499
+ source = "registry+https://github.com/rust-lang/crates.io-index"
500
+ checksum = "df3b46402a9d5adb4c86a0cf463f42e19994e3ee891101b1841f30a545cb49a9"
501
+
502
+ [[package]]
503
+ name = "hyper"
504
+ version = "1.9.0"
505
+ source = "registry+https://github.com/rust-lang/crates.io-index"
506
+ checksum = "6299f016b246a94207e63da54dbe807655bf9e00044f73ded42c3ac5305fbcca"
507
+ dependencies = [
508
+ "atomic-waker",
509
+ "bytes",
510
+ "futures-channel",
511
+ "futures-core",
512
+ "h2",
513
+ "http",
514
+ "http-body",
515
+ "httparse",
516
+ "httpdate",
517
+ "itoa",
518
+ "pin-project-lite",
519
+ "smallvec",
520
+ "tokio",
521
+ "want",
522
+ ]
523
+
524
+ [[package]]
525
+ name = "hyper-rustls"
526
+ version = "0.27.9"
527
+ source = "registry+https://github.com/rust-lang/crates.io-index"
528
+ checksum = "33ca68d021ef39cf6463ab54c1d0f5daf03377b70561305bb89a8f83aab66e0f"
529
+ dependencies = [
530
+ "http",
531
+ "hyper",
532
+ "hyper-util",
533
+ "rustls",
534
+ "tokio",
535
+ "tokio-rustls",
536
+ "tower-service",
537
+ ]
538
+
539
+ [[package]]
540
+ name = "hyper-util"
541
+ version = "0.1.20"
542
+ source = "registry+https://github.com/rust-lang/crates.io-index"
543
+ checksum = "96547c2556ec9d12fb1578c4eaf448b04993e7fb79cbaad930a656880a6bdfa0"
544
+ dependencies = [
545
+ "base64",
546
+ "bytes",
547
+ "futures-channel",
548
+ "futures-util",
549
+ "http",
550
+ "http-body",
551
+ "hyper",
552
+ "ipnet",
553
+ "libc",
554
+ "percent-encoding",
555
+ "pin-project-lite",
556
+ "socket2",
557
+ "system-configuration",
558
+ "tokio",
559
+ "tower-service",
560
+ "tracing",
561
+ "windows-registry",
562
+ ]
563
+
564
+ [[package]]
565
+ name = "icu_collections"
566
+ version = "2.2.0"
567
+ source = "registry+https://github.com/rust-lang/crates.io-index"
568
+ checksum = "2984d1cd16c883d7935b9e07e44071dca8d917fd52ecc02c04d5fa0b5a3f191c"
569
+ dependencies = [
570
+ "displaydoc",
571
+ "potential_utf",
572
+ "utf8_iter",
573
+ "yoke",
574
+ "zerofrom",
575
+ "zerovec",
576
+ ]
577
+
578
+ [[package]]
579
+ name = "icu_locale_core"
580
+ version = "2.2.0"
581
+ source = "registry+https://github.com/rust-lang/crates.io-index"
582
+ checksum = "92219b62b3e2b4d88ac5119f8904c10f8f61bf7e95b640d25ba3075e6cac2c29"
583
+ dependencies = [
584
+ "displaydoc",
585
+ "litemap",
586
+ "tinystr",
587
+ "writeable",
588
+ "zerovec",
589
+ ]
590
+
591
+ [[package]]
592
+ name = "icu_normalizer"
593
+ version = "2.2.0"
594
+ source = "registry+https://github.com/rust-lang/crates.io-index"
595
+ checksum = "c56e5ee99d6e3d33bd91c5d85458b6005a22140021cc324cea84dd0e72cff3b4"
596
+ dependencies = [
597
+ "icu_collections",
598
+ "icu_normalizer_data",
599
+ "icu_properties",
600
+ "icu_provider",
601
+ "smallvec",
602
+ "zerovec",
603
+ ]
604
+
605
+ [[package]]
606
+ name = "icu_normalizer_data"
607
+ version = "2.2.0"
608
+ source = "registry+https://github.com/rust-lang/crates.io-index"
609
+ checksum = "da3be0ae77ea334f4da67c12f149704f19f81d1adf7c51cf482943e84a2bad38"
610
+
611
+ [[package]]
612
+ name = "icu_properties"
613
+ version = "2.2.0"
614
+ source = "registry+https://github.com/rust-lang/crates.io-index"
615
+ checksum = "bee3b67d0ea5c2cca5003417989af8996f8604e34fb9ddf96208a033901e70de"
616
+ dependencies = [
617
+ "icu_collections",
618
+ "icu_locale_core",
619
+ "icu_properties_data",
620
+ "icu_provider",
621
+ "zerotrie",
622
+ "zerovec",
623
+ ]
624
+
625
+ [[package]]
626
+ name = "icu_properties_data"
627
+ version = "2.2.0"
628
+ source = "registry+https://github.com/rust-lang/crates.io-index"
629
+ checksum = "8e2bbb201e0c04f7b4b3e14382af113e17ba4f63e2c9d2ee626b720cbce54a14"
630
+
631
+ [[package]]
632
+ name = "icu_provider"
633
+ version = "2.2.0"
634
+ source = "registry+https://github.com/rust-lang/crates.io-index"
635
+ checksum = "139c4cf31c8b5f33d7e199446eff9c1e02decfc2f0eec2c8d71f65befa45b421"
636
+ dependencies = [
637
+ "displaydoc",
638
+ "icu_locale_core",
639
+ "writeable",
640
+ "yoke",
641
+ "zerofrom",
642
+ "zerotrie",
643
+ "zerovec",
644
+ ]
645
+
646
+ [[package]]
647
+ name = "id-arena"
648
+ version = "2.3.0"
649
+ source = "registry+https://github.com/rust-lang/crates.io-index"
650
+ checksum = "3d3067d79b975e8844ca9eb072e16b31c3c1c36928edf9c6789548c524d0d954"
651
+
652
+ [[package]]
653
+ name = "idna"
654
+ version = "1.1.0"
655
+ source = "registry+https://github.com/rust-lang/crates.io-index"
656
+ checksum = "3b0875f23caa03898994f6ddc501886a45c7d3d62d04d2d90788d47be1b1e4de"
657
+ dependencies = [
658
+ "idna_adapter",
659
+ "smallvec",
660
+ "utf8_iter",
661
+ ]
662
+
663
+ [[package]]
664
+ name = "idna_adapter"
665
+ version = "1.2.1"
666
+ source = "registry+https://github.com/rust-lang/crates.io-index"
667
+ checksum = "3acae9609540aa318d1bc588455225fb2085b9ed0c4f6bd0d9d5bcd86f1a0344"
668
+ dependencies = [
669
+ "icu_normalizer",
670
+ "icu_properties",
671
+ ]
672
+
673
+ [[package]]
674
+ name = "include_dir"
675
+ version = "0.7.4"
676
+ source = "registry+https://github.com/rust-lang/crates.io-index"
677
+ checksum = "923d117408f1e49d914f1a379a309cffe4f18c05cf4e3d12e613a15fc81bd0dd"
678
+ dependencies = [
679
+ "include_dir_macros",
680
+ ]
681
+
682
+ [[package]]
683
+ name = "include_dir_macros"
684
+ version = "0.7.4"
685
+ source = "registry+https://github.com/rust-lang/crates.io-index"
686
+ checksum = "7cab85a7ed0bd5f0e76d93846e0147172bed2e2d3f859bcc33a8d9699cad1a75"
687
+ dependencies = [
688
+ "proc-macro2",
689
+ "quote",
690
+ ]
691
+
692
+ [[package]]
693
+ name = "indexmap"
694
+ version = "2.14.0"
695
+ source = "registry+https://github.com/rust-lang/crates.io-index"
696
+ checksum = "d466e9454f08e4a911e14806c24e16fba1b4c121d1ea474396f396069cf949d9"
697
+ dependencies = [
698
+ "equivalent",
699
+ "hashbrown 0.17.0",
700
+ "serde",
701
+ "serde_core",
702
+ ]
703
+
704
+ [[package]]
705
+ name = "ipnet"
706
+ version = "2.12.0"
707
+ source = "registry+https://github.com/rust-lang/crates.io-index"
708
+ checksum = "d98f6fed1fde3f8c21bc40a1abb88dd75e67924f9cffc3ef95607bad8017f8e2"
709
+
710
+ [[package]]
711
+ name = "iri-string"
712
+ version = "0.7.12"
713
+ source = "registry+https://github.com/rust-lang/crates.io-index"
714
+ checksum = "25e659a4bb38e810ebc252e53b5814ff908a8c58c2a9ce2fae1bbec24cbf4e20"
715
+ dependencies = [
716
+ "memchr",
717
+ "serde",
718
+ ]
719
+
720
+ [[package]]
721
+ name = "itoa"
722
+ version = "1.0.18"
723
+ source = "registry+https://github.com/rust-lang/crates.io-index"
724
+ checksum = "8f42a60cbdf9a97f5d2305f08a87dc4e09308d1276d28c869c684d7777685682"
725
+
726
+ [[package]]
727
+ name = "jni"
728
+ version = "0.21.1"
729
+ source = "registry+https://github.com/rust-lang/crates.io-index"
730
+ checksum = "1a87aa2bb7d2af34197c04845522473242e1aa17c12f4935d5856491a7fb8c97"
731
+ dependencies = [
732
+ "cesu8",
733
+ "cfg-if",
734
+ "combine",
735
+ "jni-sys 0.3.1",
736
+ "log",
737
+ "thiserror 1.0.69",
738
+ "walkdir",
739
+ "windows-sys 0.45.0",
740
+ ]
741
+
742
+ [[package]]
743
+ name = "jni-sys"
744
+ version = "0.3.1"
745
+ source = "registry+https://github.com/rust-lang/crates.io-index"
746
+ checksum = "41a652e1f9b6e0275df1f15b32661cf0d4b78d4d87ddec5e0c3c20f097433258"
747
+ dependencies = [
748
+ "jni-sys 0.4.1",
749
+ ]
750
+
751
+ [[package]]
752
+ name = "jni-sys"
753
+ version = "0.4.1"
754
+ source = "registry+https://github.com/rust-lang/crates.io-index"
755
+ checksum = "c6377a88cb3910bee9b0fa88d4f42e1d2da8e79915598f65fb0c7ee14c878af2"
756
+ dependencies = [
757
+ "jni-sys-macros",
758
+ ]
759
+
760
+ [[package]]
761
+ name = "jni-sys-macros"
762
+ version = "0.4.1"
763
+ source = "registry+https://github.com/rust-lang/crates.io-index"
764
+ checksum = "38c0b942f458fe50cdac086d2f946512305e5631e720728f2a61aabcd47a6264"
765
+ dependencies = [
766
+ "quote",
767
+ "syn",
768
+ ]
769
+
770
+ [[package]]
771
+ name = "jobserver"
772
+ version = "0.1.34"
773
+ source = "registry+https://github.com/rust-lang/crates.io-index"
774
+ checksum = "9afb3de4395d6b3e67a780b6de64b51c978ecf11cb9a462c66be7d4ca9039d33"
775
+ dependencies = [
776
+ "getrandom 0.3.4",
777
+ "libc",
778
+ ]
779
+
780
+ [[package]]
781
+ name = "js-sys"
782
+ version = "0.3.95"
783
+ source = "registry+https://github.com/rust-lang/crates.io-index"
784
+ checksum = "2964e92d1d9dc3364cae4d718d93f227e3abb088e747d92e0395bfdedf1c12ca"
785
+ dependencies = [
786
+ "cfg-if",
787
+ "futures-util",
788
+ "once_cell",
789
+ "wasm-bindgen",
790
+ ]
791
+
792
+ [[package]]
793
+ name = "lazy_static"
794
+ version = "1.5.0"
795
+ source = "registry+https://github.com/rust-lang/crates.io-index"
796
+ checksum = "bbd2bcb4c963f2ddae06a2efc7e9f3591312473c50c6685e1f298068316e66fe"
797
+
798
+ [[package]]
799
+ name = "leb128fmt"
800
+ version = "0.1.0"
801
+ source = "registry+https://github.com/rust-lang/crates.io-index"
802
+ checksum = "09edd9e8b54e49e587e4f6295a7d29c3ea94d469cb40ab8ca70b288248a81db2"
803
+
804
+ [[package]]
805
+ name = "libc"
806
+ version = "0.2.186"
807
+ source = "registry+https://github.com/rust-lang/crates.io-index"
808
+ checksum = "68ab91017fe16c622486840e4c83c9a37afeff978bd239b5293d61ece587de66"
809
+
810
+ [[package]]
811
+ name = "litemap"
812
+ version = "0.8.2"
813
+ source = "registry+https://github.com/rust-lang/crates.io-index"
814
+ checksum = "92daf443525c4cce67b150400bc2316076100ce0b3686209eb8cf3c31612e6f0"
815
+
816
+ [[package]]
817
+ name = "lock_api"
818
+ version = "0.4.14"
819
+ source = "registry+https://github.com/rust-lang/crates.io-index"
820
+ checksum = "224399e74b87b5f3557511d98dff8b14089b3dadafcab6bb93eab67d3aace965"
821
+ dependencies = [
822
+ "scopeguard",
823
+ ]
824
+
825
+ [[package]]
826
+ name = "log"
827
+ version = "0.4.29"
828
+ source = "registry+https://github.com/rust-lang/crates.io-index"
829
+ checksum = "5e5032e24019045c762d3c0f28f5b6b8bbf38563a65908389bf7978758920897"
830
+
831
+ [[package]]
832
+ name = "lru-slab"
833
+ version = "0.1.2"
834
+ source = "registry+https://github.com/rust-lang/crates.io-index"
835
+ checksum = "112b39cec0b298b6c1999fee3e31427f74f676e4cb9879ed1a121b43661a4154"
836
+
837
+ [[package]]
838
+ name = "matchers"
839
+ version = "0.2.0"
840
+ source = "registry+https://github.com/rust-lang/crates.io-index"
841
+ checksum = "d1525a2a28c7f4fa0fc98bb91ae755d1e2d1505079e05539e35bc876b5d65ae9"
842
+ dependencies = [
843
+ "regex-automata",
844
+ ]
845
+
846
+ [[package]]
847
+ name = "matchit"
848
+ version = "0.8.4"
849
+ source = "registry+https://github.com/rust-lang/crates.io-index"
850
+ checksum = "47e1ffaa40ddd1f3ed91f717a33c8c0ee23fff369e3aa8772b9605cc1d22f4c3"
851
+
852
+ [[package]]
853
+ name = "memchr"
854
+ version = "2.8.0"
855
+ source = "registry+https://github.com/rust-lang/crates.io-index"
856
+ checksum = "f8ca58f447f06ed17d5fc4043ce1b10dd205e060fb3ce5b979b8ed8e59ff3f79"
857
+
858
+ [[package]]
859
+ name = "mime"
860
+ version = "0.3.17"
861
+ source = "registry+https://github.com/rust-lang/crates.io-index"
862
+ checksum = "6877bb514081ee2a7ff5ef9de3281f14a4dd4bceac4c09388074a6b5df8a139a"
863
+
864
+ [[package]]
865
+ name = "mime_guess"
866
+ version = "2.0.5"
867
+ source = "registry+https://github.com/rust-lang/crates.io-index"
868
+ checksum = "f7c44f8e672c00fe5308fa235f821cb4198414e1c77935c1ab6948d3fd78550e"
869
+ dependencies = [
870
+ "mime",
871
+ "unicase",
872
+ ]
873
+
874
+ [[package]]
875
+ name = "mio"
876
+ version = "1.2.0"
877
+ source = "registry+https://github.com/rust-lang/crates.io-index"
878
+ checksum = "50b7e5b27aa02a74bac8c3f23f448f8d87ff11f92d3aac1a6ed369ee08cc56c1"
879
+ dependencies = [
880
+ "libc",
881
+ "wasi",
882
+ "windows-sys 0.61.2",
883
+ ]
884
+
885
+ [[package]]
886
+ name = "nu-ansi-term"
887
+ version = "0.50.3"
888
+ source = "registry+https://github.com/rust-lang/crates.io-index"
889
+ checksum = "7957b9740744892f114936ab4a57b3f487491bbeafaf8083688b16841a4240e5"
890
+ dependencies = [
891
+ "windows-sys 0.61.2",
892
+ ]
893
+
894
+ [[package]]
895
+ name = "num-format"
896
+ version = "0.4.4"
897
+ source = "registry+https://github.com/rust-lang/crates.io-index"
898
+ checksum = "a652d9771a63711fd3c3deb670acfbe5c30a4072e664d7a3bf5a9e1056ac72c3"
899
+ dependencies = [
900
+ "arrayvec",
901
+ "itoa",
902
+ ]
903
+
904
+ [[package]]
905
+ name = "once_cell"
906
+ version = "1.21.4"
907
+ source = "registry+https://github.com/rust-lang/crates.io-index"
908
+ checksum = "9f7c3e4beb33f85d45ae3e3a1792185706c8e16d043238c593331cc7cd313b50"
909
+
910
+ [[package]]
911
+ name = "openssl-probe"
912
+ version = "0.2.1"
913
+ source = "registry+https://github.com/rust-lang/crates.io-index"
914
+ checksum = "7c87def4c32ab89d880effc9e097653c8da5d6ef28e6b539d313baaacfbafcbe"
915
+
916
+ [[package]]
917
+ name = "owo-colors"
918
+ version = "4.3.0"
919
+ source = "registry+https://github.com/rust-lang/crates.io-index"
920
+ checksum = "d211803b9b6b570f68772237e415a029d5a50c65d382910b879fb19d3271f94d"
921
+
922
+ [[package]]
923
+ name = "parking_lot"
924
+ version = "0.12.5"
925
+ source = "registry+https://github.com/rust-lang/crates.io-index"
926
+ checksum = "93857453250e3077bd71ff98b6a65ea6621a19bb0f559a85248955ac12c45a1a"
927
+ dependencies = [
928
+ "lock_api",
929
+ "parking_lot_core",
930
+ ]
931
+
932
+ [[package]]
933
+ name = "parking_lot_core"
934
+ version = "0.9.12"
935
+ source = "registry+https://github.com/rust-lang/crates.io-index"
936
+ checksum = "2621685985a2ebf1c516881c026032ac7deafcda1a2c9b7850dc81e3dfcb64c1"
937
+ dependencies = [
938
+ "cfg-if",
939
+ "libc",
940
+ "redox_syscall",
941
+ "smallvec",
942
+ "windows-link",
943
+ ]
944
+
945
+ [[package]]
946
+ name = "percent-encoding"
947
+ version = "2.3.2"
948
+ source = "registry+https://github.com/rust-lang/crates.io-index"
949
+ checksum = "9b4f627cb1b25917193a259e49bdad08f671f8d9708acfd5fe0a8c1455d87220"
950
+
951
+ [[package]]
952
+ name = "pin-project-lite"
953
+ version = "0.2.17"
954
+ source = "registry+https://github.com/rust-lang/crates.io-index"
955
+ checksum = "a89322df9ebe1c1578d689c92318e070967d1042b512afbe49518723f4e6d5cd"
956
+
957
+ [[package]]
958
+ name = "potential_utf"
959
+ version = "0.1.5"
960
+ source = "registry+https://github.com/rust-lang/crates.io-index"
961
+ checksum = "0103b1cef7ec0cf76490e969665504990193874ea05c85ff9bab8b911d0a0564"
962
+ dependencies = [
963
+ "zerovec",
964
+ ]
965
+
966
+ [[package]]
967
+ name = "ppv-lite86"
968
+ version = "0.2.21"
969
+ source = "registry+https://github.com/rust-lang/crates.io-index"
970
+ checksum = "85eae3c4ed2f50dcfe72643da4befc30deadb458a9b590d720cde2f2b1e97da9"
971
+ dependencies = [
972
+ "zerocopy",
973
+ ]
974
+
975
+ [[package]]
976
+ name = "prettyplease"
977
+ version = "0.2.37"
978
+ source = "registry+https://github.com/rust-lang/crates.io-index"
979
+ checksum = "479ca8adacdd7ce8f1fb39ce9ecccbfe93a3f1344b3d0d97f20bc0196208f62b"
980
+ dependencies = [
981
+ "proc-macro2",
982
+ "syn",
983
+ ]
984
+
985
+ [[package]]
986
+ name = "proc-macro2"
987
+ version = "1.0.106"
988
+ source = "registry+https://github.com/rust-lang/crates.io-index"
989
+ checksum = "8fd00f0bb2e90d81d1044c2b32617f68fcb9fa3bb7640c23e9c748e53fb30934"
990
+ dependencies = [
991
+ "unicode-ident",
992
+ ]
993
+
994
+ [[package]]
995
+ name = "quinn"
996
+ version = "0.11.9"
997
+ source = "registry+https://github.com/rust-lang/crates.io-index"
998
+ checksum = "b9e20a958963c291dc322d98411f541009df2ced7b5a4f2bd52337638cfccf20"
999
+ dependencies = [
1000
+ "bytes",
1001
+ "cfg_aliases",
1002
+ "pin-project-lite",
1003
+ "quinn-proto",
1004
+ "quinn-udp",
1005
+ "rustc-hash",
1006
+ "rustls",
1007
+ "socket2",
1008
+ "thiserror 2.0.18",
1009
+ "tokio",
1010
+ "tracing",
1011
+ "web-time",
1012
+ ]
1013
+
1014
+ [[package]]
1015
+ name = "quinn-proto"
1016
+ version = "0.11.14"
1017
+ source = "registry+https://github.com/rust-lang/crates.io-index"
1018
+ checksum = "434b42fec591c96ef50e21e886936e66d3cc3f737104fdb9b737c40ffb94c098"
1019
+ dependencies = [
1020
+ "aws-lc-rs",
1021
+ "bytes",
1022
+ "getrandom 0.3.4",
1023
+ "lru-slab",
1024
+ "rand 0.9.4",
1025
+ "ring",
1026
+ "rustc-hash",
1027
+ "rustls",
1028
+ "rustls-pki-types",
1029
+ "slab",
1030
+ "thiserror 2.0.18",
1031
+ "tinyvec",
1032
+ "tracing",
1033
+ "web-time",
1034
+ ]
1035
+
1036
+ [[package]]
1037
+ name = "quinn-udp"
1038
+ version = "0.5.14"
1039
+ source = "registry+https://github.com/rust-lang/crates.io-index"
1040
+ checksum = "addec6a0dcad8a8d96a771f815f0eaf55f9d1805756410b39f5fa81332574cbd"
1041
+ dependencies = [
1042
+ "cfg_aliases",
1043
+ "libc",
1044
+ "once_cell",
1045
+ "socket2",
1046
+ "tracing",
1047
+ "windows-sys 0.60.2",
1048
+ ]
1049
+
1050
+ [[package]]
1051
+ name = "quote"
1052
+ version = "1.0.45"
1053
+ source = "registry+https://github.com/rust-lang/crates.io-index"
1054
+ checksum = "41f2619966050689382d2b44f664f4bc593e129785a36d6ee376ddf37259b924"
1055
+ dependencies = [
1056
+ "proc-macro2",
1057
+ ]
1058
+
1059
+ [[package]]
1060
+ name = "r-efi"
1061
+ version = "5.3.0"
1062
+ source = "registry+https://github.com/rust-lang/crates.io-index"
1063
+ checksum = "69cdb34c158ceb288df11e18b4bd39de994f6657d83847bdffdbd7f346754b0f"
1064
+
1065
+ [[package]]
1066
+ name = "r-efi"
1067
+ version = "6.0.0"
1068
+ source = "registry+https://github.com/rust-lang/crates.io-index"
1069
+ checksum = "f8dcc9c7d52a811697d2151c701e0d08956f92b0e24136cf4cf27b57a6a0d9bf"
1070
+
1071
+ [[package]]
1072
+ name = "rand"
1073
+ version = "0.9.4"
1074
+ source = "registry+https://github.com/rust-lang/crates.io-index"
1075
+ checksum = "44c5af06bb1b7d3216d91932aed5265164bf384dc89cd6ba05cf59a35f5f76ea"
1076
+ dependencies = [
1077
+ "rand_chacha 0.9.0",
1078
+ "rand_core 0.9.5",
1079
+ ]
1080
+
1081
+ [[package]]
1082
+ name = "rand"
1083
+ version = "0.10.1"
1084
+ source = "registry+https://github.com/rust-lang/crates.io-index"
1085
+ checksum = "d2e8e8bcc7961af1fdac401278c6a831614941f6164ee3bf4ce61b7edb162207"
1086
+ dependencies = [
1087
+ "chacha20",
1088
+ "getrandom 0.4.2",
1089
+ "rand_core 0.10.1",
1090
+ ]
1091
+
1092
+ [[package]]
1093
+ name = "rand_chacha"
1094
+ version = "0.9.0"
1095
+ source = "registry+https://github.com/rust-lang/crates.io-index"
1096
+ checksum = "d3022b5f1df60f26e1ffddd6c66e8aa15de382ae63b3a0c1bfc0e4d3e3f325cb"
1097
+ dependencies = [
1098
+ "ppv-lite86",
1099
+ "rand_core 0.9.5",
1100
+ ]
1101
+
1102
+ [[package]]
1103
+ name = "rand_chacha"
1104
+ version = "0.10.0"
1105
+ source = "registry+https://github.com/rust-lang/crates.io-index"
1106
+ checksum = "3e6af7f3e25ded52c41df4e0b1af2d047e45896c2f3281792ed68a1c243daedb"
1107
+ dependencies = [
1108
+ "ppv-lite86",
1109
+ "rand_core 0.10.1",
1110
+ ]
1111
+
1112
+ [[package]]
1113
+ name = "rand_core"
1114
+ version = "0.9.5"
1115
+ source = "registry+https://github.com/rust-lang/crates.io-index"
1116
+ checksum = "76afc826de14238e6e8c374ddcc1fa19e374fd8dd986b0d2af0d02377261d83c"
1117
+ dependencies = [
1118
+ "getrandom 0.3.4",
1119
+ ]
1120
+
1121
+ [[package]]
1122
+ name = "rand_core"
1123
+ version = "0.10.1"
1124
+ source = "registry+https://github.com/rust-lang/crates.io-index"
1125
+ checksum = "63b8176103e19a2643978565ca18b50549f6101881c443590420e4dc998a3c69"
1126
+
1127
+ [[package]]
1128
+ name = "rayon"
1129
+ version = "1.12.0"
1130
+ source = "registry+https://github.com/rust-lang/crates.io-index"
1131
+ checksum = "fb39b166781f92d482534ef4b4b1b2568f42613b53e5b6c160e24cfbfa30926d"
1132
+ dependencies = [
1133
+ "either",
1134
+ "rayon-core",
1135
+ ]
1136
+
1137
+ [[package]]
1138
+ name = "rayon-core"
1139
+ version = "1.13.0"
1140
+ source = "registry+https://github.com/rust-lang/crates.io-index"
1141
+ checksum = "22e18b0f0062d30d4230b2e85ff77fdfe4326feb054b9783a3460d8435c8ab91"
1142
+ dependencies = [
1143
+ "crossbeam-deque",
1144
+ "crossbeam-utils",
1145
+ ]
1146
+
1147
+ [[package]]
1148
+ name = "redox_syscall"
1149
+ version = "0.5.18"
1150
+ source = "registry+https://github.com/rust-lang/crates.io-index"
1151
+ checksum = "ed2bf2547551a7053d6fdfafda3f938979645c44812fbfcda098faae3f1a362d"
1152
+ dependencies = [
1153
+ "bitflags",
1154
+ ]
1155
+
1156
+ [[package]]
1157
+ name = "regex-automata"
1158
+ version = "0.4.14"
1159
+ source = "registry+https://github.com/rust-lang/crates.io-index"
1160
+ checksum = "6e1dd4122fc1595e8162618945476892eefca7b88c52820e74af6262213cae8f"
1161
+ dependencies = [
1162
+ "aho-corasick",
1163
+ "memchr",
1164
+ "regex-syntax",
1165
+ ]
1166
+
1167
+ [[package]]
1168
+ name = "regex-syntax"
1169
+ version = "0.8.10"
1170
+ source = "registry+https://github.com/rust-lang/crates.io-index"
1171
+ checksum = "dc897dd8d9e8bd1ed8cdad82b5966c3e0ecae09fb1907d58efaa013543185d0a"
1172
+
1173
+ [[package]]
1174
+ name = "reqwest"
1175
+ version = "0.13.2"
1176
+ source = "registry+https://github.com/rust-lang/crates.io-index"
1177
+ checksum = "ab3f43e3283ab1488b624b44b0e988d0acea0b3214e694730a055cb6b2efa801"
1178
+ dependencies = [
1179
+ "base64",
1180
+ "bytes",
1181
+ "encoding_rs",
1182
+ "futures-core",
1183
+ "h2",
1184
+ "http",
1185
+ "http-body",
1186
+ "http-body-util",
1187
+ "hyper",
1188
+ "hyper-rustls",
1189
+ "hyper-util",
1190
+ "js-sys",
1191
+ "log",
1192
+ "mime",
1193
+ "percent-encoding",
1194
+ "pin-project-lite",
1195
+ "quinn",
1196
+ "rustls",
1197
+ "rustls-pki-types",
1198
+ "rustls-platform-verifier",
1199
+ "serde",
1200
+ "serde_json",
1201
+ "sync_wrapper",
1202
+ "tokio",
1203
+ "tokio-rustls",
1204
+ "tower",
1205
+ "tower-http",
1206
+ "tower-service",
1207
+ "url",
1208
+ "wasm-bindgen",
1209
+ "wasm-bindgen-futures",
1210
+ "web-sys",
1211
+ ]
1212
+
1213
+ [[package]]
1214
+ name = "ring"
1215
+ version = "0.17.14"
1216
+ source = "registry+https://github.com/rust-lang/crates.io-index"
1217
+ checksum = "a4689e6c2294d81e88dc6261c768b63bc4fcdb852be6d1352498b114f61383b7"
1218
+ dependencies = [
1219
+ "cc",
1220
+ "cfg-if",
1221
+ "getrandom 0.2.17",
1222
+ "libc",
1223
+ "untrusted",
1224
+ "windows-sys 0.52.0",
1225
+ ]
1226
+
1227
+ [[package]]
1228
+ name = "rustc-hash"
1229
+ version = "2.1.2"
1230
+ source = "registry+https://github.com/rust-lang/crates.io-index"
1231
+ checksum = "94300abf3f1ae2e2b8ffb7b58043de3d399c73fa6f4b73826402a5c457614dbe"
1232
+
1233
+ [[package]]
1234
+ name = "rustls"
1235
+ version = "0.23.39"
1236
+ source = "registry+https://github.com/rust-lang/crates.io-index"
1237
+ checksum = "7c2c118cb077cca2822033836dfb1b975355dfb784b5e8da48f7b6c5db74e60e"
1238
+ dependencies = [
1239
+ "aws-lc-rs",
1240
+ "once_cell",
1241
+ "rustls-pki-types",
1242
+ "rustls-webpki",
1243
+ "subtle",
1244
+ "zeroize",
1245
+ ]
1246
+
1247
+ [[package]]
1248
+ name = "rustls-native-certs"
1249
+ version = "0.8.3"
1250
+ source = "registry+https://github.com/rust-lang/crates.io-index"
1251
+ checksum = "612460d5f7bea540c490b2b6395d8e34a953e52b491accd6c86c8164c5932a63"
1252
+ dependencies = [
1253
+ "openssl-probe",
1254
+ "rustls-pki-types",
1255
+ "schannel",
1256
+ "security-framework",
1257
+ ]
1258
+
1259
+ [[package]]
1260
+ name = "rustls-pki-types"
1261
+ version = "1.14.1"
1262
+ source = "registry+https://github.com/rust-lang/crates.io-index"
1263
+ checksum = "30a7197ae7eb376e574fe940d068c30fe0462554a3ddbe4eca7838e049c937a9"
1264
+ dependencies = [
1265
+ "web-time",
1266
+ "zeroize",
1267
+ ]
1268
+
1269
+ [[package]]
1270
+ name = "rustls-platform-verifier"
1271
+ version = "0.6.2"
1272
+ source = "registry+https://github.com/rust-lang/crates.io-index"
1273
+ checksum = "1d99feebc72bae7ab76ba994bb5e121b8d83d910ca40b36e0921f53becc41784"
1274
+ dependencies = [
1275
+ "core-foundation 0.10.1",
1276
+ "core-foundation-sys",
1277
+ "jni",
1278
+ "log",
1279
+ "once_cell",
1280
+ "rustls",
1281
+ "rustls-native-certs",
1282
+ "rustls-platform-verifier-android",
1283
+ "rustls-webpki",
1284
+ "security-framework",
1285
+ "security-framework-sys",
1286
+ "webpki-root-certs",
1287
+ "windows-sys 0.61.2",
1288
+ ]
1289
+
1290
+ [[package]]
1291
+ name = "rustls-platform-verifier-android"
1292
+ version = "0.1.1"
1293
+ source = "registry+https://github.com/rust-lang/crates.io-index"
1294
+ checksum = "f87165f0995f63a9fbeea62b64d10b4d9d8e78ec6d7d51fb2125fda7bb36788f"
1295
+
1296
+ [[package]]
1297
+ name = "rustls-webpki"
1298
+ version = "0.103.13"
1299
+ source = "registry+https://github.com/rust-lang/crates.io-index"
1300
+ checksum = "61c429a8649f110dddef65e2a5ad240f747e85f7758a6bccc7e5777bd33f756e"
1301
+ dependencies = [
1302
+ "aws-lc-rs",
1303
+ "ring",
1304
+ "rustls-pki-types",
1305
+ "untrusted",
1306
+ ]
1307
+
1308
+ [[package]]
1309
+ name = "rustversion"
1310
+ version = "1.0.22"
1311
+ source = "registry+https://github.com/rust-lang/crates.io-index"
1312
+ checksum = "b39cdef0fa800fc44525c84ccb54a029961a8215f9619753635a9c0d2538d46d"
1313
+
1314
+ [[package]]
1315
+ name = "ryu"
1316
+ version = "1.0.23"
1317
+ source = "registry+https://github.com/rust-lang/crates.io-index"
1318
+ checksum = "9774ba4a74de5f7b1c1451ed6cd5285a32eddb5cccb8cc655a4e50009e06477f"
1319
+
1320
+ [[package]]
1321
+ name = "same-file"
1322
+ version = "1.0.6"
1323
+ source = "registry+https://github.com/rust-lang/crates.io-index"
1324
+ checksum = "93fc1dc3aaa9bfed95e02e6eadabb4baf7e3078b0bd1b4d7b6b0b68378900502"
1325
+ dependencies = [
1326
+ "winapi-util",
1327
+ ]
1328
+
1329
+ [[package]]
1330
+ name = "schannel"
1331
+ version = "0.1.29"
1332
+ source = "registry+https://github.com/rust-lang/crates.io-index"
1333
+ checksum = "91c1b7e4904c873ef0710c1f407dde2e6287de2bebc1bbbf7d430bb7cbffd939"
1334
+ dependencies = [
1335
+ "windows-sys 0.61.2",
1336
+ ]
1337
+
1338
+ [[package]]
1339
+ name = "scopeguard"
1340
+ version = "1.2.0"
1341
+ source = "registry+https://github.com/rust-lang/crates.io-index"
1342
+ checksum = "94143f37725109f92c262ed2cf5e59bce7498c01bcc1502d7b9afe439a4e9f49"
1343
+
1344
+ [[package]]
1345
+ name = "security-framework"
1346
+ version = "3.7.0"
1347
+ source = "registry+https://github.com/rust-lang/crates.io-index"
1348
+ checksum = "b7f4bc775c73d9a02cde8bf7b2ec4c9d12743edf609006c7facc23998404cd1d"
1349
+ dependencies = [
1350
+ "bitflags",
1351
+ "core-foundation 0.10.1",
1352
+ "core-foundation-sys",
1353
+ "libc",
1354
+ "security-framework-sys",
1355
+ ]
1356
+
1357
+ [[package]]
1358
+ name = "security-framework-sys"
1359
+ version = "2.17.0"
1360
+ source = "registry+https://github.com/rust-lang/crates.io-index"
1361
+ checksum = "6ce2691df843ecc5d231c0b14ece2acc3efb62c0a398c7e1d875f3983ce020e3"
1362
+ dependencies = [
1363
+ "core-foundation-sys",
1364
+ "libc",
1365
+ ]
1366
+
1367
+ [[package]]
1368
+ name = "semver"
1369
+ version = "1.0.28"
1370
+ source = "registry+https://github.com/rust-lang/crates.io-index"
1371
+ checksum = "8a7852d02fc848982e0c167ef163aaff9cd91dc640ba85e263cb1ce46fae51cd"
1372
+
1373
+ [[package]]
1374
+ name = "serde"
1375
+ version = "1.0.228"
1376
+ source = "registry+https://github.com/rust-lang/crates.io-index"
1377
+ checksum = "9a8e94ea7f378bd32cbbd37198a4a91436180c5bb472411e48b5ec2e2124ae9e"
1378
+ dependencies = [
1379
+ "serde_core",
1380
+ "serde_derive",
1381
+ ]
1382
+
1383
+ [[package]]
1384
+ name = "serde_core"
1385
+ version = "1.0.228"
1386
+ source = "registry+https://github.com/rust-lang/crates.io-index"
1387
+ checksum = "41d385c7d4ca58e59fc732af25c3983b67ac852c1a25000afe1175de458b67ad"
1388
+ dependencies = [
1389
+ "serde_derive",
1390
+ ]
1391
+
1392
+ [[package]]
1393
+ name = "serde_derive"
1394
+ version = "1.0.228"
1395
+ source = "registry+https://github.com/rust-lang/crates.io-index"
1396
+ checksum = "d540f220d3187173da220f885ab66608367b6574e925011a9353e4badda91d79"
1397
+ dependencies = [
1398
+ "proc-macro2",
1399
+ "quote",
1400
+ "syn",
1401
+ ]
1402
+
1403
+ [[package]]
1404
+ name = "serde_json"
1405
+ version = "1.0.149"
1406
+ source = "registry+https://github.com/rust-lang/crates.io-index"
1407
+ checksum = "83fc039473c5595ace860d8c4fafa220ff474b3fc6bfdb4293327f1a37e94d86"
1408
+ dependencies = [
1409
+ "itoa",
1410
+ "memchr",
1411
+ "serde",
1412
+ "serde_core",
1413
+ "zmij",
1414
+ ]
1415
+
1416
+ [[package]]
1417
+ name = "serde_path_to_error"
1418
+ version = "0.1.20"
1419
+ source = "registry+https://github.com/rust-lang/crates.io-index"
1420
+ checksum = "10a9ff822e371bb5403e391ecd83e182e0e77ba7f6fe0160b795797109d1b457"
1421
+ dependencies = [
1422
+ "itoa",
1423
+ "serde",
1424
+ "serde_core",
1425
+ ]
1426
+
1427
+ [[package]]
1428
+ name = "serde_spanned"
1429
+ version = "1.1.1"
1430
+ source = "registry+https://github.com/rust-lang/crates.io-index"
1431
+ checksum = "6662b5879511e06e8999a8a235d848113e942c9124f211511b16466ee2995f26"
1432
+ dependencies = [
1433
+ "serde_core",
1434
+ ]
1435
+
1436
+ [[package]]
1437
+ name = "serde_urlencoded"
1438
+ version = "0.7.1"
1439
+ source = "registry+https://github.com/rust-lang/crates.io-index"
1440
+ checksum = "d3491c14715ca2294c4d6a88f15e84739788c1d030eed8c110436aafdaa2f3fd"
1441
+ dependencies = [
1442
+ "form_urlencoded",
1443
+ "itoa",
1444
+ "ryu",
1445
+ "serde",
1446
+ ]
1447
+
1448
+ [[package]]
1449
+ name = "serde_yaml"
1450
+ version = "0.9.34+deprecated"
1451
+ source = "registry+https://github.com/rust-lang/crates.io-index"
1452
+ checksum = "6a8b1a1a2ebf674015cc02edccce75287f1a0130d394307b36743c2f5d504b47"
1453
+ dependencies = [
1454
+ "indexmap",
1455
+ "itoa",
1456
+ "ryu",
1457
+ "serde",
1458
+ "unsafe-libyaml",
1459
+ ]
1460
+
1461
+ [[package]]
1462
+ name = "sharded-slab"
1463
+ version = "0.1.7"
1464
+ source = "registry+https://github.com/rust-lang/crates.io-index"
1465
+ checksum = "f40ca3c46823713e0d4209592e8d6e826aa57e928f09752619fc696c499637f6"
1466
+ dependencies = [
1467
+ "lazy_static",
1468
+ ]
1469
+
1470
+ [[package]]
1471
+ name = "shlex"
1472
+ version = "1.3.0"
1473
+ source = "registry+https://github.com/rust-lang/crates.io-index"
1474
+ checksum = "0fda2ff0d084019ba4d7c6f371c95d8fd75ce3524c3cb8fb653a3023f6323e64"
1475
+
1476
+ [[package]]
1477
+ name = "signal-hook-registry"
1478
+ version = "1.4.8"
1479
+ source = "registry+https://github.com/rust-lang/crates.io-index"
1480
+ checksum = "c4db69cba1110affc0e9f7bcd48bbf87b3f4fc7c61fc9155afd4c469eb3d6c1b"
1481
+ dependencies = [
1482
+ "errno",
1483
+ "libc",
1484
+ ]
1485
+
1486
+ [[package]]
1487
+ name = "slab"
1488
+ version = "0.4.12"
1489
+ source = "registry+https://github.com/rust-lang/crates.io-index"
1490
+ checksum = "0c790de23124f9ab44544d7ac05d60440adc586479ce501c1d6d7da3cd8c9cf5"
1491
+
1492
+ [[package]]
1493
+ name = "smallvec"
1494
+ version = "1.15.1"
1495
+ source = "registry+https://github.com/rust-lang/crates.io-index"
1496
+ checksum = "67b1b7a3b5fe4f1376887184045fcf45c69e92af734b7aaddc05fb777b6fbd03"
1497
+
1498
+ [[package]]
1499
+ name = "socket2"
1500
+ version = "0.6.3"
1501
+ source = "registry+https://github.com/rust-lang/crates.io-index"
1502
+ checksum = "3a766e1110788c36f4fa1c2b71b387a7815aa65f88ce0229841826633d93723e"
1503
+ dependencies = [
1504
+ "libc",
1505
+ "windows-sys 0.61.2",
1506
+ ]
1507
+
1508
+ [[package]]
1509
+ name = "solverforge"
1510
+ version = "0.9.1"
1511
+ source = "registry+https://github.com/rust-lang/crates.io-index"
1512
+ checksum = "bf7016080121cd103083cff864a6bda8e2b65377bbbf852c0b05df091d584af1"
1513
+ dependencies = [
1514
+ "solverforge-config",
1515
+ "solverforge-console",
1516
+ "solverforge-core",
1517
+ "solverforge-cvrp",
1518
+ "solverforge-macros",
1519
+ "solverforge-scoring",
1520
+ "solverforge-solver",
1521
+ "tokio",
1522
+ ]
1523
+
1524
+ [[package]]
1525
+ name = "solverforge-config"
1526
+ version = "0.9.1"
1527
+ source = "registry+https://github.com/rust-lang/crates.io-index"
1528
+ checksum = "045070a1f2a86fb27e04dab7c4fed7a35a275af887622df2e64d95d05d43121e"
1529
+ dependencies = [
1530
+ "serde",
1531
+ "serde_yaml",
1532
+ "solverforge-core",
1533
+ "thiserror 2.0.18",
1534
+ "toml",
1535
+ ]
1536
+
1537
+ [[package]]
1538
+ name = "solverforge-console"
1539
+ version = "0.9.1"
1540
+ source = "registry+https://github.com/rust-lang/crates.io-index"
1541
+ checksum = "0759e5ae26e608b55c23c8b645ad63ff0f7f9db3860c1668a16966dd5bba4476"
1542
+ dependencies = [
1543
+ "num-format",
1544
+ "owo-colors",
1545
+ "tracing",
1546
+ "tracing-subscriber",
1547
+ ]
1548
+
1549
+ [[package]]
1550
+ name = "solverforge-core"
1551
+ version = "0.9.1"
1552
+ source = "registry+https://github.com/rust-lang/crates.io-index"
1553
+ checksum = "5b63b5c0213e0e602364f680637170ca9fec4b6f129de9981f118041aae5d689"
1554
+ dependencies = [
1555
+ "serde",
1556
+ "thiserror 2.0.18",
1557
+ ]
1558
+
1559
+ [[package]]
1560
+ name = "solverforge-cvrp"
1561
+ version = "0.9.1"
1562
+ source = "registry+https://github.com/rust-lang/crates.io-index"
1563
+ checksum = "287dd64c8843bfb44b75df0566dac9601ee7448926f2047066239673239a40f9"
1564
+ dependencies = [
1565
+ "solverforge-solver",
1566
+ ]
1567
+
1568
+ [[package]]
1569
+ name = "solverforge-deliveries"
1570
+ version = "1.0.0"
1571
+ dependencies = [
1572
+ "axum",
1573
+ "http-body-util",
1574
+ "parking_lot",
1575
+ "rand 0.10.1",
1576
+ "serde",
1577
+ "serde_json",
1578
+ "solverforge",
1579
+ "solverforge-maps",
1580
+ "solverforge-ui",
1581
+ "tokio",
1582
+ "tokio-stream",
1583
+ "tower",
1584
+ "tower-http",
1585
+ "uuid",
1586
+ ]
1587
+
1588
+ [[package]]
1589
+ name = "solverforge-macros"
1590
+ version = "0.9.1"
1591
+ source = "registry+https://github.com/rust-lang/crates.io-index"
1592
+ checksum = "946610887a584b315b487db0859e00abf73d54811babd627e869cbc69ea58c63"
1593
+ dependencies = [
1594
+ "proc-macro2",
1595
+ "quote",
1596
+ "syn",
1597
+ ]
1598
+
1599
+ [[package]]
1600
+ name = "solverforge-maps"
1601
+ version = "2.1.3"
1602
+ source = "registry+https://github.com/rust-lang/crates.io-index"
1603
+ checksum = "939b91fd4706c75795ef82db3a569fa47868d72763c48e24e992c00df242dbe0"
1604
+ dependencies = [
1605
+ "rayon",
1606
+ "reqwest",
1607
+ "serde",
1608
+ "serde_json",
1609
+ "tokio",
1610
+ "tracing",
1611
+ "utoipa",
1612
+ ]
1613
+
1614
+ [[package]]
1615
+ name = "solverforge-scoring"
1616
+ version = "0.9.1"
1617
+ source = "registry+https://github.com/rust-lang/crates.io-index"
1618
+ checksum = "240a9463ec2d9a50a9082bf35535179d21b4463b284c3199c0b764e0b6214a92"
1619
+ dependencies = [
1620
+ "solverforge-core",
1621
+ "thiserror 2.0.18",
1622
+ ]
1623
+
1624
+ [[package]]
1625
+ name = "solverforge-solver"
1626
+ version = "0.9.1"
1627
+ source = "registry+https://github.com/rust-lang/crates.io-index"
1628
+ checksum = "9d4ae8a78ab9b9c32f2732bf79a79c4417a86467fb1063c70f132798c3e943f5"
1629
+ dependencies = [
1630
+ "rand 0.10.1",
1631
+ "rand_chacha 0.10.0",
1632
+ "rayon",
1633
+ "serde",
1634
+ "smallvec",
1635
+ "solverforge-config",
1636
+ "solverforge-core",
1637
+ "solverforge-scoring",
1638
+ "thiserror 2.0.18",
1639
+ "tokio",
1640
+ "tracing",
1641
+ ]
1642
+
1643
+ [[package]]
1644
+ name = "solverforge-ui"
1645
+ version = "0.6.3"
1646
+ source = "registry+https://github.com/rust-lang/crates.io-index"
1647
+ checksum = "0e7539f982c47c5627e834a764af64d1dde31b806f08118491e7fac8b5388c60"
1648
+ dependencies = [
1649
+ "axum",
1650
+ "include_dir",
1651
+ ]
1652
+
1653
+ [[package]]
1654
+ name = "stable_deref_trait"
1655
+ version = "1.2.1"
1656
+ source = "registry+https://github.com/rust-lang/crates.io-index"
1657
+ checksum = "6ce2be8dc25455e1f91df71bfa12ad37d7af1092ae736f3a6cd0e37bc7810596"
1658
+
1659
+ [[package]]
1660
+ name = "subtle"
1661
+ version = "2.6.1"
1662
+ source = "registry+https://github.com/rust-lang/crates.io-index"
1663
+ checksum = "13c2bddecc57b384dee18652358fb23172facb8a2c51ccc10d74c157bdea3292"
1664
+
1665
+ [[package]]
1666
+ name = "syn"
1667
+ version = "2.0.117"
1668
+ source = "registry+https://github.com/rust-lang/crates.io-index"
1669
+ checksum = "e665b8803e7b1d2a727f4023456bbbbe74da67099c585258af0ad9c5013b9b99"
1670
+ dependencies = [
1671
+ "proc-macro2",
1672
+ "quote",
1673
+ "unicode-ident",
1674
+ ]
1675
+
1676
+ [[package]]
1677
+ name = "sync_wrapper"
1678
+ version = "1.0.2"
1679
+ source = "registry+https://github.com/rust-lang/crates.io-index"
1680
+ checksum = "0bf256ce5efdfa370213c1dabab5935a12e49f2c58d15e9eac2870d3b4f27263"
1681
+ dependencies = [
1682
+ "futures-core",
1683
+ ]
1684
+
1685
+ [[package]]
1686
+ name = "synstructure"
1687
+ version = "0.13.2"
1688
+ source = "registry+https://github.com/rust-lang/crates.io-index"
1689
+ checksum = "728a70f3dbaf5bab7f0c4b1ac8d7ae5ea60a4b5549c8a5914361c99147a709d2"
1690
+ dependencies = [
1691
+ "proc-macro2",
1692
+ "quote",
1693
+ "syn",
1694
+ ]
1695
+
1696
+ [[package]]
1697
+ name = "system-configuration"
1698
+ version = "0.7.0"
1699
+ source = "registry+https://github.com/rust-lang/crates.io-index"
1700
+ checksum = "a13f3d0daba03132c0aa9767f98351b3488edc2c100cda2d2ec2b04f3d8d3c8b"
1701
+ dependencies = [
1702
+ "bitflags",
1703
+ "core-foundation 0.9.4",
1704
+ "system-configuration-sys",
1705
+ ]
1706
+
1707
+ [[package]]
1708
+ name = "system-configuration-sys"
1709
+ version = "0.6.0"
1710
+ source = "registry+https://github.com/rust-lang/crates.io-index"
1711
+ checksum = "8e1d1b10ced5ca923a1fcb8d03e96b8d3268065d724548c0211415ff6ac6bac4"
1712
+ dependencies = [
1713
+ "core-foundation-sys",
1714
+ "libc",
1715
+ ]
1716
+
1717
+ [[package]]
1718
+ name = "thiserror"
1719
+ version = "1.0.69"
1720
+ source = "registry+https://github.com/rust-lang/crates.io-index"
1721
+ checksum = "b6aaf5339b578ea85b50e080feb250a3e8ae8cfcdff9a461c9ec2904bc923f52"
1722
+ dependencies = [
1723
+ "thiserror-impl 1.0.69",
1724
+ ]
1725
+
1726
+ [[package]]
1727
+ name = "thiserror"
1728
+ version = "2.0.18"
1729
+ source = "registry+https://github.com/rust-lang/crates.io-index"
1730
+ checksum = "4288b5bcbc7920c07a1149a35cf9590a2aa808e0bc1eafaade0b80947865fbc4"
1731
+ dependencies = [
1732
+ "thiserror-impl 2.0.18",
1733
+ ]
1734
+
1735
+ [[package]]
1736
+ name = "thiserror-impl"
1737
+ version = "1.0.69"
1738
+ source = "registry+https://github.com/rust-lang/crates.io-index"
1739
+ checksum = "4fee6c4efc90059e10f81e6d42c60a18f76588c3d74cb83a0b242a2b6c7504c1"
1740
+ dependencies = [
1741
+ "proc-macro2",
1742
+ "quote",
1743
+ "syn",
1744
+ ]
1745
+
1746
+ [[package]]
1747
+ name = "thiserror-impl"
1748
+ version = "2.0.18"
1749
+ source = "registry+https://github.com/rust-lang/crates.io-index"
1750
+ checksum = "ebc4ee7f67670e9b64d05fa4253e753e016c6c95ff35b89b7941d6b856dec1d5"
1751
+ dependencies = [
1752
+ "proc-macro2",
1753
+ "quote",
1754
+ "syn",
1755
+ ]
1756
+
1757
+ [[package]]
1758
+ name = "thread_local"
1759
+ version = "1.1.9"
1760
+ source = "registry+https://github.com/rust-lang/crates.io-index"
1761
+ checksum = "f60246a4944f24f6e018aa17cdeffb7818b76356965d03b07d6a9886e8962185"
1762
+ dependencies = [
1763
+ "cfg-if",
1764
+ ]
1765
+
1766
+ [[package]]
1767
+ name = "tinystr"
1768
+ version = "0.8.3"
1769
+ source = "registry+https://github.com/rust-lang/crates.io-index"
1770
+ checksum = "c8323304221c2a851516f22236c5722a72eaa19749016521d6dff0824447d96d"
1771
+ dependencies = [
1772
+ "displaydoc",
1773
+ "zerovec",
1774
+ ]
1775
+
1776
+ [[package]]
1777
+ name = "tinyvec"
1778
+ version = "1.11.0"
1779
+ source = "registry+https://github.com/rust-lang/crates.io-index"
1780
+ checksum = "3e61e67053d25a4e82c844e8424039d9745781b3fc4f32b8d55ed50f5f667ef3"
1781
+ dependencies = [
1782
+ "tinyvec_macros",
1783
+ ]
1784
+
1785
+ [[package]]
1786
+ name = "tinyvec_macros"
1787
+ version = "0.1.1"
1788
+ source = "registry+https://github.com/rust-lang/crates.io-index"
1789
+ checksum = "1f3ccbac311fea05f86f61904b462b55fb3df8837a366dfc601a0161d0532f20"
1790
+
1791
+ [[package]]
1792
+ name = "tokio"
1793
+ version = "1.52.1"
1794
+ source = "registry+https://github.com/rust-lang/crates.io-index"
1795
+ checksum = "b67dee974fe86fd92cc45b7a95fdd2f99a36a6d7b0d431a231178d3d670bbcc6"
1796
+ dependencies = [
1797
+ "bytes",
1798
+ "libc",
1799
+ "mio",
1800
+ "parking_lot",
1801
+ "pin-project-lite",
1802
+ "signal-hook-registry",
1803
+ "socket2",
1804
+ "tokio-macros",
1805
+ "windows-sys 0.61.2",
1806
+ ]
1807
+
1808
+ [[package]]
1809
+ name = "tokio-macros"
1810
+ version = "2.7.0"
1811
+ source = "registry+https://github.com/rust-lang/crates.io-index"
1812
+ checksum = "385a6cb71ab9ab790c5fe8d67f1645e6c450a7ce006a33de03daa956cf70a496"
1813
+ dependencies = [
1814
+ "proc-macro2",
1815
+ "quote",
1816
+ "syn",
1817
+ ]
1818
+
1819
+ [[package]]
1820
+ name = "tokio-rustls"
1821
+ version = "0.26.4"
1822
+ source = "registry+https://github.com/rust-lang/crates.io-index"
1823
+ checksum = "1729aa945f29d91ba541258c8df89027d5792d85a8841fb65e8bf0f4ede4ef61"
1824
+ dependencies = [
1825
+ "rustls",
1826
+ "tokio",
1827
+ ]
1828
+
1829
+ [[package]]
1830
+ name = "tokio-stream"
1831
+ version = "0.1.18"
1832
+ source = "registry+https://github.com/rust-lang/crates.io-index"
1833
+ checksum = "32da49809aab5c3bc678af03902d4ccddea2a87d028d86392a4b1560c6906c70"
1834
+ dependencies = [
1835
+ "futures-core",
1836
+ "pin-project-lite",
1837
+ "tokio",
1838
+ "tokio-util",
1839
+ ]
1840
+
1841
+ [[package]]
1842
+ name = "tokio-util"
1843
+ version = "0.7.18"
1844
+ source = "registry+https://github.com/rust-lang/crates.io-index"
1845
+ checksum = "9ae9cec805b01e8fc3fd2fe289f89149a9b66dd16786abd8b19cfa7b48cb0098"
1846
+ dependencies = [
1847
+ "bytes",
1848
+ "futures-core",
1849
+ "futures-sink",
1850
+ "pin-project-lite",
1851
+ "tokio",
1852
+ ]
1853
+
1854
+ [[package]]
1855
+ name = "toml"
1856
+ version = "1.1.2+spec-1.1.0"
1857
+ source = "registry+https://github.com/rust-lang/crates.io-index"
1858
+ checksum = "81f3d15e84cbcd896376e6730314d59fb5a87f31e4b038454184435cd57defee"
1859
+ dependencies = [
1860
+ "indexmap",
1861
+ "serde_core",
1862
+ "serde_spanned",
1863
+ "toml_datetime",
1864
+ "toml_parser",
1865
+ "toml_writer",
1866
+ "winnow",
1867
+ ]
1868
+
1869
+ [[package]]
1870
+ name = "toml_datetime"
1871
+ version = "1.1.1+spec-1.1.0"
1872
+ source = "registry+https://github.com/rust-lang/crates.io-index"
1873
+ checksum = "3165f65f62e28e0115a00b2ebdd37eb6f3b641855f9d636d3cd4103767159ad7"
1874
+ dependencies = [
1875
+ "serde_core",
1876
+ ]
1877
+
1878
+ [[package]]
1879
+ name = "toml_parser"
1880
+ version = "1.1.2+spec-1.1.0"
1881
+ source = "registry+https://github.com/rust-lang/crates.io-index"
1882
+ checksum = "a2abe9b86193656635d2411dc43050282ca48aa31c2451210f4202550afb7526"
1883
+ dependencies = [
1884
+ "winnow",
1885
+ ]
1886
+
1887
+ [[package]]
1888
+ name = "toml_writer"
1889
+ version = "1.1.1+spec-1.1.0"
1890
+ source = "registry+https://github.com/rust-lang/crates.io-index"
1891
+ checksum = "756daf9b1013ebe47a8776667b466417e2d4c5679d441c26230efd9ef78692db"
1892
+
1893
+ [[package]]
1894
+ name = "tower"
1895
+ version = "0.5.3"
1896
+ source = "registry+https://github.com/rust-lang/crates.io-index"
1897
+ checksum = "ebe5ef63511595f1344e2d5cfa636d973292adc0eec1f0ad45fae9f0851ab1d4"
1898
+ dependencies = [
1899
+ "futures-core",
1900
+ "futures-util",
1901
+ "pin-project-lite",
1902
+ "sync_wrapper",
1903
+ "tokio",
1904
+ "tower-layer",
1905
+ "tower-service",
1906
+ "tracing",
1907
+ ]
1908
+
1909
+ [[package]]
1910
+ name = "tower-http"
1911
+ version = "0.6.8"
1912
+ source = "registry+https://github.com/rust-lang/crates.io-index"
1913
+ checksum = "d4e6559d53cc268e5031cd8429d05415bc4cb4aefc4aa5d6cc35fbf5b924a1f8"
1914
+ dependencies = [
1915
+ "bitflags",
1916
+ "bytes",
1917
+ "futures-core",
1918
+ "futures-util",
1919
+ "http",
1920
+ "http-body",
1921
+ "http-body-util",
1922
+ "http-range-header",
1923
+ "httpdate",
1924
+ "iri-string",
1925
+ "mime",
1926
+ "mime_guess",
1927
+ "percent-encoding",
1928
+ "pin-project-lite",
1929
+ "tokio",
1930
+ "tokio-util",
1931
+ "tower",
1932
+ "tower-layer",
1933
+ "tower-service",
1934
+ "tracing",
1935
+ ]
1936
+
1937
+ [[package]]
1938
+ name = "tower-layer"
1939
+ version = "0.3.3"
1940
+ source = "registry+https://github.com/rust-lang/crates.io-index"
1941
+ checksum = "121c2a6cda46980bb0fcd1647ffaf6cd3fc79a013de288782836f6df9c48780e"
1942
+
1943
+ [[package]]
1944
+ name = "tower-service"
1945
+ version = "0.3.3"
1946
+ source = "registry+https://github.com/rust-lang/crates.io-index"
1947
+ checksum = "8df9b6e13f2d32c91b9bd719c00d1958837bc7dec474d94952798cc8e69eeec3"
1948
+
1949
+ [[package]]
1950
+ name = "tracing"
1951
+ version = "0.1.44"
1952
+ source = "registry+https://github.com/rust-lang/crates.io-index"
1953
+ checksum = "63e71662fa4b2a2c3a26f570f037eb95bb1f85397f3cd8076caed2f026a6d100"
1954
+ dependencies = [
1955
+ "log",
1956
+ "pin-project-lite",
1957
+ "tracing-attributes",
1958
+ "tracing-core",
1959
+ ]
1960
+
1961
+ [[package]]
1962
+ name = "tracing-attributes"
1963
+ version = "0.1.31"
1964
+ source = "registry+https://github.com/rust-lang/crates.io-index"
1965
+ checksum = "7490cfa5ec963746568740651ac6781f701c9c5ea257c58e057f3ba8cf69e8da"
1966
+ dependencies = [
1967
+ "proc-macro2",
1968
+ "quote",
1969
+ "syn",
1970
+ ]
1971
+
1972
+ [[package]]
1973
+ name = "tracing-core"
1974
+ version = "0.1.36"
1975
+ source = "registry+https://github.com/rust-lang/crates.io-index"
1976
+ checksum = "db97caf9d906fbde555dd62fa95ddba9eecfd14cb388e4f491a66d74cd5fb79a"
1977
+ dependencies = [
1978
+ "once_cell",
1979
+ "valuable",
1980
+ ]
1981
+
1982
+ [[package]]
1983
+ name = "tracing-log"
1984
+ version = "0.2.0"
1985
+ source = "registry+https://github.com/rust-lang/crates.io-index"
1986
+ checksum = "ee855f1f400bd0e5c02d150ae5de3840039a3f54b025156404e34c23c03f47c3"
1987
+ dependencies = [
1988
+ "log",
1989
+ "once_cell",
1990
+ "tracing-core",
1991
+ ]
1992
+
1993
+ [[package]]
1994
+ name = "tracing-subscriber"
1995
+ version = "0.3.23"
1996
+ source = "registry+https://github.com/rust-lang/crates.io-index"
1997
+ checksum = "cb7f578e5945fb242538965c2d0b04418d38ec25c79d160cd279bf0731c8d319"
1998
+ dependencies = [
1999
+ "matchers",
2000
+ "nu-ansi-term",
2001
+ "once_cell",
2002
+ "regex-automata",
2003
+ "sharded-slab",
2004
+ "smallvec",
2005
+ "thread_local",
2006
+ "tracing",
2007
+ "tracing-core",
2008
+ "tracing-log",
2009
+ ]
2010
+
2011
+ [[package]]
2012
+ name = "try-lock"
2013
+ version = "0.2.5"
2014
+ source = "registry+https://github.com/rust-lang/crates.io-index"
2015
+ checksum = "e421abadd41a4225275504ea4d6566923418b7f05506fbc9c0fe86ba7396114b"
2016
+
2017
+ [[package]]
2018
+ name = "unicase"
2019
+ version = "2.9.0"
2020
+ source = "registry+https://github.com/rust-lang/crates.io-index"
2021
+ checksum = "dbc4bc3a9f746d862c45cb89d705aa10f187bb96c76001afab07a0d35ce60142"
2022
+
2023
+ [[package]]
2024
+ name = "unicode-ident"
2025
+ version = "1.0.24"
2026
+ source = "registry+https://github.com/rust-lang/crates.io-index"
2027
+ checksum = "e6e4313cd5fcd3dad5cafa179702e2b244f760991f45397d14d4ebf38247da75"
2028
+
2029
+ [[package]]
2030
+ name = "unicode-xid"
2031
+ version = "0.2.6"
2032
+ source = "registry+https://github.com/rust-lang/crates.io-index"
2033
+ checksum = "ebc1c04c71510c7f702b52b7c350734c9ff1295c464a03335b00bb84fc54f853"
2034
+
2035
+ [[package]]
2036
+ name = "unsafe-libyaml"
2037
+ version = "0.2.11"
2038
+ source = "registry+https://github.com/rust-lang/crates.io-index"
2039
+ checksum = "673aac59facbab8a9007c7f6108d11f63b603f7cabff99fabf650fea5c32b861"
2040
+
2041
+ [[package]]
2042
+ name = "untrusted"
2043
+ version = "0.9.0"
2044
+ source = "registry+https://github.com/rust-lang/crates.io-index"
2045
+ checksum = "8ecb6da28b8a351d773b68d5825ac39017e680750f980f3a1a85cd8dd28a47c1"
2046
+
2047
+ [[package]]
2048
+ name = "url"
2049
+ version = "2.5.8"
2050
+ source = "registry+https://github.com/rust-lang/crates.io-index"
2051
+ checksum = "ff67a8a4397373c3ef660812acab3268222035010ab8680ec4215f38ba3d0eed"
2052
+ dependencies = [
2053
+ "form_urlencoded",
2054
+ "idna",
2055
+ "percent-encoding",
2056
+ "serde",
2057
+ ]
2058
+
2059
+ [[package]]
2060
+ name = "utf8_iter"
2061
+ version = "1.0.4"
2062
+ source = "registry+https://github.com/rust-lang/crates.io-index"
2063
+ checksum = "b6c140620e7ffbb22c2dee59cafe6084a59b5ffc27a8859a5f0d494b5d52b6be"
2064
+
2065
+ [[package]]
2066
+ name = "utoipa"
2067
+ version = "5.4.0"
2068
+ source = "registry+https://github.com/rust-lang/crates.io-index"
2069
+ checksum = "2fcc29c80c21c31608227e0912b2d7fddba57ad76b606890627ba8ee7964e993"
2070
+ dependencies = [
2071
+ "indexmap",
2072
+ "serde",
2073
+ "serde_json",
2074
+ "utoipa-gen",
2075
+ ]
2076
+
2077
+ [[package]]
2078
+ name = "utoipa-gen"
2079
+ version = "5.4.0"
2080
+ source = "registry+https://github.com/rust-lang/crates.io-index"
2081
+ checksum = "6d79d08d92ab8af4c5e8a6da20c47ae3f61a0f1dabc1997cdf2d082b757ca08b"
2082
+ dependencies = [
2083
+ "proc-macro2",
2084
+ "quote",
2085
+ "syn",
2086
+ ]
2087
+
2088
+ [[package]]
2089
+ name = "uuid"
2090
+ version = "1.23.1"
2091
+ source = "registry+https://github.com/rust-lang/crates.io-index"
2092
+ checksum = "ddd74a9687298c6858e9b88ec8935ec45d22e8fd5e6394fa1bd4e99a87789c76"
2093
+ dependencies = [
2094
+ "getrandom 0.4.2",
2095
+ "js-sys",
2096
+ "serde_core",
2097
+ "wasm-bindgen",
2098
+ ]
2099
+
2100
+ [[package]]
2101
+ name = "valuable"
2102
+ version = "0.1.1"
2103
+ source = "registry+https://github.com/rust-lang/crates.io-index"
2104
+ checksum = "ba73ea9cf16a25df0c8caa16c51acb937d5712a8429db78a3ee29d5dcacd3a65"
2105
+
2106
+ [[package]]
2107
+ name = "walkdir"
2108
+ version = "2.5.0"
2109
+ source = "registry+https://github.com/rust-lang/crates.io-index"
2110
+ checksum = "29790946404f91d9c5d06f9874efddea1dc06c5efe94541a7d6863108e3a5e4b"
2111
+ dependencies = [
2112
+ "same-file",
2113
+ "winapi-util",
2114
+ ]
2115
+
2116
+ [[package]]
2117
+ name = "want"
2118
+ version = "0.3.1"
2119
+ source = "registry+https://github.com/rust-lang/crates.io-index"
2120
+ checksum = "bfa7760aed19e106de2c7c0b581b509f2f25d3dacaf737cb82ac61bc6d760b0e"
2121
+ dependencies = [
2122
+ "try-lock",
2123
+ ]
2124
+
2125
+ [[package]]
2126
+ name = "wasi"
2127
+ version = "0.11.1+wasi-snapshot-preview1"
2128
+ source = "registry+https://github.com/rust-lang/crates.io-index"
2129
+ checksum = "ccf3ec651a847eb01de73ccad15eb7d99f80485de043efb2f370cd654f4ea44b"
2130
+
2131
+ [[package]]
2132
+ name = "wasip2"
2133
+ version = "1.0.3+wasi-0.2.9"
2134
+ source = "registry+https://github.com/rust-lang/crates.io-index"
2135
+ checksum = "20064672db26d7cdc89c7798c48a0fdfac8213434a1186e5ef29fd560ae223d6"
2136
+ dependencies = [
2137
+ "wit-bindgen 0.57.1",
2138
+ ]
2139
+
2140
+ [[package]]
2141
+ name = "wasip3"
2142
+ version = "0.4.0+wasi-0.3.0-rc-2026-01-06"
2143
+ source = "registry+https://github.com/rust-lang/crates.io-index"
2144
+ checksum = "5428f8bf88ea5ddc08faddef2ac4a67e390b88186c703ce6dbd955e1c145aca5"
2145
+ dependencies = [
2146
+ "wit-bindgen 0.51.0",
2147
+ ]
2148
+
2149
+ [[package]]
2150
+ name = "wasm-bindgen"
2151
+ version = "0.2.118"
2152
+ source = "registry+https://github.com/rust-lang/crates.io-index"
2153
+ checksum = "0bf938a0bacb0469e83c1e148908bd7d5a6010354cf4fb73279b7447422e3a89"
2154
+ dependencies = [
2155
+ "cfg-if",
2156
+ "once_cell",
2157
+ "rustversion",
2158
+ "wasm-bindgen-macro",
2159
+ "wasm-bindgen-shared",
2160
+ ]
2161
+
2162
+ [[package]]
2163
+ name = "wasm-bindgen-futures"
2164
+ version = "0.4.68"
2165
+ source = "registry+https://github.com/rust-lang/crates.io-index"
2166
+ checksum = "f371d383f2fb139252e0bfac3b81b265689bf45b6874af544ffa4c975ac1ebf8"
2167
+ dependencies = [
2168
+ "js-sys",
2169
+ "wasm-bindgen",
2170
+ ]
2171
+
2172
+ [[package]]
2173
+ name = "wasm-bindgen-macro"
2174
+ version = "0.2.118"
2175
+ source = "registry+https://github.com/rust-lang/crates.io-index"
2176
+ checksum = "eeff24f84126c0ec2db7a449f0c2ec963c6a49efe0698c4242929da037ca28ed"
2177
+ dependencies = [
2178
+ "quote",
2179
+ "wasm-bindgen-macro-support",
2180
+ ]
2181
+
2182
+ [[package]]
2183
+ name = "wasm-bindgen-macro-support"
2184
+ version = "0.2.118"
2185
+ source = "registry+https://github.com/rust-lang/crates.io-index"
2186
+ checksum = "9d08065faf983b2b80a79fd87d8254c409281cf7de75fc4b773019824196c904"
2187
+ dependencies = [
2188
+ "bumpalo",
2189
+ "proc-macro2",
2190
+ "quote",
2191
+ "syn",
2192
+ "wasm-bindgen-shared",
2193
+ ]
2194
+
2195
+ [[package]]
2196
+ name = "wasm-bindgen-shared"
2197
+ version = "0.2.118"
2198
+ source = "registry+https://github.com/rust-lang/crates.io-index"
2199
+ checksum = "5fd04d9e306f1907bd13c6361b5c6bfc7b3b3c095ed3f8a9246390f8dbdee129"
2200
+ dependencies = [
2201
+ "unicode-ident",
2202
+ ]
2203
+
2204
+ [[package]]
2205
+ name = "wasm-encoder"
2206
+ version = "0.244.0"
2207
+ source = "registry+https://github.com/rust-lang/crates.io-index"
2208
+ checksum = "990065f2fe63003fe337b932cfb5e3b80e0b4d0f5ff650e6985b1048f62c8319"
2209
+ dependencies = [
2210
+ "leb128fmt",
2211
+ "wasmparser",
2212
+ ]
2213
+
2214
+ [[package]]
2215
+ name = "wasm-metadata"
2216
+ version = "0.244.0"
2217
+ source = "registry+https://github.com/rust-lang/crates.io-index"
2218
+ checksum = "bb0e353e6a2fbdc176932bbaab493762eb1255a7900fe0fea1a2f96c296cc909"
2219
+ dependencies = [
2220
+ "anyhow",
2221
+ "indexmap",
2222
+ "wasm-encoder",
2223
+ "wasmparser",
2224
+ ]
2225
+
2226
+ [[package]]
2227
+ name = "wasmparser"
2228
+ version = "0.244.0"
2229
+ source = "registry+https://github.com/rust-lang/crates.io-index"
2230
+ checksum = "47b807c72e1bac69382b3a6fb3dbe8ea4c0ed87ff5629b8685ae6b9a611028fe"
2231
+ dependencies = [
2232
+ "bitflags",
2233
+ "hashbrown 0.15.5",
2234
+ "indexmap",
2235
+ "semver",
2236
+ ]
2237
+
2238
+ [[package]]
2239
+ name = "web-sys"
2240
+ version = "0.3.95"
2241
+ source = "registry+https://github.com/rust-lang/crates.io-index"
2242
+ checksum = "4f2dfbb17949fa2088e5d39408c48368947b86f7834484e87b73de55bc14d97d"
2243
+ dependencies = [
2244
+ "js-sys",
2245
+ "wasm-bindgen",
2246
+ ]
2247
+
2248
+ [[package]]
2249
+ name = "web-time"
2250
+ version = "1.1.0"
2251
+ source = "registry+https://github.com/rust-lang/crates.io-index"
2252
+ checksum = "5a6580f308b1fad9207618087a65c04e7a10bc77e02c8e84e9b00dd4b12fa0bb"
2253
+ dependencies = [
2254
+ "js-sys",
2255
+ "wasm-bindgen",
2256
+ ]
2257
+
2258
+ [[package]]
2259
+ name = "webpki-root-certs"
2260
+ version = "1.0.7"
2261
+ source = "registry+https://github.com/rust-lang/crates.io-index"
2262
+ checksum = "f31141ce3fc3e300ae89b78c0dd67f9708061d1d2eda54b8209346fd6be9a92c"
2263
+ dependencies = [
2264
+ "rustls-pki-types",
2265
+ ]
2266
+
2267
+ [[package]]
2268
+ name = "winapi-util"
2269
+ version = "0.1.11"
2270
+ source = "registry+https://github.com/rust-lang/crates.io-index"
2271
+ checksum = "c2a7b1c03c876122aa43f3020e6c3c3ee5c05081c9a00739faf7503aeba10d22"
2272
+ dependencies = [
2273
+ "windows-sys 0.61.2",
2274
+ ]
2275
+
2276
+ [[package]]
2277
+ name = "windows-link"
2278
+ version = "0.2.1"
2279
+ source = "registry+https://github.com/rust-lang/crates.io-index"
2280
+ checksum = "f0805222e57f7521d6a62e36fa9163bc891acd422f971defe97d64e70d0a4fe5"
2281
+
2282
+ [[package]]
2283
+ name = "windows-registry"
2284
+ version = "0.6.1"
2285
+ source = "registry+https://github.com/rust-lang/crates.io-index"
2286
+ checksum = "02752bf7fbdcce7f2a27a742f798510f3e5ad88dbe84871e5168e2120c3d5720"
2287
+ dependencies = [
2288
+ "windows-link",
2289
+ "windows-result",
2290
+ "windows-strings",
2291
+ ]
2292
+
2293
+ [[package]]
2294
+ name = "windows-result"
2295
+ version = "0.4.1"
2296
+ source = "registry+https://github.com/rust-lang/crates.io-index"
2297
+ checksum = "7781fa89eaf60850ac3d2da7af8e5242a5ea78d1a11c49bf2910bb5a73853eb5"
2298
+ dependencies = [
2299
+ "windows-link",
2300
+ ]
2301
+
2302
+ [[package]]
2303
+ name = "windows-strings"
2304
+ version = "0.5.1"
2305
+ source = "registry+https://github.com/rust-lang/crates.io-index"
2306
+ checksum = "7837d08f69c77cf6b07689544538e017c1bfcf57e34b4c0ff58e6c2cd3b37091"
2307
+ dependencies = [
2308
+ "windows-link",
2309
+ ]
2310
+
2311
+ [[package]]
2312
+ name = "windows-sys"
2313
+ version = "0.45.0"
2314
+ source = "registry+https://github.com/rust-lang/crates.io-index"
2315
+ checksum = "75283be5efb2831d37ea142365f009c02ec203cd29a3ebecbc093d52315b66d0"
2316
+ dependencies = [
2317
+ "windows-targets 0.42.2",
2318
+ ]
2319
+
2320
+ [[package]]
2321
+ name = "windows-sys"
2322
+ version = "0.52.0"
2323
+ source = "registry+https://github.com/rust-lang/crates.io-index"
2324
+ checksum = "282be5f36a8ce781fad8c8ae18fa3f9beff57ec1b52cb3de0789201425d9a33d"
2325
+ dependencies = [
2326
+ "windows-targets 0.52.6",
2327
+ ]
2328
+
2329
+ [[package]]
2330
+ name = "windows-sys"
2331
+ version = "0.60.2"
2332
+ source = "registry+https://github.com/rust-lang/crates.io-index"
2333
+ checksum = "f2f500e4d28234f72040990ec9d39e3a6b950f9f22d3dba18416c35882612bcb"
2334
+ dependencies = [
2335
+ "windows-targets 0.53.5",
2336
+ ]
2337
+
2338
+ [[package]]
2339
+ name = "windows-sys"
2340
+ version = "0.61.2"
2341
+ source = "registry+https://github.com/rust-lang/crates.io-index"
2342
+ checksum = "ae137229bcbd6cdf0f7b80a31df61766145077ddf49416a728b02cb3921ff3fc"
2343
+ dependencies = [
2344
+ "windows-link",
2345
+ ]
2346
+
2347
+ [[package]]
2348
+ name = "windows-targets"
2349
+ version = "0.42.2"
2350
+ source = "registry+https://github.com/rust-lang/crates.io-index"
2351
+ checksum = "8e5180c00cd44c9b1c88adb3693291f1cd93605ded80c250a75d472756b4d071"
2352
+ dependencies = [
2353
+ "windows_aarch64_gnullvm 0.42.2",
2354
+ "windows_aarch64_msvc 0.42.2",
2355
+ "windows_i686_gnu 0.42.2",
2356
+ "windows_i686_msvc 0.42.2",
2357
+ "windows_x86_64_gnu 0.42.2",
2358
+ "windows_x86_64_gnullvm 0.42.2",
2359
+ "windows_x86_64_msvc 0.42.2",
2360
+ ]
2361
+
2362
+ [[package]]
2363
+ name = "windows-targets"
2364
+ version = "0.52.6"
2365
+ source = "registry+https://github.com/rust-lang/crates.io-index"
2366
+ checksum = "9b724f72796e036ab90c1021d4780d4d3d648aca59e491e6b98e725b84e99973"
2367
+ dependencies = [
2368
+ "windows_aarch64_gnullvm 0.52.6",
2369
+ "windows_aarch64_msvc 0.52.6",
2370
+ "windows_i686_gnu 0.52.6",
2371
+ "windows_i686_gnullvm 0.52.6",
2372
+ "windows_i686_msvc 0.52.6",
2373
+ "windows_x86_64_gnu 0.52.6",
2374
+ "windows_x86_64_gnullvm 0.52.6",
2375
+ "windows_x86_64_msvc 0.52.6",
2376
+ ]
2377
+
2378
+ [[package]]
2379
+ name = "windows-targets"
2380
+ version = "0.53.5"
2381
+ source = "registry+https://github.com/rust-lang/crates.io-index"
2382
+ checksum = "4945f9f551b88e0d65f3db0bc25c33b8acea4d9e41163edf90dcd0b19f9069f3"
2383
+ dependencies = [
2384
+ "windows-link",
2385
+ "windows_aarch64_gnullvm 0.53.1",
2386
+ "windows_aarch64_msvc 0.53.1",
2387
+ "windows_i686_gnu 0.53.1",
2388
+ "windows_i686_gnullvm 0.53.1",
2389
+ "windows_i686_msvc 0.53.1",
2390
+ "windows_x86_64_gnu 0.53.1",
2391
+ "windows_x86_64_gnullvm 0.53.1",
2392
+ "windows_x86_64_msvc 0.53.1",
2393
+ ]
2394
+
2395
+ [[package]]
2396
+ name = "windows_aarch64_gnullvm"
2397
+ version = "0.42.2"
2398
+ source = "registry+https://github.com/rust-lang/crates.io-index"
2399
+ checksum = "597a5118570b68bc08d8d59125332c54f1ba9d9adeedeef5b99b02ba2b0698f8"
2400
+
2401
+ [[package]]
2402
+ name = "windows_aarch64_gnullvm"
2403
+ version = "0.52.6"
2404
+ source = "registry+https://github.com/rust-lang/crates.io-index"
2405
+ checksum = "32a4622180e7a0ec044bb555404c800bc9fd9ec262ec147edd5989ccd0c02cd3"
2406
+
2407
+ [[package]]
2408
+ name = "windows_aarch64_gnullvm"
2409
+ version = "0.53.1"
2410
+ source = "registry+https://github.com/rust-lang/crates.io-index"
2411
+ checksum = "a9d8416fa8b42f5c947f8482c43e7d89e73a173cead56d044f6a56104a6d1b53"
2412
+
2413
+ [[package]]
2414
+ name = "windows_aarch64_msvc"
2415
+ version = "0.42.2"
2416
+ source = "registry+https://github.com/rust-lang/crates.io-index"
2417
+ checksum = "e08e8864a60f06ef0d0ff4ba04124db8b0fb3be5776a5cd47641e942e58c4d43"
2418
+
2419
+ [[package]]
2420
+ name = "windows_aarch64_msvc"
2421
+ version = "0.52.6"
2422
+ source = "registry+https://github.com/rust-lang/crates.io-index"
2423
+ checksum = "09ec2a7bb152e2252b53fa7803150007879548bc709c039df7627cabbd05d469"
2424
+
2425
+ [[package]]
2426
+ name = "windows_aarch64_msvc"
2427
+ version = "0.53.1"
2428
+ source = "registry+https://github.com/rust-lang/crates.io-index"
2429
+ checksum = "b9d782e804c2f632e395708e99a94275910eb9100b2114651e04744e9b125006"
2430
+
2431
+ [[package]]
2432
+ name = "windows_i686_gnu"
2433
+ version = "0.42.2"
2434
+ source = "registry+https://github.com/rust-lang/crates.io-index"
2435
+ checksum = "c61d927d8da41da96a81f029489353e68739737d3beca43145c8afec9a31a84f"
2436
+
2437
+ [[package]]
2438
+ name = "windows_i686_gnu"
2439
+ version = "0.52.6"
2440
+ source = "registry+https://github.com/rust-lang/crates.io-index"
2441
+ checksum = "8e9b5ad5ab802e97eb8e295ac6720e509ee4c243f69d781394014ebfe8bbfa0b"
2442
+
2443
+ [[package]]
2444
+ name = "windows_i686_gnu"
2445
+ version = "0.53.1"
2446
+ source = "registry+https://github.com/rust-lang/crates.io-index"
2447
+ checksum = "960e6da069d81e09becb0ca57a65220ddff016ff2d6af6a223cf372a506593a3"
2448
+
2449
+ [[package]]
2450
+ name = "windows_i686_gnullvm"
2451
+ version = "0.52.6"
2452
+ source = "registry+https://github.com/rust-lang/crates.io-index"
2453
+ checksum = "0eee52d38c090b3caa76c563b86c3a4bd71ef1a819287c19d586d7334ae8ed66"
2454
+
2455
+ [[package]]
2456
+ name = "windows_i686_gnullvm"
2457
+ version = "0.53.1"
2458
+ source = "registry+https://github.com/rust-lang/crates.io-index"
2459
+ checksum = "fa7359d10048f68ab8b09fa71c3daccfb0e9b559aed648a8f95469c27057180c"
2460
+
2461
+ [[package]]
2462
+ name = "windows_i686_msvc"
2463
+ version = "0.42.2"
2464
+ source = "registry+https://github.com/rust-lang/crates.io-index"
2465
+ checksum = "44d840b6ec649f480a41c8d80f9c65108b92d89345dd94027bfe06ac444d1060"
2466
+
2467
+ [[package]]
2468
+ name = "windows_i686_msvc"
2469
+ version = "0.52.6"
2470
+ source = "registry+https://github.com/rust-lang/crates.io-index"
2471
+ checksum = "240948bc05c5e7c6dabba28bf89d89ffce3e303022809e73deaefe4f6ec56c66"
2472
+
2473
+ [[package]]
2474
+ name = "windows_i686_msvc"
2475
+ version = "0.53.1"
2476
+ source = "registry+https://github.com/rust-lang/crates.io-index"
2477
+ checksum = "1e7ac75179f18232fe9c285163565a57ef8d3c89254a30685b57d83a38d326c2"
2478
+
2479
+ [[package]]
2480
+ name = "windows_x86_64_gnu"
2481
+ version = "0.42.2"
2482
+ source = "registry+https://github.com/rust-lang/crates.io-index"
2483
+ checksum = "8de912b8b8feb55c064867cf047dda097f92d51efad5b491dfb98f6bbb70cb36"
2484
+
2485
+ [[package]]
2486
+ name = "windows_x86_64_gnu"
2487
+ version = "0.52.6"
2488
+ source = "registry+https://github.com/rust-lang/crates.io-index"
2489
+ checksum = "147a5c80aabfbf0c7d901cb5895d1de30ef2907eb21fbbab29ca94c5b08b1a78"
2490
+
2491
+ [[package]]
2492
+ name = "windows_x86_64_gnu"
2493
+ version = "0.53.1"
2494
+ source = "registry+https://github.com/rust-lang/crates.io-index"
2495
+ checksum = "9c3842cdd74a865a8066ab39c8a7a473c0778a3f29370b5fd6b4b9aa7df4a499"
2496
+
2497
+ [[package]]
2498
+ name = "windows_x86_64_gnullvm"
2499
+ version = "0.42.2"
2500
+ source = "registry+https://github.com/rust-lang/crates.io-index"
2501
+ checksum = "26d41b46a36d453748aedef1486d5c7a85db22e56aff34643984ea85514e94a3"
2502
+
2503
+ [[package]]
2504
+ name = "windows_x86_64_gnullvm"
2505
+ version = "0.52.6"
2506
+ source = "registry+https://github.com/rust-lang/crates.io-index"
2507
+ checksum = "24d5b23dc417412679681396f2b49f3de8c1473deb516bd34410872eff51ed0d"
2508
+
2509
+ [[package]]
2510
+ name = "windows_x86_64_gnullvm"
2511
+ version = "0.53.1"
2512
+ source = "registry+https://github.com/rust-lang/crates.io-index"
2513
+ checksum = "0ffa179e2d07eee8ad8f57493436566c7cc30ac536a3379fdf008f47f6bb7ae1"
2514
+
2515
+ [[package]]
2516
+ name = "windows_x86_64_msvc"
2517
+ version = "0.42.2"
2518
+ source = "registry+https://github.com/rust-lang/crates.io-index"
2519
+ checksum = "9aec5da331524158c6d1a4ac0ab1541149c0b9505fde06423b02f5ef0106b9f0"
2520
+
2521
+ [[package]]
2522
+ name = "windows_x86_64_msvc"
2523
+ version = "0.52.6"
2524
+ source = "registry+https://github.com/rust-lang/crates.io-index"
2525
+ checksum = "589f6da84c646204747d1270a2a5661ea66ed1cced2631d546fdfb155959f9ec"
2526
+
2527
+ [[package]]
2528
+ name = "windows_x86_64_msvc"
2529
+ version = "0.53.1"
2530
+ source = "registry+https://github.com/rust-lang/crates.io-index"
2531
+ checksum = "d6bbff5f0aada427a1e5a6da5f1f98158182f26556f345ac9e04d36d0ebed650"
2532
+
2533
+ [[package]]
2534
+ name = "winnow"
2535
+ version = "1.0.2"
2536
+ source = "registry+https://github.com/rust-lang/crates.io-index"
2537
+ checksum = "2ee1708bef14716a11bae175f579062d4554d95be2c6829f518df847b7b3fdd0"
2538
+
2539
+ [[package]]
2540
+ name = "wit-bindgen"
2541
+ version = "0.51.0"
2542
+ source = "registry+https://github.com/rust-lang/crates.io-index"
2543
+ checksum = "d7249219f66ced02969388cf2bb044a09756a083d0fab1e566056b04d9fbcaa5"
2544
+ dependencies = [
2545
+ "wit-bindgen-rust-macro",
2546
+ ]
2547
+
2548
+ [[package]]
2549
+ name = "wit-bindgen"
2550
+ version = "0.57.1"
2551
+ source = "registry+https://github.com/rust-lang/crates.io-index"
2552
+ checksum = "1ebf944e87a7c253233ad6766e082e3cd714b5d03812acc24c318f549614536e"
2553
+
2554
+ [[package]]
2555
+ name = "wit-bindgen-core"
2556
+ version = "0.51.0"
2557
+ source = "registry+https://github.com/rust-lang/crates.io-index"
2558
+ checksum = "ea61de684c3ea68cb082b7a88508a8b27fcc8b797d738bfc99a82facf1d752dc"
2559
+ dependencies = [
2560
+ "anyhow",
2561
+ "heck",
2562
+ "wit-parser",
2563
+ ]
2564
+
2565
+ [[package]]
2566
+ name = "wit-bindgen-rust"
2567
+ version = "0.51.0"
2568
+ source = "registry+https://github.com/rust-lang/crates.io-index"
2569
+ checksum = "b7c566e0f4b284dd6561c786d9cb0142da491f46a9fbed79ea69cdad5db17f21"
2570
+ dependencies = [
2571
+ "anyhow",
2572
+ "heck",
2573
+ "indexmap",
2574
+ "prettyplease",
2575
+ "syn",
2576
+ "wasm-metadata",
2577
+ "wit-bindgen-core",
2578
+ "wit-component",
2579
+ ]
2580
+
2581
+ [[package]]
2582
+ name = "wit-bindgen-rust-macro"
2583
+ version = "0.51.0"
2584
+ source = "registry+https://github.com/rust-lang/crates.io-index"
2585
+ checksum = "0c0f9bfd77e6a48eccf51359e3ae77140a7f50b1e2ebfe62422d8afdaffab17a"
2586
+ dependencies = [
2587
+ "anyhow",
2588
+ "prettyplease",
2589
+ "proc-macro2",
2590
+ "quote",
2591
+ "syn",
2592
+ "wit-bindgen-core",
2593
+ "wit-bindgen-rust",
2594
+ ]
2595
+
2596
+ [[package]]
2597
+ name = "wit-component"
2598
+ version = "0.244.0"
2599
+ source = "registry+https://github.com/rust-lang/crates.io-index"
2600
+ checksum = "9d66ea20e9553b30172b5e831994e35fbde2d165325bec84fc43dbf6f4eb9cb2"
2601
+ dependencies = [
2602
+ "anyhow",
2603
+ "bitflags",
2604
+ "indexmap",
2605
+ "log",
2606
+ "serde",
2607
+ "serde_derive",
2608
+ "serde_json",
2609
+ "wasm-encoder",
2610
+ "wasm-metadata",
2611
+ "wasmparser",
2612
+ "wit-parser",
2613
+ ]
2614
+
2615
+ [[package]]
2616
+ name = "wit-parser"
2617
+ version = "0.244.0"
2618
+ source = "registry+https://github.com/rust-lang/crates.io-index"
2619
+ checksum = "ecc8ac4bc1dc3381b7f59c34f00b67e18f910c2c0f50015669dde7def656a736"
2620
+ dependencies = [
2621
+ "anyhow",
2622
+ "id-arena",
2623
+ "indexmap",
2624
+ "log",
2625
+ "semver",
2626
+ "serde",
2627
+ "serde_derive",
2628
+ "serde_json",
2629
+ "unicode-xid",
2630
+ "wasmparser",
2631
+ ]
2632
+
2633
+ [[package]]
2634
+ name = "writeable"
2635
+ version = "0.6.3"
2636
+ source = "registry+https://github.com/rust-lang/crates.io-index"
2637
+ checksum = "1ffae5123b2d3fc086436f8834ae3ab053a283cfac8fe0a0b8eaae044768a4c4"
2638
+
2639
+ [[package]]
2640
+ name = "yoke"
2641
+ version = "0.8.2"
2642
+ source = "registry+https://github.com/rust-lang/crates.io-index"
2643
+ checksum = "abe8c5fda708d9ca3df187cae8bfb9ceda00dd96231bed36e445a1a48e66f9ca"
2644
+ dependencies = [
2645
+ "stable_deref_trait",
2646
+ "yoke-derive",
2647
+ "zerofrom",
2648
+ ]
2649
+
2650
+ [[package]]
2651
+ name = "yoke-derive"
2652
+ version = "0.8.2"
2653
+ source = "registry+https://github.com/rust-lang/crates.io-index"
2654
+ checksum = "de844c262c8848816172cef550288e7dc6c7b7814b4ee56b3e1553f275f1858e"
2655
+ dependencies = [
2656
+ "proc-macro2",
2657
+ "quote",
2658
+ "syn",
2659
+ "synstructure",
2660
+ ]
2661
+
2662
+ [[package]]
2663
+ name = "zerocopy"
2664
+ version = "0.8.48"
2665
+ source = "registry+https://github.com/rust-lang/crates.io-index"
2666
+ checksum = "eed437bf9d6692032087e337407a86f04cd8d6a16a37199ed57949d415bd68e9"
2667
+ dependencies = [
2668
+ "zerocopy-derive",
2669
+ ]
2670
+
2671
+ [[package]]
2672
+ name = "zerocopy-derive"
2673
+ version = "0.8.48"
2674
+ source = "registry+https://github.com/rust-lang/crates.io-index"
2675
+ checksum = "70e3cd084b1788766f53af483dd21f93881ff30d7320490ec3ef7526d203bad4"
2676
+ dependencies = [
2677
+ "proc-macro2",
2678
+ "quote",
2679
+ "syn",
2680
+ ]
2681
+
2682
+ [[package]]
2683
+ name = "zerofrom"
2684
+ version = "0.1.7"
2685
+ source = "registry+https://github.com/rust-lang/crates.io-index"
2686
+ checksum = "69faa1f2a1ea75661980b013019ed6687ed0e83d069bc1114e2cc74c6c04c4df"
2687
+ dependencies = [
2688
+ "zerofrom-derive",
2689
+ ]
2690
+
2691
+ [[package]]
2692
+ name = "zerofrom-derive"
2693
+ version = "0.1.7"
2694
+ source = "registry+https://github.com/rust-lang/crates.io-index"
2695
+ checksum = "11532158c46691caf0f2593ea8358fed6bbf68a0315e80aae9bd41fbade684a1"
2696
+ dependencies = [
2697
+ "proc-macro2",
2698
+ "quote",
2699
+ "syn",
2700
+ "synstructure",
2701
+ ]
2702
+
2703
+ [[package]]
2704
+ name = "zeroize"
2705
+ version = "1.8.2"
2706
+ source = "registry+https://github.com/rust-lang/crates.io-index"
2707
+ checksum = "b97154e67e32c85465826e8bcc1c59429aaaf107c1e4a9e53c8d8ccd5eff88d0"
2708
+
2709
+ [[package]]
2710
+ name = "zerotrie"
2711
+ version = "0.2.4"
2712
+ source = "registry+https://github.com/rust-lang/crates.io-index"
2713
+ checksum = "0f9152d31db0792fa83f70fb2f83148effb5c1f5b8c7686c3459e361d9bc20bf"
2714
+ dependencies = [
2715
+ "displaydoc",
2716
+ "yoke",
2717
+ "zerofrom",
2718
+ ]
2719
+
2720
+ [[package]]
2721
+ name = "zerovec"
2722
+ version = "0.11.6"
2723
+ source = "registry+https://github.com/rust-lang/crates.io-index"
2724
+ checksum = "90f911cbc359ab6af17377d242225f4d75119aec87ea711a880987b18cd7b239"
2725
+ dependencies = [
2726
+ "yoke",
2727
+ "zerofrom",
2728
+ "zerovec-derive",
2729
+ ]
2730
+
2731
+ [[package]]
2732
+ name = "zerovec-derive"
2733
+ version = "0.11.3"
2734
+ source = "registry+https://github.com/rust-lang/crates.io-index"
2735
+ checksum = "625dc425cab0dca6dc3c3319506e6593dcb08a9f387ea3b284dbd52a92c40555"
2736
+ dependencies = [
2737
+ "proc-macro2",
2738
+ "quote",
2739
+ "syn",
2740
+ ]
2741
+
2742
+ [[package]]
2743
+ name = "zmij"
2744
+ version = "1.0.21"
2745
+ source = "registry+https://github.com/rust-lang/crates.io-index"
2746
+ checksum = "b8848ee67ecc8aedbaf3e4122217aff892639231befc6a1b58d29fff4c2cabaa"
Cargo.toml ADDED
@@ -0,0 +1,37 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ [package]
2
+ name = "solverforge-deliveries"
3
+ version = "1.0.0"
4
+ edition = "2021"
5
+ rust-version = "1.95"
6
+ description = "Constraint optimizer built with SolverForge"
7
+
8
+ [[bin]]
9
+ name = "solverforge_deliveries"
10
+ path = "src/main.rs"
11
+
12
+ [dependencies]
13
+ solverforge = { version = "0.9.1", features = [
14
+ "serde",
15
+ "console",
16
+ "verbose-logging",
17
+ ] }
18
+ solverforge-ui = "0.6.3"
19
+ solverforge-maps = "2.1.3"
20
+ # Web server
21
+ axum = "0.8.9"
22
+ tokio = { version = "1.52.1", features = ["full"] }
23
+ tokio-stream = { version = "0.1.18", features = ["sync"] }
24
+ tower-http = { version = "0.6.8", features = ["fs", "cors"] }
25
+ tower = "0.5.3"
26
+
27
+ # Serialization
28
+ serde = { version = "1.0.228", features = ["derive"] }
29
+ serde_json = "1.0.149"
30
+ rand = "0.10.1"
31
+
32
+ # Utilities
33
+ uuid = { version = "1.23.1", features = ["v4", "serde"] }
34
+ parking_lot = "0.12.5"
35
+
36
+ [dev-dependencies]
37
+ http-body-util = "0.1.3"
solver.toml ADDED
@@ -0,0 +1,76 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ environment_mode = "reproducible"
2
+ random_seed = 42
3
+
4
+ [termination]
5
+ seconds_spent_limit = 60
6
+
7
+ [[phases]]
8
+ type = "construction_heuristic"
9
+ construction_heuristic_type = "list_clarke_wright"
10
+ entity_class = "Vehicle"
11
+ variable_name = "delivery_order"
12
+
13
+ [[phases]]
14
+ type = "construction_heuristic"
15
+ construction_heuristic_type = "list_k_opt"
16
+ k = 2
17
+ entity_class = "Vehicle"
18
+ variable_name = "delivery_order"
19
+
20
+ [[phases]]
21
+ type = "local_search"
22
+
23
+ [phases.acceptor]
24
+ type = "late_acceptance"
25
+ late_acceptance_size = 200
26
+
27
+ [phases.forager]
28
+ type = "accepted_count"
29
+ limit = 4
30
+
31
+ [phases.move_selector]
32
+ type = "union_move_selector"
33
+
34
+ [[phases.move_selector.selectors]]
35
+ type = "nearby_list_change_move_selector"
36
+ max_nearby = 20
37
+ entity_class = "Vehicle"
38
+ variable_name = "delivery_order"
39
+
40
+ [[phases.move_selector.selectors]]
41
+ type = "nearby_list_swap_move_selector"
42
+ max_nearby = 20
43
+ entity_class = "Vehicle"
44
+ variable_name = "delivery_order"
45
+
46
+ [[phases.move_selector.selectors]]
47
+ type = "list_reverse_move_selector"
48
+ entity_class = "Vehicle"
49
+ variable_name = "delivery_order"
50
+
51
+ [[phases.move_selector.selectors]]
52
+ type = "k_opt_move_selector"
53
+ k = 3
54
+ min_segment_len = 1
55
+ max_nearby = 10
56
+ entity_class = "Vehicle"
57
+ variable_name = "delivery_order"
58
+
59
+ [[phases.move_selector.selectors]]
60
+ type = "list_ruin_move_selector"
61
+ min_ruin_count = 2
62
+ max_ruin_count = 5
63
+ moves_per_step = 10
64
+ entity_class = "Vehicle"
65
+ variable_name = "delivery_order"
66
+
67
+ [[phases.move_selector.selectors]]
68
+ type = "limited_neighborhood"
69
+ selected_count_limit = 500
70
+
71
+ [phases.move_selector.selectors.selector]
72
+ type = "sublist_change_move_selector"
73
+ min_sublist_size = 1
74
+ max_sublist_size = 3
75
+ entity_class = "Vehicle"
76
+ variable_name = "delivery_order"
solverforge.app.toml ADDED
@@ -0,0 +1,62 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ [app]
2
+ name = "solverforge-deliveries"
3
+ starter = "neutral-shell"
4
+ cli_version = "2.0.0"
5
+
6
+ [runtime]
7
+ target = "solverforge 0.9.1"
8
+ runtime_source = "crates.io: solverforge 0.9.1"
9
+ ui_source = "crates.io: solverforge-ui 0.6.3"
10
+ maps_source = "crates.io: solverforge-maps 2.1.3"
11
+
12
+ [demo]
13
+ default_size = "PHILADELPHIA"
14
+ available_sizes = [
15
+ "PHILADELPHIA",
16
+ "HARTFORD",
17
+ "FIRENZE",
18
+ ]
19
+
20
+ [solution]
21
+ name = "Plan"
22
+ score = "HardSoftScore"
23
+
24
+ [[facts]]
25
+ name = "delivery"
26
+ plural = "deliveries"
27
+ kind = "problem_fact"
28
+
29
+ [[entities]]
30
+ name = "vehicle"
31
+ plural = "vehicles"
32
+ kind = "planning_entity"
33
+
34
+ [[variables]]
35
+ entity = "vehicle"
36
+ entity_plural = "vehicles"
37
+ field = "delivery_order"
38
+ kind = "list"
39
+ range = ""
40
+ elements = "deliveries"
41
+ allows_unassigned = false
42
+ enabled = true
43
+
44
+ [[constraints]]
45
+ name = "all_deliveries_assigned"
46
+ module = "all_deliveries_assigned"
47
+ enabled = true
48
+
49
+ [[constraints]]
50
+ name = "delivery_time_windows"
51
+ module = "delivery_time_windows"
52
+ enabled = true
53
+
54
+ [[constraints]]
55
+ name = "total_travel_time"
56
+ module = "total_travel_time"
57
+ enabled = true
58
+
59
+ [[constraints]]
60
+ name = "vehicle_capacity"
61
+ module = "vehicle_capacity"
62
+ enabled = true
src/api/dto.rs ADDED
@@ -0,0 +1,230 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ use serde::{Deserialize, Serialize};
2
+ use serde_json::{Map, Value};
3
+ use solverforge::{HardSoftScore, SolverSnapshot, SolverSnapshotAnalysis, SolverStatus};
4
+
5
+ use crate::domain::{DeliveryInsertionCandidate, Plan, RoutesSnapshot};
6
+
7
+ mod runtime;
8
+
9
+ pub use runtime::{lifecycle_state_label, terminal_reason_label, TelemetryDto};
10
+
11
+ #[derive(Debug, Clone, Serialize, Deserialize)]
12
+ #[serde(rename_all = "camelCase")]
13
+ pub struct PlanDto {
14
+ #[serde(flatten)]
15
+ pub fields: Map<String, Value>,
16
+ #[serde(default)]
17
+ pub score: Option<String>,
18
+ }
19
+
20
+ #[derive(Debug, Clone, Serialize)]
21
+ #[serde(rename_all = "camelCase")]
22
+ pub struct ConstraintAnalysisDto {
23
+ pub name: String,
24
+ pub weight: String,
25
+ pub score: String,
26
+ pub match_count: usize,
27
+ }
28
+
29
+ #[derive(Debug, Clone, Serialize)]
30
+ #[serde(rename_all = "camelCase")]
31
+ pub struct AnalyzeResponse {
32
+ pub score: String,
33
+ pub constraints: Vec<ConstraintAnalysisDto>,
34
+ }
35
+
36
+ #[derive(Debug, Clone, Serialize)]
37
+ #[serde(rename_all = "camelCase")]
38
+ pub struct JobSummaryDto {
39
+ pub id: String,
40
+ pub job_id: String,
41
+ pub lifecycle_state: &'static str,
42
+ pub terminal_reason: Option<&'static str>,
43
+ pub checkpoint_available: bool,
44
+ pub event_sequence: u64,
45
+ pub snapshot_revision: Option<u64>,
46
+ pub current_score: Option<String>,
47
+ pub best_score: Option<String>,
48
+ pub telemetry: TelemetryDto,
49
+ }
50
+
51
+ #[derive(Debug, Clone, Serialize)]
52
+ #[serde(rename_all = "camelCase")]
53
+ pub struct JobSnapshotDto {
54
+ pub id: String,
55
+ pub job_id: String,
56
+ pub snapshot_revision: u64,
57
+ pub lifecycle_state: &'static str,
58
+ pub terminal_reason: Option<&'static str>,
59
+ pub current_score: Option<String>,
60
+ pub best_score: Option<String>,
61
+ pub telemetry: TelemetryDto,
62
+ pub solution: PlanDto,
63
+ }
64
+
65
+ #[derive(Debug, Clone, Serialize)]
66
+ #[serde(rename_all = "camelCase")]
67
+ pub struct JobAnalysisDto {
68
+ pub id: String,
69
+ pub job_id: String,
70
+ pub snapshot_revision: u64,
71
+ pub lifecycle_state: &'static str,
72
+ pub terminal_reason: Option<&'static str>,
73
+ pub analysis: AnalyzeResponse,
74
+ }
75
+
76
+ #[derive(Debug, Clone, Serialize)]
77
+ #[serde(rename_all = "camelCase")]
78
+ pub struct JobRoutesDto {
79
+ pub id: String,
80
+ pub job_id: String,
81
+ pub snapshot_revision: u64,
82
+ #[serde(flatten)]
83
+ pub routes: RoutesSnapshot,
84
+ }
85
+
86
+ #[derive(Debug, Clone, Deserialize)]
87
+ #[serde(rename_all = "camelCase")]
88
+ pub struct DeliveryInsertionRequestDto {
89
+ pub plan: PlanDto,
90
+ pub delivery_id: usize,
91
+ pub limit: Option<usize>,
92
+ }
93
+
94
+ #[derive(Debug, Clone, Serialize)]
95
+ #[serde(rename_all = "camelCase")]
96
+ pub struct DeliveryInsertionCandidateDto {
97
+ pub vehicle_id: usize,
98
+ pub vehicle_name: String,
99
+ pub insert_index: usize,
100
+ pub hard_score: i64,
101
+ pub soft_score: i64,
102
+ pub score: String,
103
+ pub delta_hard: i64,
104
+ pub delta_soft: i64,
105
+ pub preview_plan: PlanDto,
106
+ }
107
+
108
+ #[derive(Debug, Clone, Serialize)]
109
+ #[serde(rename_all = "camelCase")]
110
+ pub struct DeliveryInsertionResponseDto {
111
+ pub delivery_id: usize,
112
+ pub candidates: Vec<DeliveryInsertionCandidateDto>,
113
+ }
114
+
115
+ impl PlanDto {
116
+ pub fn from_plan(plan: &Plan) -> Self {
117
+ let plan = plan.refreshed_for_transport();
118
+ let score = plan.score.as_ref().map(ToString::to_string);
119
+ let mut fields = match serde_json::to_value(plan).expect("failed to serialize plan") {
120
+ Value::Object(map) => map,
121
+ _ => Map::new(),
122
+ };
123
+ fields.remove("score");
124
+
125
+ Self { fields, score }
126
+ }
127
+
128
+ pub fn to_domain(&self) -> Result<Plan, serde_json::Error> {
129
+ let mut fields = self.fields.clone();
130
+ fields.insert("score".to_string(), Value::Null);
131
+ let mut plan: Plan = serde_json::from_value(Value::Object(fields))?;
132
+ plan.normalize();
133
+ Ok(plan)
134
+ }
135
+ }
136
+
137
+ impl JobSummaryDto {
138
+ pub fn from_status(job_id: usize, status: &SolverStatus<HardSoftScore>) -> Self {
139
+ Self {
140
+ id: job_id.to_string(),
141
+ job_id: job_id.to_string(),
142
+ lifecycle_state: lifecycle_state_label(status.lifecycle_state),
143
+ terminal_reason: status.terminal_reason.map(terminal_reason_label),
144
+ checkpoint_available: status.checkpoint_available,
145
+ event_sequence: status.event_sequence,
146
+ snapshot_revision: status.latest_snapshot_revision,
147
+ current_score: status.current_score.map(|score| score.to_string()),
148
+ best_score: status.best_score.map(|score| score.to_string()),
149
+ telemetry: TelemetryDto::from_runtime(status.telemetry),
150
+ }
151
+ }
152
+ }
153
+
154
+ impl JobSnapshotDto {
155
+ pub fn from_snapshot(snapshot: &SolverSnapshot<Plan>) -> Self {
156
+ Self {
157
+ id: snapshot.job_id.to_string(),
158
+ job_id: snapshot.job_id.to_string(),
159
+ snapshot_revision: snapshot.snapshot_revision,
160
+ lifecycle_state: lifecycle_state_label(snapshot.lifecycle_state),
161
+ terminal_reason: snapshot.terminal_reason.map(terminal_reason_label),
162
+ current_score: snapshot.current_score.map(|score| score.to_string()),
163
+ best_score: snapshot.best_score.map(|score| score.to_string()),
164
+ telemetry: TelemetryDto::from_runtime(snapshot.telemetry),
165
+ solution: PlanDto::from_plan(&snapshot.solution),
166
+ }
167
+ }
168
+ }
169
+
170
+ impl JobAnalysisDto {
171
+ pub fn from_snapshot_analysis(
172
+ snapshot: &SolverSnapshotAnalysis<HardSoftScore>,
173
+ analysis: AnalyzeResponse,
174
+ ) -> Self {
175
+ Self {
176
+ id: snapshot.job_id.to_string(),
177
+ job_id: snapshot.job_id.to_string(),
178
+ snapshot_revision: snapshot.snapshot_revision,
179
+ lifecycle_state: lifecycle_state_label(snapshot.lifecycle_state),
180
+ terminal_reason: snapshot.terminal_reason.map(terminal_reason_label),
181
+ analysis,
182
+ }
183
+ }
184
+ }
185
+
186
+ impl JobRoutesDto {
187
+ pub fn new(job_id: usize, snapshot_revision: u64, routes: RoutesSnapshot) -> Self {
188
+ Self {
189
+ id: job_id.to_string(),
190
+ job_id: job_id.to_string(),
191
+ snapshot_revision,
192
+ routes,
193
+ }
194
+ }
195
+ }
196
+
197
+ impl DeliveryInsertionCandidateDto {
198
+ pub fn from_candidate(candidate: DeliveryInsertionCandidate) -> Self {
199
+ Self {
200
+ vehicle_id: candidate.vehicle_id,
201
+ vehicle_name: candidate.vehicle_name,
202
+ insert_index: candidate.insert_index,
203
+ hard_score: candidate.hard_score,
204
+ soft_score: candidate.soft_score,
205
+ score: HardSoftScore::of(candidate.hard_score, candidate.soft_score).to_string(),
206
+ delta_hard: candidate.delta_hard,
207
+ delta_soft: candidate.delta_soft,
208
+ preview_plan: PlanDto::from_plan(&candidate.preview_plan),
209
+ }
210
+ }
211
+ }
212
+
213
+ pub fn analysis_response(analysis: &solverforge::ScoreAnalysis<HardSoftScore>) -> AnalyzeResponse {
214
+ AnalyzeResponse {
215
+ score: analysis.score.to_string(),
216
+ constraints: analysis
217
+ .constraints
218
+ .iter()
219
+ .map(|constraint| ConstraintAnalysisDto {
220
+ name: constraint.name.clone(),
221
+ weight: constraint.weight.to_string(),
222
+ score: constraint.score.to_string(),
223
+ match_count: constraint.match_count,
224
+ })
225
+ .collect(),
226
+ }
227
+ }
228
+
229
+ #[cfg(test)]
230
+ mod tests;
src/api/dto/runtime.rs ADDED
@@ -0,0 +1,83 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ use serde::Serialize;
2
+ use solverforge::{SolverLifecycleState, SolverTelemetry, SolverTerminalReason};
3
+ use std::time::Duration;
4
+
5
+ #[derive(Debug, Clone, Copy, Serialize)]
6
+ #[serde(rename_all = "camelCase")]
7
+ pub struct TelemetryDto {
8
+ pub elapsed_ms: u64,
9
+ pub step_count: u64,
10
+ pub moves_generated: u64,
11
+ pub moves_evaluated: u64,
12
+ pub moves_accepted: u64,
13
+ pub score_calculations: u64,
14
+ pub generation_ms: u64,
15
+ pub evaluation_ms: u64,
16
+ pub moves_per_second: u64,
17
+ pub acceptance_rate: f64,
18
+ }
19
+
20
+ impl TelemetryDto {
21
+ pub fn from_runtime(telemetry: SolverTelemetry) -> Self {
22
+ Self {
23
+ elapsed_ms: duration_to_millis(telemetry.elapsed),
24
+ step_count: telemetry.step_count,
25
+ moves_generated: telemetry.moves_generated,
26
+ moves_evaluated: telemetry.moves_evaluated,
27
+ moves_accepted: telemetry.moves_accepted,
28
+ score_calculations: telemetry.score_calculations,
29
+ generation_ms: duration_to_millis(telemetry.generation_time),
30
+ evaluation_ms: duration_to_millis(telemetry.evaluation_time),
31
+ moves_per_second: whole_units_per_second(telemetry.moves_evaluated, telemetry.elapsed),
32
+ acceptance_rate: derive_acceptance_rate(
33
+ telemetry.moves_accepted,
34
+ telemetry.moves_evaluated,
35
+ ),
36
+ }
37
+ }
38
+ }
39
+
40
+ pub fn lifecycle_state_label(state: SolverLifecycleState) -> &'static str {
41
+ match state {
42
+ SolverLifecycleState::Solving => "SOLVING",
43
+ SolverLifecycleState::PauseRequested => "PAUSE_REQUESTED",
44
+ SolverLifecycleState::Paused => "PAUSED",
45
+ SolverLifecycleState::Completed => "COMPLETED",
46
+ SolverLifecycleState::Cancelled => "CANCELLED",
47
+ SolverLifecycleState::Failed => "FAILED",
48
+ }
49
+ }
50
+
51
+ pub fn terminal_reason_label(reason: SolverTerminalReason) -> &'static str {
52
+ match reason {
53
+ SolverTerminalReason::Completed => "completed",
54
+ SolverTerminalReason::TerminatedByConfig => "terminated_by_config",
55
+ SolverTerminalReason::Cancelled => "cancelled",
56
+ SolverTerminalReason::Failed => "failed",
57
+ }
58
+ }
59
+
60
+ fn duration_to_millis(duration: Duration) -> u64 {
61
+ duration.as_millis().min(u128::from(u64::MAX)) as u64
62
+ }
63
+
64
+ fn whole_units_per_second(count: u64, elapsed: Duration) -> u64 {
65
+ let nanos = elapsed.as_nanos();
66
+ if nanos == 0 {
67
+ 0
68
+ } else {
69
+ let per_second = u128::from(count)
70
+ .saturating_mul(1_000_000_000)
71
+ .checked_div(nanos)
72
+ .unwrap_or(0);
73
+ per_second.min(u128::from(u64::MAX)) as u64
74
+ }
75
+ }
76
+
77
+ fn derive_acceptance_rate(moves_accepted: u64, moves_evaluated: u64) -> f64 {
78
+ if moves_evaluated == 0 {
79
+ 0.0
80
+ } else {
81
+ moves_accepted as f64 / moves_evaluated as f64
82
+ }
83
+ }
src/api/dto/tests.rs ADDED
@@ -0,0 +1,38 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ use super::*;
2
+
3
+ #[test]
4
+ fn plan_dto_serializes_hard_soft_score_as_display_string() {
5
+ let mut plan = Plan::new("score check", Vec::new(), Vec::new());
6
+ plan.score = Some(HardSoftScore::of(0, -335));
7
+
8
+ let dto = PlanDto::from_plan(&plan);
9
+ assert_eq!(dto.score.as_deref(), Some("0hard/-335soft"));
10
+
11
+ let value = serde_json::to_value(&dto).expect("dto should serialize");
12
+ assert_eq!(value["score"], Value::String("0hard/-335soft".to_string()));
13
+ assert!(
14
+ !value["score"].is_object(),
15
+ "score must not serialize as a JSON object"
16
+ );
17
+ }
18
+
19
+ #[test]
20
+ fn plan_dto_ignores_inbound_score() {
21
+ let mut fields = Map::new();
22
+ fields.insert("name".to_string(), Value::String("spoofed".to_string()));
23
+ fields.insert(
24
+ "routingMode".to_string(),
25
+ Value::String("straight_line".to_string()),
26
+ );
27
+ fields.insert("viewState".to_string(), Value::Object(Map::new()));
28
+ fields.insert("deliveries".to_string(), Value::Array(Vec::new()));
29
+ fields.insert("vehicles".to_string(), Value::Array(Vec::new()));
30
+
31
+ let dto = PlanDto {
32
+ fields,
33
+ score: Some("0hard/-335soft".to_string()),
34
+ };
35
+ let plan = dto.to_domain().expect("dto should deserialize");
36
+
37
+ assert_eq!(plan.score, None);
38
+ }
src/api/errors.rs ADDED
@@ -0,0 +1,35 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ //! Shared HTTP error mapping for runtime and routing failures.
2
+ //!
3
+ //! Keeping this out of `routes.rs` lets the handler file read as a tutorial
4
+ //! walkthrough of the public API instead of as a collection of mapping helpers.
5
+
6
+ use axum::http::StatusCode;
7
+
8
+ /// Parses the path segment used by stock retained-job routes.
9
+ pub(super) fn parse_job_id(id: &str) -> Result<usize, StatusCode> {
10
+ id.parse::<usize>().map_err(|_| StatusCode::NOT_FOUND)
11
+ }
12
+
13
+ /// Maps retained-runtime errors onto HTTP statuses the stock UI understands.
14
+ pub(super) fn status_from_solver_error(error: solverforge::SolverManagerError) -> StatusCode {
15
+ match error {
16
+ solverforge::SolverManagerError::NoFreeJobSlots => StatusCode::SERVICE_UNAVAILABLE,
17
+ solverforge::SolverManagerError::JobNotFound { .. } => StatusCode::NOT_FOUND,
18
+ solverforge::SolverManagerError::InvalidStateTransition { .. } => StatusCode::CONFLICT,
19
+ solverforge::SolverManagerError::NoSnapshotAvailable { .. } => StatusCode::CONFLICT,
20
+ solverforge::SolverManagerError::SnapshotNotFound { .. } => StatusCode::NOT_FOUND,
21
+ }
22
+ }
23
+
24
+ /// Maps map/routing preparation errors onto client-facing route statuses.
25
+ pub(super) fn status_from_routing_error(error: solverforge_maps::RoutingError) -> StatusCode {
26
+ match error {
27
+ solverforge_maps::RoutingError::InvalidCoordinate { .. } => StatusCode::BAD_REQUEST,
28
+ solverforge_maps::RoutingError::Cancelled => StatusCode::REQUEST_TIMEOUT,
29
+ solverforge_maps::RoutingError::Network(_)
30
+ | solverforge_maps::RoutingError::Parse(_)
31
+ | solverforge_maps::RoutingError::Io(_)
32
+ | solverforge_maps::RoutingError::SnapFailed { .. }
33
+ | solverforge_maps::RoutingError::NoPath { .. } => StatusCode::BAD_GATEWAY,
34
+ }
35
+ }
src/api/mod.rs ADDED
@@ -0,0 +1,12 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ //! HTTP transport surface for the deliveries tutorial.
2
+ //!
3
+ //! The API layer stays intentionally thin: routes decode requests, DTOs define
4
+ //! the browser-visible JSON contract, and `SolverService` owns retained jobs.
5
+
6
+ mod dto;
7
+ mod errors;
8
+ mod routes;
9
+ mod sse;
10
+
11
+ pub use dto::PlanDto;
12
+ pub use routes::{router, AppState};
src/api/routes.rs ADDED
@@ -0,0 +1,288 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ //! HTTP routes for the deliveries tutorial app.
2
+ //!
3
+ //! Each handler follows the same beginner-friendly shape:
4
+ //! decode request -> prepare the domain model if needed -> call the retained
5
+ //! solver facade -> encode a DTO for the browser.
6
+
7
+ use axum::{
8
+ extract::{Path, Query, State},
9
+ http::StatusCode,
10
+ routing::{get, post},
11
+ Json, Router,
12
+ };
13
+ use serde::{Deserialize, Serialize};
14
+ use std::sync::Arc;
15
+
16
+ use super::dto::{
17
+ analysis_response, DeliveryInsertionCandidateDto, DeliveryInsertionRequestDto,
18
+ DeliveryInsertionResponseDto, JobAnalysisDto, JobRoutesDto, JobSnapshotDto, JobSummaryDto,
19
+ PlanDto,
20
+ };
21
+ use super::errors::{parse_job_id, status_from_routing_error, status_from_solver_error};
22
+ use super::sse;
23
+ use crate::data::{generate, DemoData};
24
+ use crate::domain::{build_routes_snapshot, prepare_plan, rank_delivery_insertions};
25
+ use crate::solver::SolverService;
26
+
27
+ /// Shared application state stored once inside Axum.
28
+ pub struct AppState {
29
+ pub solver: SolverService,
30
+ }
31
+
32
+ impl AppState {
33
+ pub fn new() -> Self {
34
+ Self {
35
+ solver: SolverService::new(),
36
+ }
37
+ }
38
+ }
39
+
40
+ impl Default for AppState {
41
+ fn default() -> Self {
42
+ Self::new()
43
+ }
44
+ }
45
+
46
+ /// Registers the public HTTP surface used by the browser and tests.
47
+ pub fn router(state: Arc<AppState>) -> Router {
48
+ Router::new()
49
+ .route("/health", get(health))
50
+ .route("/info", get(info))
51
+ .route("/demo-data", get(list_demo_data))
52
+ .route("/demo-data/{id}", get(get_demo_data))
53
+ .route("/jobs", post(create_job))
54
+ .route("/jobs/{id}", get(get_job).delete(delete_job))
55
+ .route("/jobs/{id}/status", get(get_job_status))
56
+ .route("/jobs/{id}/snapshot", get(get_snapshot))
57
+ .route("/jobs/{id}/analysis", get(analyze_by_id))
58
+ .route("/jobs/{id}/routes", get(get_routes))
59
+ .route("/jobs/{id}/pause", post(pause_job))
60
+ .route("/jobs/{id}/resume", post(resume_job))
61
+ .route("/jobs/{id}/cancel", post(cancel_job))
62
+ .route("/jobs/{id}/events", get(sse::events))
63
+ .route(
64
+ "/recommendations/delivery-insertions",
65
+ post(recommend_delivery_insertions),
66
+ )
67
+ .with_state(state)
68
+ }
69
+
70
+ #[derive(Serialize)]
71
+ struct HealthResponse {
72
+ status: &'static str,
73
+ }
74
+
75
+ async fn health() -> Json<HealthResponse> {
76
+ Json(HealthResponse { status: "UP" })
77
+ }
78
+
79
+ #[derive(Serialize)]
80
+ #[serde(rename_all = "camelCase")]
81
+ struct InfoResponse {
82
+ name: &'static str,
83
+ version: &'static str,
84
+ solver_engine: &'static str,
85
+ }
86
+
87
+ async fn info() -> Json<InfoResponse> {
88
+ Json(InfoResponse {
89
+ name: "SolverForge Deliveries",
90
+ version: env!("CARGO_PKG_VERSION"),
91
+ solver_engine: "SolverForge",
92
+ })
93
+ }
94
+
95
+ /// Lists the deterministic demo datasets accepted by `/demo-data/{id}`.
96
+ async fn list_demo_data() -> Json<Vec<&'static str>> {
97
+ Json(vec![
98
+ DemoData::Philadelphia.id(),
99
+ DemoData::Hartford.id(),
100
+ DemoData::Firenze.id(),
101
+ ])
102
+ }
103
+
104
+ /// Materializes one demo plan and sends it through the same DTO as snapshots.
105
+ async fn get_demo_data(Path(id): Path<String>) -> Result<Json<PlanDto>, StatusCode> {
106
+ let demo = id.parse::<DemoData>().map_err(|_| StatusCode::NOT_FOUND)?;
107
+ let plan = generate(demo);
108
+ Ok(Json(PlanDto::from_plan(&plan)))
109
+ }
110
+
111
+ #[derive(Serialize)]
112
+ #[serde(rename_all = "camelCase")]
113
+ struct CreateJobResponse {
114
+ id: String,
115
+ }
116
+
117
+ async fn create_job(
118
+ State(state): State<Arc<AppState>>,
119
+ Json(dto): Json<PlanDto>,
120
+ ) -> Result<Json<CreateJobResponse>, StatusCode> {
121
+ let mut plan = dto.to_domain().map_err(|_| StatusCode::BAD_REQUEST)?;
122
+ // Route matrices and shadow variables must be ready before SolverForge
123
+ // starts construction, because the list-variable hooks read them directly.
124
+ prepare_plan(&mut plan)
125
+ .await
126
+ .map_err(status_from_routing_error)?;
127
+ let id = state
128
+ .solver
129
+ .start_job(plan)
130
+ .map_err(status_from_solver_error)?;
131
+ Ok(Json(CreateJobResponse { id }))
132
+ }
133
+
134
+ /// Returns the retained-job summary without requiring a snapshot payload.
135
+ async fn get_job(
136
+ State(state): State<Arc<AppState>>,
137
+ Path(id): Path<String>,
138
+ ) -> Result<Json<JobSummaryDto>, StatusCode> {
139
+ let job_id = parse_job_id(&id)?;
140
+ let status = state
141
+ .solver
142
+ .get_status(&id)
143
+ .map_err(status_from_solver_error)?;
144
+ Ok(Json(JobSummaryDto::from_status(job_id, &status)))
145
+ }
146
+
147
+ /// Stock alias used by the shared SolverForge UI job-status helpers.
148
+ async fn get_job_status(
149
+ State(state): State<Arc<AppState>>,
150
+ Path(id): Path<String>,
151
+ ) -> Result<Json<JobSummaryDto>, StatusCode> {
152
+ get_job(State(state), Path(id)).await
153
+ }
154
+
155
+ #[derive(Debug, Default, Deserialize)]
156
+ struct SnapshotQuery {
157
+ snapshot_revision: Option<u64>,
158
+ }
159
+
160
+ async fn get_snapshot(
161
+ State(state): State<Arc<AppState>>,
162
+ Path(id): Path<String>,
163
+ Query(query): Query<SnapshotQuery>,
164
+ ) -> Result<Json<JobSnapshotDto>, StatusCode> {
165
+ let snapshot = state
166
+ .solver
167
+ .get_snapshot(&id, query.snapshot_revision)
168
+ .map_err(status_from_solver_error)?;
169
+ Ok(Json(JobSnapshotDto::from_snapshot(&snapshot)))
170
+ }
171
+
172
+ /// Runs exact score analysis against a retained snapshot revision.
173
+ async fn analyze_by_id(
174
+ State(state): State<Arc<AppState>>,
175
+ Path(id): Path<String>,
176
+ Query(query): Query<SnapshotQuery>,
177
+ ) -> Result<Json<JobAnalysisDto>, StatusCode> {
178
+ let snapshot_analysis = state
179
+ .solver
180
+ .analyze_snapshot(&id, query.snapshot_revision)
181
+ .map_err(status_from_solver_error)?;
182
+ let analysis = analysis_response(&snapshot_analysis.analysis);
183
+ Ok(Json(JobAnalysisDto::from_snapshot_analysis(
184
+ &snapshot_analysis,
185
+ analysis,
186
+ )))
187
+ }
188
+
189
+ /// Builds route geometry for the exact retained snapshot the browser is viewing.
190
+ async fn get_routes(
191
+ State(state): State<Arc<AppState>>,
192
+ Path(id): Path<String>,
193
+ Query(query): Query<SnapshotQuery>,
194
+ ) -> Result<Json<JobRoutesDto>, StatusCode> {
195
+ let job_id = parse_job_id(&id)?;
196
+ let mut snapshot = state
197
+ .solver
198
+ .get_snapshot(&id, query.snapshot_revision)
199
+ .map_err(status_from_solver_error)?;
200
+ if snapshot
201
+ .solution
202
+ .vehicles
203
+ .iter()
204
+ .any(|vehicle| vehicle.prepared_routing.is_none())
205
+ {
206
+ // Older snapshots can be reconstructed from transport data. If the
207
+ // transient routing cache is absent, rebuild it before drawing routes.
208
+ prepare_plan(&mut snapshot.solution)
209
+ .await
210
+ .map_err(status_from_routing_error)?;
211
+ }
212
+ let routes = build_routes_snapshot(&snapshot.solution)
213
+ .await
214
+ .map_err(status_from_routing_error)?;
215
+ Ok(Json(JobRoutesDto::new(
216
+ job_id,
217
+ snapshot.snapshot_revision,
218
+ routes,
219
+ )))
220
+ }
221
+
222
+ /// Requests a runtime-managed pause at the next safe solver point.
223
+ async fn pause_job(
224
+ State(state): State<Arc<AppState>>,
225
+ Path(id): Path<String>,
226
+ ) -> Result<StatusCode, StatusCode> {
227
+ state.solver.pause(&id).map_err(status_from_solver_error)?;
228
+ Ok(StatusCode::ACCEPTED)
229
+ }
230
+
231
+ /// Resumes a paused retained job.
232
+ async fn resume_job(
233
+ State(state): State<Arc<AppState>>,
234
+ Path(id): Path<String>,
235
+ ) -> Result<StatusCode, StatusCode> {
236
+ state.solver.resume(&id).map_err(status_from_solver_error)?;
237
+ Ok(StatusCode::ACCEPTED)
238
+ }
239
+
240
+ /// Cancels a live or paused retained job without deleting its final snapshot.
241
+ async fn cancel_job(
242
+ State(state): State<Arc<AppState>>,
243
+ Path(id): Path<String>,
244
+ ) -> Result<StatusCode, StatusCode> {
245
+ state.solver.cancel(&id).map_err(status_from_solver_error)?;
246
+ Ok(StatusCode::ACCEPTED)
247
+ }
248
+
249
+ /// Deletes a terminal retained job and its cached SSE bootstrap state.
250
+ async fn delete_job(
251
+ State(state): State<Arc<AppState>>,
252
+ Path(id): Path<String>,
253
+ ) -> Result<StatusCode, StatusCode> {
254
+ state.solver.delete(&id).map_err(status_from_solver_error)?;
255
+ Ok(StatusCode::NO_CONTENT)
256
+ }
257
+
258
+ /// Ranks candidate vehicle/position insertions for one delivery.
259
+ async fn recommend_delivery_insertions(
260
+ Json(request): Json<DeliveryInsertionRequestDto>,
261
+ ) -> Result<Json<DeliveryInsertionResponseDto>, StatusCode> {
262
+ let mut plan = request
263
+ .plan
264
+ .to_domain()
265
+ .map_err(|_| StatusCode::BAD_REQUEST)?;
266
+ if request.delivery_id >= plan.deliveries.len() {
267
+ return Err(StatusCode::BAD_REQUEST);
268
+ }
269
+ // Candidate scoring uses the same prepared data as real solving so the
270
+ // modal preview matches the constraints and route metrics.
271
+ prepare_plan(&mut plan)
272
+ .await
273
+ .map_err(status_from_routing_error)?;
274
+ let candidates = rank_delivery_insertions(
275
+ &plan,
276
+ request.delivery_id,
277
+ request.limit.unwrap_or(8).min(24),
278
+ )
279
+ .await
280
+ .map_err(status_from_routing_error)?
281
+ .into_iter()
282
+ .map(DeliveryInsertionCandidateDto::from_candidate)
283
+ .collect();
284
+ Ok(Json(DeliveryInsertionResponseDto {
285
+ delivery_id: request.delivery_id,
286
+ candidates,
287
+ }))
288
+ }
src/api/sse.rs ADDED
@@ -0,0 +1,41 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ use axum::{
2
+ body::Body,
3
+ extract::{Path, State},
4
+ http::{header, StatusCode},
5
+ response::Response,
6
+ };
7
+ use std::sync::Arc;
8
+ use tokio_stream::wrappers::BroadcastStream;
9
+ use tokio_stream::StreamExt;
10
+
11
+ use super::routes::AppState;
12
+
13
+ pub async fn events(
14
+ State(state): State<Arc<AppState>>,
15
+ Path(id): Path<String>,
16
+ ) -> Result<Response<Body>, StatusCode> {
17
+ let rx = state.solver.subscribe(&id).ok_or(StatusCode::NOT_FOUND)?;
18
+ let bootstrap_json = state
19
+ .solver
20
+ .bootstrap_event(&id)
21
+ .map_err(|_| StatusCode::NOT_FOUND)?;
22
+ let bootstrap = tokio_stream::iter(std::iter::once(Ok::<_, std::convert::Infallible>(
23
+ format!("data: {}\n\n", bootstrap_json).into_bytes(),
24
+ )));
25
+
26
+ let live = BroadcastStream::new(rx).filter_map(|msg| match msg {
27
+ Ok(json) => Some(Ok::<_, std::convert::Infallible>(
28
+ format!("data: {}\n\n", json).into_bytes(),
29
+ )),
30
+ Err(_) => None, // Lagged — skip missed messages
31
+ });
32
+
33
+ let stream = bootstrap.chain(live);
34
+
35
+ Ok(Response::builder()
36
+ .header(header::CONTENT_TYPE, "text/event-stream")
37
+ .header(header::CACHE_CONTROL, "no-cache")
38
+ .header("X-Accel-Buffering", "no")
39
+ .body(Body::from_stream(stream))
40
+ .unwrap())
41
+ }
src/constraints/all_deliveries_assigned.rs ADDED
@@ -0,0 +1,26 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ use crate::domain::{
2
+ Delivery, Plan, PlanConstraintStreams, Vehicle, UNASSIGNED_DELIVERY_HARD_PENALTY,
3
+ };
4
+ use solverforge::prelude::*;
5
+ use solverforge::stream::joiner::equal_bi;
6
+ use solverforge::IncrementalConstraint;
7
+
8
+ /// HARD: every delivery must appear in some vehicle route.
9
+ pub fn constraint() -> impl IncrementalConstraint<Plan, HardSoftScore> {
10
+ ConstraintFactory::<Plan, HardSoftScore>::new()
11
+ .deliveries()
12
+ // The right side flattens every vehicle route into assigned delivery
13
+ // ids. A delivery that does not exist in that flattened stream is
14
+ // unassigned and receives the dominant hard penalty.
15
+ .if_not_exists((
16
+ ConstraintFactory::<Plan, HardSoftScore>::new()
17
+ .vehicles()
18
+ .flattened(|vehicle: &Vehicle| &vehicle.delivery_order),
19
+ equal_bi(
20
+ |delivery: &Delivery| delivery.id,
21
+ |assigned: &usize| *assigned,
22
+ ),
23
+ ))
24
+ .penalize_hard_with(|_: &Delivery| HardSoftScore::of(UNASSIGNED_DELIVERY_HARD_PENALTY, 0))
25
+ .named("All Deliveries Assigned")
26
+ }
src/constraints/delivery_time_windows.rs ADDED
@@ -0,0 +1,16 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ use crate::domain::{Plan, PlanConstraintStreams, Vehicle};
2
+ use solverforge::prelude::*;
3
+ use solverforge::IncrementalConstraint;
4
+
5
+ /// HARD: each vehicle route must respect delivery time windows.
6
+ pub fn constraint() -> impl IncrementalConstraint<Plan, HardSoftScore> {
7
+ ConstraintFactory::<Plan, HardSoftScore>::new()
8
+ .vehicles()
9
+ // Time-window work is precomputed as a vehicle shadow value, so this
10
+ // rule can stay incremental and read one scalar per changed route.
11
+ .filter(|vehicle: &Vehicle| vehicle.time_window_violation_seconds() > 0)
12
+ .penalize_hard_with(|vehicle: &Vehicle| {
13
+ HardSoftScore::of(vehicle.time_window_violation_seconds(), 0)
14
+ })
15
+ .named("Delivery Time Windows")
16
+ }
src/constraints/mod.rs ADDED
@@ -0,0 +1,32 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ //! Constraint assembly for delivery routing.
2
+ //!
3
+ //! Each sibling file contributes one named rule. `create_constraints()` lists
4
+ //! them in the order we want beginners to see in score analysis output.
5
+
6
+ use crate::domain::Plan;
7
+ use solverforge::prelude::*;
8
+
9
+ pub use self::assemble::create_constraints;
10
+
11
+ // @solverforge:begin constraint-modules
12
+ mod all_deliveries_assigned;
13
+ mod delivery_time_windows;
14
+ mod total_travel_time;
15
+ mod vehicle_capacity;
16
+ // @solverforge:end constraint-modules
17
+
18
+ mod assemble {
19
+ use super::*;
20
+
21
+ /// Collects the full scoring model used by `Plan`.
22
+ pub fn create_constraints() -> impl ConstraintSet<Plan, HardSoftScore> {
23
+ // @solverforge:begin constraint-calls
24
+ (
25
+ all_deliveries_assigned::constraint(),
26
+ vehicle_capacity::constraint(),
27
+ delivery_time_windows::constraint(),
28
+ total_travel_time::constraint(),
29
+ )
30
+ // @solverforge:end constraint-calls
31
+ }
32
+ }
src/constraints/total_travel_time.rs ADDED
@@ -0,0 +1,11 @@
 
 
 
 
 
 
 
 
 
 
 
 
1
+ use crate::domain::{Plan, PlanConstraintStreams, Vehicle};
2
+ use solverforge::prelude::*;
3
+ use solverforge::IncrementalConstraint;
4
+
5
+ /// SOFT: prefer less total travel time across all routes.
6
+ pub fn constraint() -> impl IncrementalConstraint<Plan, HardSoftScore> {
7
+ ConstraintFactory::<Plan, HardSoftScore>::new()
8
+ .vehicles()
9
+ .penalize_with(|vehicle: &Vehicle| HardSoftScore::of(0, vehicle.total_travel_seconds()))
10
+ .named("Total Travel Time")
11
+ }
src/constraints/vehicle_capacity.rs ADDED
@@ -0,0 +1,14 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ use crate::domain::{Plan, PlanConstraintStreams, Vehicle};
2
+ use solverforge::prelude::*;
3
+ use solverforge::IncrementalConstraint;
4
+
5
+ /// HARD: a vehicle's assigned demand cannot exceed its capacity.
6
+ pub fn constraint() -> impl IncrementalConstraint<Plan, HardSoftScore> {
7
+ ConstraintFactory::<Plan, HardSoftScore>::new()
8
+ .vehicles()
9
+ // Capacity overage is also a route shadow value. SolverForge updates it
10
+ // after list moves, and this constraint only scores positive excess.
11
+ .filter(|vehicle: &Vehicle| vehicle.capacity_overage() > 0)
12
+ .penalize_hard_with(|vehicle: &Vehicle| HardSoftScore::of(vehicle.capacity_overage(), 0))
13
+ .named("Vehicle Capacity")
14
+ }
src/data/data_seed.rs ADDED
@@ -0,0 +1,10 @@
 
 
 
 
 
 
 
 
 
 
 
1
+ mod entrypoints;
2
+ mod firenze;
3
+ mod hartford;
4
+ mod philadelphia;
5
+ mod types;
6
+
7
+ pub use entrypoints::{generate, DemoData};
8
+
9
+ #[cfg(test)]
10
+ mod tests;
src/data/data_seed/entrypoints.rs ADDED
@@ -0,0 +1,127 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ use std::str::FromStr;
2
+
3
+ use rand::rngs::StdRng;
4
+ use rand::{RngExt, SeedableRng};
5
+
6
+ use super::types::{LocationData, VEHICLE_NAMES};
7
+ use super::{firenze, hartford, philadelphia};
8
+ use crate::domain::{Delivery, Plan, RoutingMode, Vehicle};
9
+
10
+ #[derive(Debug, Clone, Copy)]
11
+ pub enum DemoData {
12
+ Philadelphia,
13
+ Hartford,
14
+ Firenze,
15
+ }
16
+
17
+ impl DemoData {
18
+ pub fn id(self) -> &'static str {
19
+ match self {
20
+ DemoData::Philadelphia => "PHILADELPHIA",
21
+ DemoData::Hartford => "HARTFORD",
22
+ DemoData::Firenze => "FIRENZE",
23
+ }
24
+ }
25
+
26
+ pub fn label(self) -> &'static str {
27
+ match self {
28
+ DemoData::Philadelphia => "Philadelphia",
29
+ DemoData::Hartford => "Hartford",
30
+ DemoData::Firenze => "Firenze",
31
+ }
32
+ }
33
+ }
34
+
35
+ impl FromStr for DemoData {
36
+ type Err = ();
37
+
38
+ fn from_str(s: &str) -> Result<Self, Self::Err> {
39
+ match s.trim().to_uppercase().as_str() {
40
+ "PHILADELPHIA" => Ok(DemoData::Philadelphia),
41
+ "HARTFORD" => Ok(DemoData::Hartford),
42
+ "FIRENZE" => Ok(DemoData::Firenze),
43
+ _ => Err(()),
44
+ }
45
+ }
46
+ }
47
+
48
+ pub fn generate(demo: DemoData) -> Plan {
49
+ match demo {
50
+ DemoData::Philadelphia => generate_demo_data(
51
+ demo,
52
+ 0,
53
+ philadelphia::DEPOTS,
54
+ philadelphia::VISIT_GROUPS,
55
+ 6 * 3600,
56
+ 36,
57
+ 48,
58
+ ),
59
+ DemoData::Hartford => generate_demo_data(
60
+ demo,
61
+ 1,
62
+ hartford::DEPOTS,
63
+ hartford::VISIT_GROUPS,
64
+ 6 * 3600,
65
+ 24,
66
+ 34,
67
+ ),
68
+ DemoData::Firenze => generate_demo_data(
69
+ demo,
70
+ 2,
71
+ firenze::DEPOTS,
72
+ firenze::VISIT_GROUPS,
73
+ 6 * 3600,
74
+ 38,
75
+ 52,
76
+ ),
77
+ }
78
+ }
79
+
80
+ fn generate_demo_data(
81
+ demo: DemoData,
82
+ seed: u64,
83
+ depots: &[LocationData],
84
+ stop_groups: &[&[LocationData]],
85
+ departure_time: i64,
86
+ min_capacity: i32,
87
+ max_capacity: i32,
88
+ ) -> Plan {
89
+ let mut rng = StdRng::seed_from_u64(seed);
90
+ let vehicles = depots
91
+ .iter()
92
+ .enumerate()
93
+ .map(|(idx, depot)| {
94
+ Vehicle::new(
95
+ idx,
96
+ VEHICLE_NAMES[idx % VEHICLE_NAMES.len()],
97
+ rng.random_range(min_capacity..=max_capacity),
98
+ depot.lat,
99
+ depot.lng,
100
+ departure_time,
101
+ )
102
+ })
103
+ .collect::<Vec<_>>();
104
+
105
+ let deliveries = stop_groups
106
+ .iter()
107
+ .flat_map(|stops| stops.iter())
108
+ .enumerate()
109
+ .map(|(idx, location)| {
110
+ let (kind, min_start_time, max_end_time, demand_range, service_range) =
111
+ location.customer_type.profile();
112
+ Delivery::new(
113
+ idx,
114
+ location.name,
115
+ kind,
116
+ (location.lat, location.lng),
117
+ rng.random_range(demand_range.0..=demand_range.1),
118
+ (min_start_time, max_end_time),
119
+ rng.random_range(service_range.0..=service_range.1),
120
+ )
121
+ })
122
+ .collect::<Vec<_>>();
123
+
124
+ let mut plan = Plan::new(demo.label(), deliveries, vehicles);
125
+ plan.routing_mode = RoutingMode::RoadNetwork;
126
+ plan
127
+ }
src/data/data_seed/firenze.rs ADDED
@@ -0,0 +1,8 @@
 
 
 
 
 
 
 
 
 
1
+ use super::types::LocationData;
2
+
3
+ mod depots;
4
+ mod visits;
5
+ mod visits_extra;
6
+
7
+ pub(super) use depots::DEPOTS;
8
+ pub(super) const VISIT_GROUPS: &[&[LocationData]] = &[visits::VISITS, visits_extra::VISITS];
src/data/data_seed/firenze/depots.rs ADDED
@@ -0,0 +1,64 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ use super::super::types::{CustomerType, LocationData};
2
+
3
+ pub(in crate::data::data_seed) const DEPOTS: &[LocationData] = &[
4
+ LocationData {
5
+ name: "Centro Storico Depot",
6
+ lat: 43.7696,
7
+ lng: 11.2558,
8
+ customer_type: CustomerType::Business,
9
+ },
10
+ LocationData {
11
+ name: "Santa Maria Novella Depot",
12
+ lat: 43.7745,
13
+ lng: 11.2487,
14
+ customer_type: CustomerType::Business,
15
+ },
16
+ LocationData {
17
+ name: "Campo di Marte Depot",
18
+ lat: 43.7820,
19
+ lng: 11.2820,
20
+ customer_type: CustomerType::Business,
21
+ },
22
+ LocationData {
23
+ name: "Rifredi Depot",
24
+ lat: 43.7950,
25
+ lng: 11.2410,
26
+ customer_type: CustomerType::Business,
27
+ },
28
+ LocationData {
29
+ name: "Novoli Depot",
30
+ lat: 43.7880,
31
+ lng: 11.2220,
32
+ customer_type: CustomerType::Business,
33
+ },
34
+ LocationData {
35
+ name: "Gavinana Depot",
36
+ lat: 43.7520,
37
+ lng: 11.2680,
38
+ customer_type: CustomerType::Business,
39
+ },
40
+ LocationData {
41
+ name: "Mercato Centrale Depot",
42
+ lat: 43.7762,
43
+ lng: 11.2540,
44
+ customer_type: CustomerType::Business,
45
+ },
46
+ LocationData {
47
+ name: "Santa Croce Depot",
48
+ lat: 43.7688,
49
+ lng: 11.2620,
50
+ customer_type: CustomerType::Business,
51
+ },
52
+ LocationData {
53
+ name: "Santo Spirito Depot",
54
+ lat: 43.7665,
55
+ lng: 11.2470,
56
+ customer_type: CustomerType::Business,
57
+ },
58
+ LocationData {
59
+ name: "Careggi Depot",
60
+ lat: 43.8020,
61
+ lng: 11.2530,
62
+ customer_type: CustomerType::Business,
63
+ },
64
+ ];
src/data/data_seed/firenze/visits.rs ADDED
@@ -0,0 +1,292 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ use super::super::types::{CustomerType, LocationData};
2
+
3
+ pub(in crate::data::data_seed) const VISITS: &[LocationData] = &[
4
+ LocationData {
5
+ name: "Trattoria Mario",
6
+ lat: 43.7762,
7
+ lng: 11.2540,
8
+ customer_type: CustomerType::Restaurant,
9
+ },
10
+ LocationData {
11
+ name: "Buca Mario",
12
+ lat: 43.7698,
13
+ lng: 11.2505,
14
+ customer_type: CustomerType::Restaurant,
15
+ },
16
+ LocationData {
17
+ name: "Il Latini",
18
+ lat: 43.7705,
19
+ lng: 11.2495,
20
+ customer_type: CustomerType::Restaurant,
21
+ },
22
+ LocationData {
23
+ name: "Osteria dell'Enoteca",
24
+ lat: 43.7680,
25
+ lng: 11.2545,
26
+ customer_type: CustomerType::Restaurant,
27
+ },
28
+ LocationData {
29
+ name: "Trattoria Sostanza",
30
+ lat: 43.7735,
31
+ lng: 11.2470,
32
+ customer_type: CustomerType::Restaurant,
33
+ },
34
+ LocationData {
35
+ name: "All'Antico Vinaio",
36
+ lat: 43.7690,
37
+ lng: 11.2570,
38
+ customer_type: CustomerType::Restaurant,
39
+ },
40
+ LocationData {
41
+ name: "Mercato Centrale",
42
+ lat: 43.7762,
43
+ lng: 11.2540,
44
+ customer_type: CustomerType::Restaurant,
45
+ },
46
+ LocationData {
47
+ name: "Cibreo",
48
+ lat: 43.7702,
49
+ lng: 11.2670,
50
+ customer_type: CustomerType::Restaurant,
51
+ },
52
+ LocationData {
53
+ name: "Ora d'Aria",
54
+ lat: 43.7710,
55
+ lng: 11.2610,
56
+ customer_type: CustomerType::Restaurant,
57
+ },
58
+ LocationData {
59
+ name: "Buca Lapi",
60
+ lat: 43.7720,
61
+ lng: 11.2535,
62
+ customer_type: CustomerType::Restaurant,
63
+ },
64
+ LocationData {
65
+ name: "Il Palagio",
66
+ lat: 43.7680,
67
+ lng: 11.2550,
68
+ customer_type: CustomerType::Restaurant,
69
+ },
70
+ LocationData {
71
+ name: "Enoteca Pinchiorri",
72
+ lat: 43.7695,
73
+ lng: 11.2620,
74
+ customer_type: CustomerType::Restaurant,
75
+ },
76
+ LocationData {
77
+ name: "La Giostra",
78
+ lat: 43.7745,
79
+ lng: 11.2650,
80
+ customer_type: CustomerType::Restaurant,
81
+ },
82
+ LocationData {
83
+ name: "Fishing Lab",
84
+ lat: 43.7693,
85
+ lng: 11.2563,
86
+ customer_type: CustomerType::Restaurant,
87
+ },
88
+ LocationData {
89
+ name: "Trattoria Cammillo",
90
+ lat: 43.7665,
91
+ lng: 11.2520,
92
+ customer_type: CustomerType::Restaurant,
93
+ },
94
+ LocationData {
95
+ name: "Palazzo Vecchio",
96
+ lat: 43.7693,
97
+ lng: 11.2563,
98
+ customer_type: CustomerType::Business,
99
+ },
100
+ LocationData {
101
+ name: "Uffizi Gallery",
102
+ lat: 43.7677,
103
+ lng: 11.2553,
104
+ customer_type: CustomerType::Business,
105
+ },
106
+ LocationData {
107
+ name: "Gucci Garden",
108
+ lat: 43.7692,
109
+ lng: 11.2556,
110
+ customer_type: CustomerType::Business,
111
+ },
112
+ LocationData {
113
+ name: "Ferragamo Museum",
114
+ lat: 43.7700,
115
+ lng: 11.2530,
116
+ customer_type: CustomerType::Business,
117
+ },
118
+ LocationData {
119
+ name: "Ospedale Santa Maria",
120
+ lat: 43.7830,
121
+ lng: 11.2690,
122
+ customer_type: CustomerType::Business,
123
+ },
124
+ LocationData {
125
+ name: "Universita degli Studi",
126
+ lat: 43.7765,
127
+ lng: 11.2555,
128
+ customer_type: CustomerType::Business,
129
+ },
130
+ LocationData {
131
+ name: "Palazzo Strozzi",
132
+ lat: 43.7706,
133
+ lng: 11.2515,
134
+ customer_type: CustomerType::Business,
135
+ },
136
+ LocationData {
137
+ name: "Biblioteca Nazionale",
138
+ lat: 43.7660,
139
+ lng: 11.2650,
140
+ customer_type: CustomerType::Business,
141
+ },
142
+ LocationData {
143
+ name: "Teatro del Maggio",
144
+ lat: 43.7780,
145
+ lng: 11.2470,
146
+ customer_type: CustomerType::Business,
147
+ },
148
+ LocationData {
149
+ name: "Palazzo Pitti",
150
+ lat: 43.7665,
151
+ lng: 11.2470,
152
+ customer_type: CustomerType::Business,
153
+ },
154
+ LocationData {
155
+ name: "Accademia Gallery",
156
+ lat: 43.7768,
157
+ lng: 11.2590,
158
+ customer_type: CustomerType::Business,
159
+ },
160
+ LocationData {
161
+ name: "Ospedale Meyer",
162
+ lat: 43.7910,
163
+ lng: 11.2520,
164
+ customer_type: CustomerType::Business,
165
+ },
166
+ LocationData {
167
+ name: "Polo Universitario",
168
+ lat: 43.7920,
169
+ lng: 11.2180,
170
+ customer_type: CustomerType::Business,
171
+ },
172
+ LocationData {
173
+ name: "Santo Spirito",
174
+ lat: 43.7665,
175
+ lng: 11.2470,
176
+ customer_type: CustomerType::Residential,
177
+ },
178
+ LocationData {
179
+ name: "San Frediano",
180
+ lat: 43.7680,
181
+ lng: 11.2420,
182
+ customer_type: CustomerType::Residential,
183
+ },
184
+ LocationData {
185
+ name: "Santa Croce",
186
+ lat: 43.7688,
187
+ lng: 11.2620,
188
+ customer_type: CustomerType::Residential,
189
+ },
190
+ LocationData {
191
+ name: "San Lorenzo",
192
+ lat: 43.7755,
193
+ lng: 11.2540,
194
+ customer_type: CustomerType::Residential,
195
+ },
196
+ LocationData {
197
+ name: "San Marco",
198
+ lat: 43.7768,
199
+ lng: 11.2590,
200
+ customer_type: CustomerType::Residential,
201
+ },
202
+ LocationData {
203
+ name: "Sant'Ambrogio",
204
+ lat: 43.7688,
205
+ lng: 11.2620,
206
+ customer_type: CustomerType::Residential,
207
+ },
208
+ LocationData {
209
+ name: "Campo di Marte",
210
+ lat: 43.7820,
211
+ lng: 11.2820,
212
+ customer_type: CustomerType::Residential,
213
+ },
214
+ LocationData {
215
+ name: "Novoli",
216
+ lat: 43.7880,
217
+ lng: 11.2220,
218
+ customer_type: CustomerType::Residential,
219
+ },
220
+ LocationData {
221
+ name: "Rifredi",
222
+ lat: 43.7950,
223
+ lng: 11.2410,
224
+ customer_type: CustomerType::Residential,
225
+ },
226
+ LocationData {
227
+ name: "Le Cure",
228
+ lat: 43.7890,
229
+ lng: 11.2580,
230
+ customer_type: CustomerType::Residential,
231
+ },
232
+ LocationData {
233
+ name: "Careggi",
234
+ lat: 43.8020,
235
+ lng: 11.2530,
236
+ customer_type: CustomerType::Residential,
237
+ },
238
+ LocationData {
239
+ name: "Peretola",
240
+ lat: 43.7960,
241
+ lng: 11.2050,
242
+ customer_type: CustomerType::Residential,
243
+ },
244
+ LocationData {
245
+ name: "Isolotto",
246
+ lat: 43.7620,
247
+ lng: 11.2200,
248
+ customer_type: CustomerType::Residential,
249
+ },
250
+ LocationData {
251
+ name: "Gavinana",
252
+ lat: 43.7520,
253
+ lng: 11.2680,
254
+ customer_type: CustomerType::Residential,
255
+ },
256
+ LocationData {
257
+ name: "Galluzzo",
258
+ lat: 43.7400,
259
+ lng: 11.2480,
260
+ customer_type: CustomerType::Residential,
261
+ },
262
+ LocationData {
263
+ name: "Porta Romana",
264
+ lat: 43.7610,
265
+ lng: 11.2560,
266
+ customer_type: CustomerType::Residential,
267
+ },
268
+ LocationData {
269
+ name: "Bellosguardo",
270
+ lat: 43.7650,
271
+ lng: 11.2350,
272
+ customer_type: CustomerType::Residential,
273
+ },
274
+ LocationData {
275
+ name: "Arcetri",
276
+ lat: 43.7500,
277
+ lng: 11.2530,
278
+ customer_type: CustomerType::Residential,
279
+ },
280
+ LocationData {
281
+ name: "Fiesole",
282
+ lat: 43.8055,
283
+ lng: 11.2935,
284
+ customer_type: CustomerType::Residential,
285
+ },
286
+ LocationData {
287
+ name: "Settignano",
288
+ lat: 43.7850,
289
+ lng: 11.3100,
290
+ customer_type: CustomerType::Residential,
291
+ },
292
+ ];
src/data/data_seed/firenze/visits_extra.rs ADDED
@@ -0,0 +1,196 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ use super::super::types::{CustomerType, LocationData};
2
+
3
+ pub(in crate::data::data_seed) const VISITS: &[LocationData] = &[
4
+ LocationData {
5
+ name: "Mercato di Sant'Ambrogio",
6
+ lat: 43.7688,
7
+ lng: 11.2620,
8
+ customer_type: CustomerType::Restaurant,
9
+ },
10
+ LocationData {
11
+ name: "Procacci",
12
+ lat: 43.7706,
13
+ lng: 11.2515,
14
+ customer_type: CustomerType::Restaurant,
15
+ },
16
+ LocationData {
17
+ name: "La Menagere",
18
+ lat: 43.7755,
19
+ lng: 11.2540,
20
+ customer_type: CustomerType::Restaurant,
21
+ },
22
+ LocationData {
23
+ name: "Rivoire",
24
+ lat: 43.7693,
25
+ lng: 11.2563,
26
+ customer_type: CustomerType::Restaurant,
27
+ },
28
+ LocationData {
29
+ name: "Gelateria dei Neri",
30
+ lat: 43.7688,
31
+ lng: 11.2620,
32
+ customer_type: CustomerType::Restaurant,
33
+ },
34
+ LocationData {
35
+ name: "Trattoria Za Za",
36
+ lat: 43.7762,
37
+ lng: 11.2540,
38
+ customer_type: CustomerType::Restaurant,
39
+ },
40
+ LocationData {
41
+ name: "Il Santo Bevitore",
42
+ lat: 43.7680,
43
+ lng: 11.2420,
44
+ customer_type: CustomerType::Restaurant,
45
+ },
46
+ LocationData {
47
+ name: "Da Ruggero",
48
+ lat: 43.7610,
49
+ lng: 11.2560,
50
+ customer_type: CustomerType::Restaurant,
51
+ },
52
+ LocationData {
53
+ name: "Perseus",
54
+ lat: 43.7890,
55
+ lng: 11.2580,
56
+ customer_type: CustomerType::Restaurant,
57
+ },
58
+ LocationData {
59
+ name: "Coquinarius",
60
+ lat: 43.7720,
61
+ lng: 11.2535,
62
+ customer_type: CustomerType::Restaurant,
63
+ },
64
+ LocationData {
65
+ name: "Santa Maria del Fiore",
66
+ lat: 43.7755,
67
+ lng: 11.2540,
68
+ customer_type: CustomerType::Business,
69
+ },
70
+ LocationData {
71
+ name: "Stazione Leopolda",
72
+ lat: 43.7780,
73
+ lng: 11.2470,
74
+ customer_type: CustomerType::Business,
75
+ },
76
+ LocationData {
77
+ name: "Fortezza da Basso",
78
+ lat: 43.7762,
79
+ lng: 11.2540,
80
+ customer_type: CustomerType::Business,
81
+ },
82
+ LocationData {
83
+ name: "Palazzo Medici Riccardi",
84
+ lat: 43.7765,
85
+ lng: 11.2555,
86
+ customer_type: CustomerType::Business,
87
+ },
88
+ LocationData {
89
+ name: "San Lorenzo Market",
90
+ lat: 43.7762,
91
+ lng: 11.2540,
92
+ customer_type: CustomerType::Business,
93
+ },
94
+ LocationData {
95
+ name: "Santa Maria Novella",
96
+ lat: 43.7745,
97
+ lng: 11.2487,
98
+ customer_type: CustomerType::Business,
99
+ },
100
+ LocationData {
101
+ name: "Boboli Gardens Office",
102
+ lat: 43.7665,
103
+ lng: 11.2470,
104
+ customer_type: CustomerType::Business,
105
+ },
106
+ LocationData {
107
+ name: "Villa Bardini",
108
+ lat: 43.7660,
109
+ lng: 11.2650,
110
+ customer_type: CustomerType::Business,
111
+ },
112
+ LocationData {
113
+ name: "Careggi Hospital",
114
+ lat: 43.8020,
115
+ lng: 11.2530,
116
+ customer_type: CustomerType::Business,
117
+ },
118
+ LocationData {
119
+ name: "Coverciano Offices",
120
+ lat: 43.7850,
121
+ lng: 11.3100,
122
+ customer_type: CustomerType::Business,
123
+ },
124
+ LocationData {
125
+ name: "Firenze Airport Cargo",
126
+ lat: 43.7960,
127
+ lng: 11.2050,
128
+ customer_type: CustomerType::Business,
129
+ },
130
+ LocationData {
131
+ name: "Statuto",
132
+ lat: 43.7890,
133
+ lng: 11.2580,
134
+ customer_type: CustomerType::Residential,
135
+ },
136
+ LocationData {
137
+ name: "Piazza Beccaria",
138
+ lat: 43.7688,
139
+ lng: 11.2620,
140
+ customer_type: CustomerType::Residential,
141
+ },
142
+ LocationData {
143
+ name: "Coverciano",
144
+ lat: 43.7850,
145
+ lng: 11.3100,
146
+ customer_type: CustomerType::Residential,
147
+ },
148
+ LocationData {
149
+ name: "Campo di Marte East",
150
+ lat: 43.7820,
151
+ lng: 11.2820,
152
+ customer_type: CustomerType::Residential,
153
+ },
154
+ LocationData {
155
+ name: "Novoli South",
156
+ lat: 43.7880,
157
+ lng: 11.2220,
158
+ customer_type: CustomerType::Residential,
159
+ },
160
+ LocationData {
161
+ name: "Rifredi South",
162
+ lat: 43.7950,
163
+ lng: 11.2410,
164
+ customer_type: CustomerType::Residential,
165
+ },
166
+ LocationData {
167
+ name: "Careggi North",
168
+ lat: 43.8020,
169
+ lng: 11.2530,
170
+ customer_type: CustomerType::Residential,
171
+ },
172
+ LocationData {
173
+ name: "Isolotto South",
174
+ lat: 43.7620,
175
+ lng: 11.2200,
176
+ customer_type: CustomerType::Residential,
177
+ },
178
+ LocationData {
179
+ name: "Gavinana South",
180
+ lat: 43.7520,
181
+ lng: 11.2680,
182
+ customer_type: CustomerType::Residential,
183
+ },
184
+ LocationData {
185
+ name: "Galluzzo Center",
186
+ lat: 43.7400,
187
+ lng: 11.2480,
188
+ customer_type: CustomerType::Residential,
189
+ },
190
+ LocationData {
191
+ name: "Arcetri Hill",
192
+ lat: 43.7500,
193
+ lng: 11.2530,
194
+ customer_type: CustomerType::Residential,
195
+ },
196
+ ];
src/data/data_seed/hartford.rs ADDED
@@ -0,0 +1,8 @@
 
 
 
 
 
 
 
 
 
1
+ use super::types::LocationData;
2
+
3
+ mod depots;
4
+ mod visits;
5
+ mod visits_extra;
6
+
7
+ pub(super) use depots::DEPOTS;
8
+ pub(super) const VISIT_GROUPS: &[&[LocationData]] = &[visits::VISITS, visits_extra::VISITS];
src/data/data_seed/hartford/depots.rs ADDED
@@ -0,0 +1,64 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ use super::super::types::{CustomerType, LocationData};
2
+
3
+ pub(in crate::data::data_seed) const DEPOTS: &[LocationData] = &[
4
+ LocationData {
5
+ name: "Downtown Hartford Depot",
6
+ lat: 41.7658,
7
+ lng: -72.6734,
8
+ customer_type: CustomerType::Business,
9
+ },
10
+ LocationData {
11
+ name: "Asylum Hill Depot",
12
+ lat: 41.7700,
13
+ lng: -72.6900,
14
+ customer_type: CustomerType::Business,
15
+ },
16
+ LocationData {
17
+ name: "South End Depot",
18
+ lat: 41.7400,
19
+ lng: -72.6750,
20
+ customer_type: CustomerType::Business,
21
+ },
22
+ LocationData {
23
+ name: "West End Depot",
24
+ lat: 41.7680,
25
+ lng: -72.7100,
26
+ customer_type: CustomerType::Business,
27
+ },
28
+ LocationData {
29
+ name: "Barry Square Depot",
30
+ lat: 41.7450,
31
+ lng: -72.6800,
32
+ customer_type: CustomerType::Business,
33
+ },
34
+ LocationData {
35
+ name: "Clay Arsenal Depot",
36
+ lat: 41.7750,
37
+ lng: -72.6850,
38
+ customer_type: CustomerType::Business,
39
+ },
40
+ LocationData {
41
+ name: "Science Center Depot",
42
+ lat: 41.7650,
43
+ lng: -72.6695,
44
+ customer_type: CustomerType::Business,
45
+ },
46
+ LocationData {
47
+ name: "Frog Hollow Depot",
48
+ lat: 41.7580,
49
+ lng: -72.6900,
50
+ customer_type: CustomerType::Business,
51
+ },
52
+ LocationData {
53
+ name: "Blue Hills Depot",
54
+ lat: 41.7850,
55
+ lng: -72.7050,
56
+ customer_type: CustomerType::Business,
57
+ },
58
+ LocationData {
59
+ name: "Charter Oak Depot",
60
+ lat: 41.7495,
61
+ lng: -72.6650,
62
+ customer_type: CustomerType::Business,
63
+ },
64
+ ];
src/data/data_seed/hartford/visits.rs ADDED
@@ -0,0 +1,184 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ use super::super::types::{CustomerType, LocationData};
2
+
3
+ pub(in crate::data::data_seed) const VISITS: &[LocationData] = &[
4
+ LocationData {
5
+ name: "Max Downtown",
6
+ lat: 41.7670,
7
+ lng: -72.6730,
8
+ customer_type: CustomerType::Restaurant,
9
+ },
10
+ LocationData {
11
+ name: "Trumbull Kitchen",
12
+ lat: 41.7650,
13
+ lng: -72.6750,
14
+ customer_type: CustomerType::Restaurant,
15
+ },
16
+ LocationData {
17
+ name: "Salute",
18
+ lat: 41.7630,
19
+ lng: -72.6740,
20
+ customer_type: CustomerType::Restaurant,
21
+ },
22
+ LocationData {
23
+ name: "Peppercorns Grill",
24
+ lat: 41.7680,
25
+ lng: -72.6700,
26
+ customer_type: CustomerType::Restaurant,
27
+ },
28
+ LocationData {
29
+ name: "Feng Asian Bistro",
30
+ lat: 41.7640,
31
+ lng: -72.6725,
32
+ customer_type: CustomerType::Restaurant,
33
+ },
34
+ LocationData {
35
+ name: "On20",
36
+ lat: 41.7655,
37
+ lng: -72.6728,
38
+ customer_type: CustomerType::Restaurant,
39
+ },
40
+ LocationData {
41
+ name: "First and Last Tavern",
42
+ lat: 41.7620,
43
+ lng: -72.7050,
44
+ customer_type: CustomerType::Restaurant,
45
+ },
46
+ LocationData {
47
+ name: "Agave Grill",
48
+ lat: 41.7580,
49
+ lng: -72.6820,
50
+ customer_type: CustomerType::Restaurant,
51
+ },
52
+ LocationData {
53
+ name: "Bear's Smokehouse",
54
+ lat: 41.7550,
55
+ lng: -72.6780,
56
+ customer_type: CustomerType::Restaurant,
57
+ },
58
+ LocationData {
59
+ name: "City Steam Brewery",
60
+ lat: 41.7630,
61
+ lng: -72.6750,
62
+ customer_type: CustomerType::Restaurant,
63
+ },
64
+ LocationData {
65
+ name: "Travelers Tower",
66
+ lat: 41.7658,
67
+ lng: -72.6734,
68
+ customer_type: CustomerType::Business,
69
+ },
70
+ LocationData {
71
+ name: "Hartford Steam Boiler",
72
+ lat: 41.7680,
73
+ lng: -72.6700,
74
+ customer_type: CustomerType::Business,
75
+ },
76
+ LocationData {
77
+ name: "Aetna Building",
78
+ lat: 41.7700,
79
+ lng: -72.6900,
80
+ customer_type: CustomerType::Business,
81
+ },
82
+ LocationData {
83
+ name: "Connecticut Convention Center",
84
+ lat: 41.7615,
85
+ lng: -72.6820,
86
+ customer_type: CustomerType::Business,
87
+ },
88
+ LocationData {
89
+ name: "Hartford Hospital",
90
+ lat: 41.7547,
91
+ lng: -72.6858,
92
+ customer_type: CustomerType::Business,
93
+ },
94
+ LocationData {
95
+ name: "Connecticut Children's",
96
+ lat: 41.7560,
97
+ lng: -72.6850,
98
+ customer_type: CustomerType::Business,
99
+ },
100
+ LocationData {
101
+ name: "Trinity College",
102
+ lat: 41.7580,
103
+ lng: -72.6900,
104
+ customer_type: CustomerType::Business,
105
+ },
106
+ LocationData {
107
+ name: "Connecticut Science Center",
108
+ lat: 41.7650,
109
+ lng: -72.6695,
110
+ customer_type: CustomerType::Business,
111
+ },
112
+ LocationData {
113
+ name: "West End Hartford",
114
+ lat: 41.7680,
115
+ lng: -72.7000,
116
+ customer_type: CustomerType::Residential,
117
+ },
118
+ LocationData {
119
+ name: "Asylum Hill",
120
+ lat: 41.7720,
121
+ lng: -72.6850,
122
+ customer_type: CustomerType::Residential,
123
+ },
124
+ LocationData {
125
+ name: "Frog Hollow",
126
+ lat: 41.7580,
127
+ lng: -72.6900,
128
+ customer_type: CustomerType::Residential,
129
+ },
130
+ LocationData {
131
+ name: "Barry Square",
132
+ lat: 41.7450,
133
+ lng: -72.6800,
134
+ customer_type: CustomerType::Residential,
135
+ },
136
+ LocationData {
137
+ name: "South End",
138
+ lat: 41.7400,
139
+ lng: -72.6750,
140
+ customer_type: CustomerType::Residential,
141
+ },
142
+ LocationData {
143
+ name: "Blue Hills",
144
+ lat: 41.7850,
145
+ lng: -72.7050,
146
+ customer_type: CustomerType::Residential,
147
+ },
148
+ LocationData {
149
+ name: "Parkville",
150
+ lat: 41.7650,
151
+ lng: -72.7100,
152
+ customer_type: CustomerType::Residential,
153
+ },
154
+ LocationData {
155
+ name: "Behind the Rocks",
156
+ lat: 41.7550,
157
+ lng: -72.7050,
158
+ customer_type: CustomerType::Residential,
159
+ },
160
+ LocationData {
161
+ name: "Charter Oak",
162
+ lat: 41.7495,
163
+ lng: -72.6650,
164
+ customer_type: CustomerType::Residential,
165
+ },
166
+ LocationData {
167
+ name: "Sheldon Charter Oak",
168
+ lat: 41.7510,
169
+ lng: -72.6700,
170
+ customer_type: CustomerType::Residential,
171
+ },
172
+ LocationData {
173
+ name: "Clay Arsenal",
174
+ lat: 41.7750,
175
+ lng: -72.6850,
176
+ customer_type: CustomerType::Residential,
177
+ },
178
+ LocationData {
179
+ name: "Upper Albany",
180
+ lat: 41.7780,
181
+ lng: -72.6950,
182
+ customer_type: CustomerType::Residential,
183
+ },
184
+ ];
src/data/data_seed/hartford/visits_extra.rs ADDED
@@ -0,0 +1,124 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ use super::super::types::{CustomerType, LocationData};
2
+
3
+ pub(in crate::data::data_seed) const VISITS: &[LocationData] = &[
4
+ LocationData {
5
+ name: "The Place 2 Be",
6
+ lat: 41.7670,
7
+ lng: -72.6730,
8
+ customer_type: CustomerType::Restaurant,
9
+ },
10
+ LocationData {
11
+ name: "Republic at the Linden",
12
+ lat: 41.7650,
13
+ lng: -72.6750,
14
+ customer_type: CustomerType::Restaurant,
15
+ },
16
+ LocationData {
17
+ name: "Sorella",
18
+ lat: 41.7630,
19
+ lng: -72.6750,
20
+ customer_type: CustomerType::Restaurant,
21
+ },
22
+ LocationData {
23
+ name: "Black-Eyed Sally's",
24
+ lat: 41.7580,
25
+ lng: -72.6820,
26
+ customer_type: CustomerType::Restaurant,
27
+ },
28
+ LocationData {
29
+ name: "The Russell Hartford",
30
+ lat: 41.7655,
31
+ lng: -72.6728,
32
+ customer_type: CustomerType::Restaurant,
33
+ },
34
+ LocationData {
35
+ name: "Fiddleheads Cafe",
36
+ lat: 41.7580,
37
+ lng: -72.6900,
38
+ customer_type: CustomerType::Restaurant,
39
+ },
40
+ LocationData {
41
+ name: "Bushnell Center",
42
+ lat: 41.7615,
43
+ lng: -72.6820,
44
+ customer_type: CustomerType::Business,
45
+ },
46
+ LocationData {
47
+ name: "State Capitol",
48
+ lat: 41.7630,
49
+ lng: -72.6740,
50
+ customer_type: CustomerType::Business,
51
+ },
52
+ LocationData {
53
+ name: "Hartford Public Library",
54
+ lat: 41.7650,
55
+ lng: -72.6695,
56
+ customer_type: CustomerType::Business,
57
+ },
58
+ LocationData {
59
+ name: "XL Center",
60
+ lat: 41.7658,
61
+ lng: -72.6734,
62
+ customer_type: CustomerType::Business,
63
+ },
64
+ LocationData {
65
+ name: "Union Station Hartford",
66
+ lat: 41.7680,
67
+ lng: -72.6700,
68
+ customer_type: CustomerType::Business,
69
+ },
70
+ LocationData {
71
+ name: "Real Art Ways",
72
+ lat: 41.7650,
73
+ lng: -72.7100,
74
+ customer_type: CustomerType::Business,
75
+ },
76
+ LocationData {
77
+ name: "Colt Gateway",
78
+ lat: 41.7495,
79
+ lng: -72.6650,
80
+ customer_type: CustomerType::Business,
81
+ },
82
+ LocationData {
83
+ name: "South Green",
84
+ lat: 41.7550,
85
+ lng: -72.6780,
86
+ customer_type: CustomerType::Residential,
87
+ },
88
+ LocationData {
89
+ name: "North Meadows",
90
+ lat: 41.7750,
91
+ lng: -72.6850,
92
+ customer_type: CustomerType::Residential,
93
+ },
94
+ LocationData {
95
+ name: "South Meadows",
96
+ lat: 41.7400,
97
+ lng: -72.6750,
98
+ customer_type: CustomerType::Residential,
99
+ },
100
+ LocationData {
101
+ name: "West Hartford Line",
102
+ lat: 41.7680,
103
+ lng: -72.7000,
104
+ customer_type: CustomerType::Residential,
105
+ },
106
+ LocationData {
107
+ name: "Southwest Hartford",
108
+ lat: 41.7550,
109
+ lng: -72.7050,
110
+ customer_type: CustomerType::Residential,
111
+ },
112
+ LocationData {
113
+ name: "North End",
114
+ lat: 41.7780,
115
+ lng: -72.6950,
116
+ customer_type: CustomerType::Residential,
117
+ },
118
+ LocationData {
119
+ name: "Park Terrace",
120
+ lat: 41.7450,
121
+ lng: -72.6800,
122
+ customer_type: CustomerType::Residential,
123
+ },
124
+ ];
src/data/data_seed/philadelphia.rs ADDED
@@ -0,0 +1,8 @@
 
 
 
 
 
 
 
 
 
1
+ use super::types::LocationData;
2
+
3
+ mod depots;
4
+ mod visits;
5
+ mod visits_extra;
6
+
7
+ pub(super) use depots::DEPOTS;
8
+ pub(super) const VISIT_GROUPS: &[&[LocationData]] = &[visits::VISITS, visits_extra::VISITS];
src/data/data_seed/philadelphia/depots.rs ADDED
@@ -0,0 +1,64 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ use super::super::types::{CustomerType, LocationData};
2
+
3
+ pub(in crate::data::data_seed) const DEPOTS: &[LocationData] = &[
4
+ LocationData {
5
+ name: "Central Depot - City Hall",
6
+ lat: 39.9526,
7
+ lng: -75.1652,
8
+ customer_type: CustomerType::Business,
9
+ },
10
+ LocationData {
11
+ name: "South Philly Depot",
12
+ lat: 39.9256,
13
+ lng: -75.1697,
14
+ customer_type: CustomerType::Business,
15
+ },
16
+ LocationData {
17
+ name: "University City Depot",
18
+ lat: 39.9522,
19
+ lng: -75.1932,
20
+ customer_type: CustomerType::Business,
21
+ },
22
+ LocationData {
23
+ name: "North Philly Depot",
24
+ lat: 39.9907,
25
+ lng: -75.1556,
26
+ customer_type: CustomerType::Business,
27
+ },
28
+ LocationData {
29
+ name: "Fishtown Depot",
30
+ lat: 39.9712,
31
+ lng: -75.1340,
32
+ customer_type: CustomerType::Business,
33
+ },
34
+ LocationData {
35
+ name: "West Philly Depot",
36
+ lat: 39.9601,
37
+ lng: -75.2175,
38
+ customer_type: CustomerType::Business,
39
+ },
40
+ LocationData {
41
+ name: "Logan Square Depot",
42
+ lat: 39.9567,
43
+ lng: -75.1720,
44
+ customer_type: CustomerType::Business,
45
+ },
46
+ LocationData {
47
+ name: "Pennsport Depot",
48
+ lat: 39.9320,
49
+ lng: -75.1450,
50
+ customer_type: CustomerType::Business,
51
+ },
52
+ LocationData {
53
+ name: "Kensington Depot",
54
+ lat: 39.9850,
55
+ lng: -75.1280,
56
+ customer_type: CustomerType::Business,
57
+ },
58
+ LocationData {
59
+ name: "Spruce Hill Depot",
60
+ lat: 39.9530,
61
+ lng: -75.2100,
62
+ customer_type: CustomerType::Business,
63
+ },
64
+ ];
src/data/data_seed/philadelphia/visits.rs ADDED
@@ -0,0 +1,298 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ use super::super::types::{CustomerType, LocationData};
2
+
3
+ pub(in crate::data::data_seed) const VISITS: &[LocationData] = &[
4
+ LocationData {
5
+ name: "Reading Terminal Market",
6
+ lat: 39.9535,
7
+ lng: -75.1589,
8
+ customer_type: CustomerType::Restaurant,
9
+ },
10
+ LocationData {
11
+ name: "Parc Restaurant",
12
+ lat: 39.9493,
13
+ lng: -75.1727,
14
+ customer_type: CustomerType::Restaurant,
15
+ },
16
+ LocationData {
17
+ name: "Zahav",
18
+ lat: 39.9430,
19
+ lng: -75.1474,
20
+ customer_type: CustomerType::Restaurant,
21
+ },
22
+ LocationData {
23
+ name: "Vetri Cucina",
24
+ lat: 39.9499,
25
+ lng: -75.1659,
26
+ customer_type: CustomerType::Restaurant,
27
+ },
28
+ LocationData {
29
+ name: "Talula's Garden",
30
+ lat: 39.9470,
31
+ lng: -75.1709,
32
+ customer_type: CustomerType::Restaurant,
33
+ },
34
+ LocationData {
35
+ name: "Fork",
36
+ lat: 39.9493,
37
+ lng: -75.1539,
38
+ customer_type: CustomerType::Restaurant,
39
+ },
40
+ LocationData {
41
+ name: "Morimoto",
42
+ lat: 39.9488,
43
+ lng: -75.1559,
44
+ customer_type: CustomerType::Restaurant,
45
+ },
46
+ LocationData {
47
+ name: "Vernick Food & Drink",
48
+ lat: 39.9508,
49
+ lng: -75.1718,
50
+ customer_type: CustomerType::Restaurant,
51
+ },
52
+ LocationData {
53
+ name: "Friday Saturday Sunday",
54
+ lat: 39.9492,
55
+ lng: -75.1715,
56
+ customer_type: CustomerType::Restaurant,
57
+ },
58
+ LocationData {
59
+ name: "Royal Izakaya",
60
+ lat: 39.9410,
61
+ lng: -75.1509,
62
+ customer_type: CustomerType::Restaurant,
63
+ },
64
+ LocationData {
65
+ name: "Laurel",
66
+ lat: 39.9392,
67
+ lng: -75.1538,
68
+ customer_type: CustomerType::Restaurant,
69
+ },
70
+ LocationData {
71
+ name: "Marigold Kitchen",
72
+ lat: 39.9533,
73
+ lng: -75.1920,
74
+ customer_type: CustomerType::Restaurant,
75
+ },
76
+ LocationData {
77
+ name: "Comcast Center",
78
+ lat: 39.9543,
79
+ lng: -75.1690,
80
+ customer_type: CustomerType::Business,
81
+ },
82
+ LocationData {
83
+ name: "Liberty Place",
84
+ lat: 39.9520,
85
+ lng: -75.1685,
86
+ customer_type: CustomerType::Business,
87
+ },
88
+ LocationData {
89
+ name: "BNY Mellon Center",
90
+ lat: 39.9505,
91
+ lng: -75.1660,
92
+ customer_type: CustomerType::Business,
93
+ },
94
+ LocationData {
95
+ name: "One Liberty Place",
96
+ lat: 39.9520,
97
+ lng: -75.1685,
98
+ customer_type: CustomerType::Business,
99
+ },
100
+ LocationData {
101
+ name: "Aramark Tower",
102
+ lat: 39.9550,
103
+ lng: -75.1705,
104
+ customer_type: CustomerType::Business,
105
+ },
106
+ LocationData {
107
+ name: "PSFS Building",
108
+ lat: 39.9521,
109
+ lng: -75.1602,
110
+ customer_type: CustomerType::Business,
111
+ },
112
+ LocationData {
113
+ name: "Three Logan Square",
114
+ lat: 39.9567,
115
+ lng: -75.1720,
116
+ customer_type: CustomerType::Business,
117
+ },
118
+ LocationData {
119
+ name: "Two Commerce Square",
120
+ lat: 39.9551,
121
+ lng: -75.1675,
122
+ customer_type: CustomerType::Business,
123
+ },
124
+ LocationData {
125
+ name: "Penn Medicine",
126
+ lat: 39.9500,
127
+ lng: -75.1930,
128
+ customer_type: CustomerType::Business,
129
+ },
130
+ LocationData {
131
+ name: "Children's Hospital",
132
+ lat: 39.9482,
133
+ lng: -75.1950,
134
+ customer_type: CustomerType::Business,
135
+ },
136
+ LocationData {
137
+ name: "Drexel University",
138
+ lat: 39.9566,
139
+ lng: -75.1899,
140
+ customer_type: CustomerType::Business,
141
+ },
142
+ LocationData {
143
+ name: "Temple University",
144
+ lat: 39.9812,
145
+ lng: -75.1554,
146
+ customer_type: CustomerType::Business,
147
+ },
148
+ LocationData {
149
+ name: "Jefferson Hospital",
150
+ lat: 39.9487,
151
+ lng: -75.1577,
152
+ customer_type: CustomerType::Business,
153
+ },
154
+ LocationData {
155
+ name: "Pennsylvania Hospital",
156
+ lat: 39.9445,
157
+ lng: -75.1545,
158
+ customer_type: CustomerType::Business,
159
+ },
160
+ LocationData {
161
+ name: "FMC Tower",
162
+ lat: 39.9499,
163
+ lng: -75.1780,
164
+ customer_type: CustomerType::Business,
165
+ },
166
+ LocationData {
167
+ name: "Cira Centre",
168
+ lat: 39.9560,
169
+ lng: -75.1822,
170
+ customer_type: CustomerType::Business,
171
+ },
172
+ LocationData {
173
+ name: "Rittenhouse Square",
174
+ lat: 39.9496,
175
+ lng: -75.1718,
176
+ customer_type: CustomerType::Residential,
177
+ },
178
+ LocationData {
179
+ name: "Washington Square West",
180
+ lat: 39.9468,
181
+ lng: -75.1545,
182
+ customer_type: CustomerType::Residential,
183
+ },
184
+ LocationData {
185
+ name: "Society Hill",
186
+ lat: 39.9425,
187
+ lng: -75.1478,
188
+ customer_type: CustomerType::Residential,
189
+ },
190
+ LocationData {
191
+ name: "Old City",
192
+ lat: 39.9510,
193
+ lng: -75.1450,
194
+ customer_type: CustomerType::Residential,
195
+ },
196
+ LocationData {
197
+ name: "Northern Liberties",
198
+ lat: 39.9650,
199
+ lng: -75.1420,
200
+ customer_type: CustomerType::Residential,
201
+ },
202
+ LocationData {
203
+ name: "Fishtown",
204
+ lat: 39.9712,
205
+ lng: -75.1340,
206
+ customer_type: CustomerType::Residential,
207
+ },
208
+ LocationData {
209
+ name: "Queen Village",
210
+ lat: 39.9380,
211
+ lng: -75.1520,
212
+ customer_type: CustomerType::Residential,
213
+ },
214
+ LocationData {
215
+ name: "Bella Vista",
216
+ lat: 39.9395,
217
+ lng: -75.1598,
218
+ customer_type: CustomerType::Residential,
219
+ },
220
+ LocationData {
221
+ name: "Graduate Hospital",
222
+ lat: 39.9425,
223
+ lng: -75.1768,
224
+ customer_type: CustomerType::Residential,
225
+ },
226
+ LocationData {
227
+ name: "Fairmount",
228
+ lat: 39.9680,
229
+ lng: -75.1750,
230
+ customer_type: CustomerType::Residential,
231
+ },
232
+ LocationData {
233
+ name: "Spring Garden",
234
+ lat: 39.9620,
235
+ lng: -75.1620,
236
+ customer_type: CustomerType::Residential,
237
+ },
238
+ LocationData {
239
+ name: "Art Museum Area",
240
+ lat: 39.9656,
241
+ lng: -75.1810,
242
+ customer_type: CustomerType::Residential,
243
+ },
244
+ LocationData {
245
+ name: "Brewerytown",
246
+ lat: 39.9750,
247
+ lng: -75.1850,
248
+ customer_type: CustomerType::Residential,
249
+ },
250
+ LocationData {
251
+ name: "East Passyunk",
252
+ lat: 39.9310,
253
+ lng: -75.1605,
254
+ customer_type: CustomerType::Residential,
255
+ },
256
+ LocationData {
257
+ name: "Point Breeze",
258
+ lat: 39.9285,
259
+ lng: -75.1780,
260
+ customer_type: CustomerType::Residential,
261
+ },
262
+ LocationData {
263
+ name: "Pennsport",
264
+ lat: 39.9320,
265
+ lng: -75.1450,
266
+ customer_type: CustomerType::Residential,
267
+ },
268
+ LocationData {
269
+ name: "Powelton Village",
270
+ lat: 39.9610,
271
+ lng: -75.1950,
272
+ customer_type: CustomerType::Residential,
273
+ },
274
+ LocationData {
275
+ name: "Spruce Hill",
276
+ lat: 39.9530,
277
+ lng: -75.2100,
278
+ customer_type: CustomerType::Residential,
279
+ },
280
+ LocationData {
281
+ name: "Cedar Park",
282
+ lat: 39.9490,
283
+ lng: -75.2200,
284
+ customer_type: CustomerType::Residential,
285
+ },
286
+ LocationData {
287
+ name: "Kensington",
288
+ lat: 39.9850,
289
+ lng: -75.1280,
290
+ customer_type: CustomerType::Residential,
291
+ },
292
+ LocationData {
293
+ name: "Port Richmond",
294
+ lat: 39.9870,
295
+ lng: -75.1120,
296
+ customer_type: CustomerType::Residential,
297
+ },
298
+ ];
src/data/data_seed/philadelphia/visits_extra.rs ADDED
@@ -0,0 +1,202 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ use super::super::types::{CustomerType, LocationData};
2
+
3
+ pub(in crate::data::data_seed) const VISITS: &[LocationData] = &[
4
+ LocationData {
5
+ name: "Di Bruno Bros",
6
+ lat: 39.9496,
7
+ lng: -75.1718,
8
+ customer_type: CustomerType::Restaurant,
9
+ },
10
+ LocationData {
11
+ name: "Federal Donuts Center City",
12
+ lat: 39.9492,
13
+ lng: -75.1715,
14
+ customer_type: CustomerType::Restaurant,
15
+ },
16
+ LocationData {
17
+ name: "High Street Philly",
18
+ lat: 39.9493,
19
+ lng: -75.1539,
20
+ customer_type: CustomerType::Restaurant,
21
+ },
22
+ LocationData {
23
+ name: "Suraya",
24
+ lat: 39.9712,
25
+ lng: -75.1340,
26
+ customer_type: CustomerType::Restaurant,
27
+ },
28
+ LocationData {
29
+ name: "Wm Mulherin's Sons",
30
+ lat: 39.9712,
31
+ lng: -75.1340,
32
+ customer_type: CustomerType::Restaurant,
33
+ },
34
+ LocationData {
35
+ name: "El Vez",
36
+ lat: 39.9499,
37
+ lng: -75.1659,
38
+ customer_type: CustomerType::Restaurant,
39
+ },
40
+ LocationData {
41
+ name: "Barbuzzo",
42
+ lat: 39.9493,
43
+ lng: -75.1727,
44
+ customer_type: CustomerType::Restaurant,
45
+ },
46
+ LocationData {
47
+ name: "The Love",
48
+ lat: 39.9508,
49
+ lng: -75.1718,
50
+ customer_type: CustomerType::Restaurant,
51
+ },
52
+ LocationData {
53
+ name: "30th Street Station",
54
+ lat: 39.9560,
55
+ lng: -75.1822,
56
+ customer_type: CustomerType::Business,
57
+ },
58
+ LocationData {
59
+ name: "University of Pennsylvania",
60
+ lat: 39.9500,
61
+ lng: -75.1930,
62
+ customer_type: CustomerType::Business,
63
+ },
64
+ LocationData {
65
+ name: "Franklin Institute",
66
+ lat: 39.9567,
67
+ lng: -75.1720,
68
+ customer_type: CustomerType::Business,
69
+ },
70
+ LocationData {
71
+ name: "Academy of Natural Sciences",
72
+ lat: 39.9567,
73
+ lng: -75.1720,
74
+ customer_type: CustomerType::Business,
75
+ },
76
+ LocationData {
77
+ name: "Kimmel Center",
78
+ lat: 39.9499,
79
+ lng: -75.1659,
80
+ customer_type: CustomerType::Business,
81
+ },
82
+ LocationData {
83
+ name: "City Hall Annex",
84
+ lat: 39.9526,
85
+ lng: -75.1652,
86
+ customer_type: CustomerType::Business,
87
+ },
88
+ LocationData {
89
+ name: "Independence Hall",
90
+ lat: 39.9510,
91
+ lng: -75.1450,
92
+ customer_type: CustomerType::Business,
93
+ },
94
+ LocationData {
95
+ name: "Navy Yard Offices",
96
+ lat: 39.9256,
97
+ lng: -75.1697,
98
+ customer_type: CustomerType::Business,
99
+ },
100
+ LocationData {
101
+ name: "Penn's Landing",
102
+ lat: 39.9410,
103
+ lng: -75.1509,
104
+ customer_type: CustomerType::Business,
105
+ },
106
+ LocationData {
107
+ name: "Rodin Museum",
108
+ lat: 39.9656,
109
+ lng: -75.1810,
110
+ customer_type: CustomerType::Business,
111
+ },
112
+ LocationData {
113
+ name: "Barnes Foundation",
114
+ lat: 39.9656,
115
+ lng: -75.1810,
116
+ customer_type: CustomerType::Business,
117
+ },
118
+ LocationData {
119
+ name: "Fitler Square",
120
+ lat: 39.9499,
121
+ lng: -75.1780,
122
+ customer_type: CustomerType::Residential,
123
+ },
124
+ LocationData {
125
+ name: "Logan Square",
126
+ lat: 39.9567,
127
+ lng: -75.1720,
128
+ customer_type: CustomerType::Residential,
129
+ },
130
+ LocationData {
131
+ name: "Callowhill",
132
+ lat: 39.9620,
133
+ lng: -75.1620,
134
+ customer_type: CustomerType::Residential,
135
+ },
136
+ LocationData {
137
+ name: "Francisville",
138
+ lat: 39.9680,
139
+ lng: -75.1750,
140
+ customer_type: CustomerType::Residential,
141
+ },
142
+ LocationData {
143
+ name: "Whitman",
144
+ lat: 39.9256,
145
+ lng: -75.1697,
146
+ customer_type: CustomerType::Residential,
147
+ },
148
+ LocationData {
149
+ name: "Passyunk Square",
150
+ lat: 39.9310,
151
+ lng: -75.1605,
152
+ customer_type: CustomerType::Residential,
153
+ },
154
+ LocationData {
155
+ name: "Girard Estates",
156
+ lat: 39.9285,
157
+ lng: -75.1780,
158
+ customer_type: CustomerType::Residential,
159
+ },
160
+ LocationData {
161
+ name: "West Powelton",
162
+ lat: 39.9610,
163
+ lng: -75.1950,
164
+ customer_type: CustomerType::Residential,
165
+ },
166
+ LocationData {
167
+ name: "Mantua",
168
+ lat: 39.9610,
169
+ lng: -75.1950,
170
+ customer_type: CustomerType::Residential,
171
+ },
172
+ LocationData {
173
+ name: "Walnut Hill",
174
+ lat: 39.9530,
175
+ lng: -75.2100,
176
+ customer_type: CustomerType::Residential,
177
+ },
178
+ LocationData {
179
+ name: "Cobbs Creek",
180
+ lat: 39.9490,
181
+ lng: -75.2200,
182
+ customer_type: CustomerType::Residential,
183
+ },
184
+ LocationData {
185
+ name: "Harrowgate",
186
+ lat: 39.9850,
187
+ lng: -75.1280,
188
+ customer_type: CustomerType::Residential,
189
+ },
190
+ LocationData {
191
+ name: "Tacony",
192
+ lat: 39.9870,
193
+ lng: -75.1120,
194
+ customer_type: CustomerType::Residential,
195
+ },
196
+ LocationData {
197
+ name: "Manayunk",
198
+ lat: 39.9750,
199
+ lng: -75.1850,
200
+ customer_type: CustomerType::Residential,
201
+ },
202
+ ];
src/data/data_seed/tests.rs ADDED
@@ -0,0 +1,123 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ use super::*;
2
+ use crate::data::data_seed::types::LocationData;
3
+ use solverforge_maps::{BoundingBox, Coord, NetworkConfig, RoadNetwork};
4
+
5
+ #[test]
6
+ fn parses_demo_data_ids_case_insensitively() {
7
+ assert!(matches!(
8
+ "philadelphia".parse::<DemoData>(),
9
+ Ok(DemoData::Philadelphia)
10
+ ));
11
+ assert!(matches!(
12
+ "HARTFORD".parse::<DemoData>(),
13
+ Ok(DemoData::Hartford)
14
+ ));
15
+ }
16
+
17
+ #[test]
18
+ fn generates_city_demo_plan() {
19
+ for (demo, expected_stops, expected_name) in [
20
+ (
21
+ DemoData::Philadelphia,
22
+ visit_count(philadelphia::VISIT_GROUPS),
23
+ "Philadelphia",
24
+ ),
25
+ (
26
+ DemoData::Hartford,
27
+ visit_count(hartford::VISIT_GROUPS),
28
+ "Hartford",
29
+ ),
30
+ (
31
+ DemoData::Firenze,
32
+ visit_count(firenze::VISIT_GROUPS),
33
+ "Firenze",
34
+ ),
35
+ ] {
36
+ let plan = generate(demo);
37
+ assert_eq!(plan.name, expected_name);
38
+ assert_eq!(plan.vehicles.len(), 10);
39
+ assert_eq!(plan.deliveries.len(), expected_stops);
40
+ assert!(plan
41
+ .vehicles
42
+ .iter()
43
+ .all(|vehicle| vehicle.delivery_order.is_empty()));
44
+ }
45
+ }
46
+
47
+ fn visit_count(groups: &[&[LocationData]]) -> usize {
48
+ groups.iter().map(|group| group.len()).sum()
49
+ }
50
+
51
+ #[test]
52
+ fn demo_delivery_counts_scale_with_ten_vehicles() {
53
+ assert_eq!(generate(DemoData::Philadelphia).deliveries.len(), 82);
54
+ assert_eq!(generate(DemoData::Hartford).deliveries.len(), 50);
55
+ assert_eq!(generate(DemoData::Firenze).deliveries.len(), 80);
56
+ }
57
+
58
+ #[test]
59
+ fn demo_plans_have_enough_vehicle_capacity() {
60
+ for demo in [
61
+ DemoData::Philadelphia,
62
+ DemoData::Hartford,
63
+ DemoData::Firenze,
64
+ ] {
65
+ let plan = generate(demo);
66
+ let total_capacity: i32 = plan.vehicles.iter().map(|vehicle| vehicle.capacity).sum();
67
+ let total_demand: i32 = plan.deliveries.iter().map(|delivery| delivery.demand).sum();
68
+
69
+ assert!(
70
+ total_capacity >= total_demand,
71
+ "{demo:?} demo should be capacity-feasible before route ordering: capacity={total_capacity}, demand={total_demand}"
72
+ );
73
+ }
74
+ }
75
+
76
+ #[tokio::test]
77
+ async fn live_demo_locations_are_mutually_reachable_when_enabled() {
78
+ if std::env::var("SOLVERFORGE_RUN_LIVE_TESTS").ok().as_deref() != Some("1") {
79
+ return;
80
+ }
81
+
82
+ for demo in [
83
+ DemoData::Philadelphia,
84
+ DemoData::Hartford,
85
+ DemoData::Firenze,
86
+ ] {
87
+ let plan = generate(demo);
88
+ let mut named_coords = Vec::new();
89
+ for delivery in &plan.deliveries {
90
+ named_coords.push((
91
+ format!("delivery {}", delivery.label),
92
+ delivery.coord().unwrap(),
93
+ ));
94
+ }
95
+ for vehicle in &plan.vehicles {
96
+ named_coords.push((
97
+ format!("depot {}", vehicle.name),
98
+ vehicle.depot_coord().unwrap(),
99
+ ));
100
+ }
101
+
102
+ let coords: Vec<Coord> = named_coords.iter().map(|(_, coord)| *coord).collect();
103
+ let bbox = BoundingBox::from_coords(&coords).expand_for_routing(&coords);
104
+ let network = RoadNetwork::load_or_fetch(&bbox, &NetworkConfig::default(), None)
105
+ .await
106
+ .unwrap();
107
+ let matrix = network.compute_matrix(&coords, None).await;
108
+ let unreachable = matrix
109
+ .unreachable_pairs()
110
+ .into_iter()
111
+ .map(|(from_idx, to_idx)| {
112
+ let from_name = &named_coords[from_idx].0;
113
+ let to_name = &named_coords[to_idx].0;
114
+ format!("{from_name} -> {to_name}")
115
+ })
116
+ .collect::<Vec<_>>();
117
+
118
+ assert!(
119
+ unreachable.is_empty(),
120
+ "{demo:?} has unreachable directed routes: {unreachable:?}"
121
+ );
122
+ }
123
+ }
src/data/data_seed/types.rs ADDED
@@ -0,0 +1,48 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ use crate::domain::DeliveryKind;
2
+
3
+ pub(super) const VEHICLE_NAMES: [&str; 10] = [
4
+ "Alpha", "Bravo", "Charlie", "Delta", "Echo", "Foxtrot", "Golf", "Hotel", "India", "Juliet",
5
+ ];
6
+
7
+ #[derive(Clone, Copy)]
8
+ pub(super) struct LocationData {
9
+ pub name: &'static str,
10
+ pub lat: f64,
11
+ pub lng: f64,
12
+ pub customer_type: CustomerType,
13
+ }
14
+
15
+ #[derive(Clone, Copy)]
16
+ pub(super) enum CustomerType {
17
+ Residential,
18
+ Business,
19
+ Restaurant,
20
+ }
21
+
22
+ impl CustomerType {
23
+ pub(super) fn profile(self) -> (DeliveryKind, i64, i64, (i32, i32), (i64, i64)) {
24
+ match self {
25
+ CustomerType::Residential => (
26
+ DeliveryKind::Residential,
27
+ 17 * 3600,
28
+ 20 * 3600,
29
+ (1, 2),
30
+ (5 * 60, 10 * 60),
31
+ ),
32
+ CustomerType::Business => (
33
+ DeliveryKind::Business,
34
+ 9 * 3600,
35
+ 17 * 3600,
36
+ (3, 6),
37
+ (15 * 60, 30 * 60),
38
+ ),
39
+ CustomerType::Restaurant => (
40
+ DeliveryKind::Restaurant,
41
+ 6 * 3600,
42
+ 10 * 3600,
43
+ (5, 10),
44
+ (20 * 60, 40 * 60),
45
+ ),
46
+ }
47
+ }
48
+ }
src/data/mod.rs ADDED
@@ -0,0 +1,3 @@
 
 
 
 
1
+ mod data_seed;
2
+
3
+ pub use data_seed::{generate, DemoData};
src/domain/coord_value.rs ADDED
@@ -0,0 +1,39 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ use serde::{Deserialize, Serialize};
2
+ use std::fmt;
3
+ use std::hash::{Hash, Hasher};
4
+
5
+ #[derive(Copy, Clone, Default, Serialize, Deserialize)]
6
+ #[serde(transparent)]
7
+ pub struct CoordValue(pub f64);
8
+
9
+ impl CoordValue {
10
+ pub fn get(self) -> f64 {
11
+ self.0
12
+ }
13
+ }
14
+
15
+ impl From<f64> for CoordValue {
16
+ fn from(value: f64) -> Self {
17
+ Self(value)
18
+ }
19
+ }
20
+
21
+ impl fmt::Debug for CoordValue {
22
+ fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
23
+ self.0.fmt(f)
24
+ }
25
+ }
26
+
27
+ impl PartialEq for CoordValue {
28
+ fn eq(&self, other: &Self) -> bool {
29
+ self.0.to_bits() == other.0.to_bits()
30
+ }
31
+ }
32
+
33
+ impl Eq for CoordValue {}
34
+
35
+ impl Hash for CoordValue {
36
+ fn hash<H: Hasher>(&self, state: &mut H) {
37
+ self.0.to_bits().hash(state);
38
+ }
39
+ }
src/domain/delivery.rs ADDED
@@ -0,0 +1,95 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ //! Delivery problem facts.
2
+ //!
3
+ //! A delivery is input data, not something SolverForge mutates directly. The
4
+ //! solver places delivery ids into each vehicle's list variable.
5
+
6
+ use serde::{Deserialize, Serialize};
7
+ use solverforge::prelude::*;
8
+ use solverforge_maps::{Coord, RoutingError};
9
+
10
+ use super::CoordValue;
11
+
12
+ /// A delivery stop that can be assigned into a vehicle route.
13
+ #[problem_fact]
14
+ #[derive(Serialize, Deserialize)]
15
+ #[serde(rename_all = "camelCase")]
16
+ pub struct Delivery {
17
+ #[planning_id]
18
+ pub id: usize,
19
+ pub label: String,
20
+ pub kind: DeliveryKind,
21
+ /// Latitude in decimal degrees, wrapped so derived equality stays stable.
22
+ pub lat: CoordValue,
23
+ /// Longitude in decimal degrees, wrapped so derived equality stays stable.
24
+ pub lng: CoordValue,
25
+ /// Load consumed from the assigned vehicle capacity.
26
+ pub demand: i32,
27
+ /// Earliest allowed service start, expressed as seconds after midnight.
28
+ pub min_start_time: i64,
29
+ /// Latest allowed service end, expressed as seconds after midnight.
30
+ pub max_end_time: i64,
31
+ /// Time spent at the stop after arrival.
32
+ pub service_duration: i64,
33
+ }
34
+
35
+ /// Coarse stop type used to shape demo-data demand and UI icons.
36
+ #[derive(Clone, Copy, Debug, Default, Eq, PartialEq, Serialize, Deserialize)]
37
+ #[serde(rename_all = "snake_case")]
38
+ pub enum DeliveryKind {
39
+ Residential,
40
+ Business,
41
+ Restaurant,
42
+ #[default]
43
+ Other,
44
+ }
45
+
46
+ impl Delivery {
47
+ /// Creates one problem fact from transport-friendly primitive values.
48
+ pub fn new(
49
+ id: usize,
50
+ label: impl Into<String>,
51
+ kind: DeliveryKind,
52
+ coord: (f64, f64),
53
+ demand: i32,
54
+ time_window: (i64, i64),
55
+ service_duration: i64,
56
+ ) -> Self {
57
+ Self {
58
+ id,
59
+ label: label.into(),
60
+ kind,
61
+ lat: coord.0.into(),
62
+ lng: coord.1.into(),
63
+ demand,
64
+ min_start_time: time_window.0,
65
+ max_end_time: time_window.1,
66
+ service_duration,
67
+ }
68
+ }
69
+
70
+ /// Converts the serialized coordinates into the map library's checked type.
71
+ pub fn coord(&self) -> Result<Coord, RoutingError> {
72
+ Ok(Coord::try_new(self.lat.get(), self.lng.get())?)
73
+ }
74
+ }
75
+
76
+ #[cfg(test)]
77
+ mod tests {
78
+ use super::*;
79
+
80
+ #[test]
81
+ fn test_delivery_construction() {
82
+ let fact = Delivery::new(
83
+ 3,
84
+ "Test stop",
85
+ DeliveryKind::Business,
86
+ (43.77, 11.25),
87
+ 4,
88
+ (9 * 3600, 17 * 3600),
89
+ 20 * 60,
90
+ );
91
+ assert_eq!(fact.id, 3);
92
+ assert_eq!(fact.label, "Test stop");
93
+ assert_eq!(fact.kind, DeliveryKind::Business);
94
+ }
95
+ }
src/domain/mod.rs ADDED
@@ -0,0 +1,42 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ //! Planning-model manifest and domain-layer exports.
2
+ //!
3
+ //! `planning_model!` is the single SolverForge model boundary. It lists the
4
+ //! file-backed domain modules, exports the public names used by the rest of the
5
+ //! app, and keeps list-variable hooks close to the domain they describe.
6
+
7
+ solverforge::planning_model! {
8
+ root = "src/domain";
9
+
10
+ // @solverforge:begin domain-exports
11
+ mod coord_value;
12
+ mod delivery;
13
+ mod plan;
14
+ mod preview;
15
+ mod vehicle;
16
+
17
+ pub use coord_value::CoordValue;
18
+ pub use delivery::Delivery;
19
+ pub use delivery::DeliveryKind;
20
+ pub use plan::{Plan, PlanConstraintStreams};
21
+ pub use preview::{
22
+ DeliveryPreview, PlanPreview, PlanViewState, RoutingMode, TimelineView, VehiclePreview,
23
+ VehiclePreviewStop,
24
+ };
25
+ pub use vehicle::Vehicle;
26
+ // @solverforge:end domain-exports
27
+
28
+ mod route_metrics;
29
+
30
+ pub use route_metrics::{
31
+ build_routes_snapshot, delivery_clarke_wright_depot, delivery_element_load,
32
+ delivery_k_opt_depot, delivery_k_opt_feasible, delivery_route_capacity,
33
+ delivery_route_distance, evaluate_plan, get_delivery_route, prepare_plan,
34
+ preview_for_plan, rank_delivery_insertions, replace_delivery_route,
35
+ DeliveryInsertionCandidate, DeliveryRoutingSolution, PlanScoreComponents,
36
+ PreparedVehicleRouting, RouteLegGeometry, RouteLegSummary, RoutesSnapshot,
37
+ VehicleRouteMetrics, UNASSIGNED_DELIVERY_HARD_PENALTY,
38
+ };
39
+ }
40
+
41
+ #[cfg(test)]
42
+ mod plan_tests;
src/domain/plan.rs ADDED
@@ -0,0 +1,169 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ //! Planning solution for the delivery-routing problem.
2
+ //!
3
+ //! The `Plan` owns facts, planning entities, score, routing mode, and transient
4
+ //! prepared route matrices. It is both the solver input and the shape serialized
5
+ //! through the API after `PlanDto` flattens it.
6
+
7
+ use std::collections::HashMap;
8
+ use std::sync::Arc;
9
+
10
+ use serde::{Deserialize, Serialize};
11
+ use solverforge::cvrp::ProblemData;
12
+ use solverforge::prelude::*;
13
+
14
+ // @solverforge:begin solution-imports
15
+ use super::route_metrics::preview_for_plan;
16
+ use super::{Delivery, PlanViewState, RoutingMode, Vehicle};
17
+ // @solverforge:end solution-imports
18
+
19
+ #[planning_solution(
20
+ constraints = "crate::constraints::create_constraints",
21
+ solver_toml = "../../solver.toml"
22
+ )]
23
+ #[shadow_variable_updates(
24
+ list_owner = "vehicles",
25
+ post_update_listener = "refresh_vehicle_route_shadows"
26
+ )]
27
+ #[derive(Serialize, Deserialize)]
28
+ #[serde(rename_all = "camelCase")]
29
+ pub struct Plan {
30
+ pub name: String,
31
+ #[serde(default)]
32
+ pub routing_mode: RoutingMode,
33
+ #[serde(default)]
34
+ pub view_state: PlanViewState,
35
+ // @solverforge:begin solution-collections
36
+ #[problem_fact_collection]
37
+ pub deliveries: Vec<Delivery>,
38
+ #[planning_entity_collection]
39
+ pub vehicles: Vec<Vehicle>,
40
+ // @solverforge:end solution-collections
41
+ #[planning_score]
42
+ pub score: Option<HardSoftScore>,
43
+ /// Transient CVRP matrices shared with SolverForge list-variable hooks.
44
+ ///
45
+ /// This is skipped in JSON because it is rebuilt from coordinates before a
46
+ /// solve or route-geometry request.
47
+ #[serde(skip, default)]
48
+ pub prepared_problem_data: Vec<Arc<ProblemData>>,
49
+ }
50
+
51
+ impl Plan {
52
+ /// Builds a normalized plan from facts and route-owning vehicles.
53
+ pub fn new(name: impl Into<String>, deliveries: Vec<Delivery>, vehicles: Vec<Vehicle>) -> Self {
54
+ let mut plan = Self {
55
+ name: name.into(),
56
+ routing_mode: RoutingMode::default(),
57
+ view_state: PlanViewState::default(),
58
+ deliveries,
59
+ vehicles,
60
+ score: None,
61
+ prepared_problem_data: Vec::new(),
62
+ };
63
+ plan.normalize();
64
+ plan
65
+ }
66
+
67
+ /// Reassigns dense ids and clears transient routing caches after decoding.
68
+ ///
69
+ /// SolverForge list variables store delivery indexes. If transport data
70
+ /// arrived with older public ids, this maps route entries back onto the
71
+ /// current dense delivery positions before scoring.
72
+ pub fn normalize(&mut self) {
73
+ let delivery_id_map: HashMap<usize, usize> = self
74
+ .deliveries
75
+ .iter()
76
+ .enumerate()
77
+ .map(|(idx, delivery)| (delivery.id, idx))
78
+ .collect();
79
+
80
+ for (idx, delivery) in self.deliveries.iter_mut().enumerate() {
81
+ delivery.id = idx;
82
+ }
83
+
84
+ for (idx, vehicle) in self.vehicles.iter_mut().enumerate() {
85
+ vehicle.id = idx;
86
+ vehicle.prepared_routing = None;
87
+ vehicle.delivery_order = vehicle
88
+ .delivery_order
89
+ .iter()
90
+ .filter_map(|old_id| delivery_id_map.get(old_id).copied())
91
+ .collect();
92
+ vehicle.refresh_route_shadows();
93
+ }
94
+ self.prepared_problem_data.clear();
95
+ }
96
+
97
+ /// Removes one delivery id from every route before insertion previewing.
98
+ pub fn remove_delivery_assignments(&mut self, delivery_id: usize) {
99
+ for vehicle in &mut self.vehicles {
100
+ vehicle
101
+ .delivery_order
102
+ .retain(|assigned| *assigned != delivery_id);
103
+ }
104
+ }
105
+
106
+ /// List-variable post-update hook used by SolverForge shadow variables.
107
+ pub fn refresh_vehicle_route_shadows(&mut self, vehicle_idx: usize) {
108
+ if let Some(vehicle) = self.vehicles.get_mut(vehicle_idx) {
109
+ vehicle.refresh_route_shadows();
110
+ }
111
+ }
112
+
113
+ /// Clones the plan and attaches the UI-facing route preview.
114
+ pub fn refreshed_for_transport(&self) -> Self {
115
+ let mut plan = self.clone();
116
+ plan.view_state.preview = Some(preview_for_plan(&plan));
117
+ plan
118
+ }
119
+ }
120
+
121
+ impl solverforge::cvrp::VrpSolution for Plan {
122
+ /// Gives SolverForge's CVRP move selectors access to prepared matrices.
123
+ fn vehicle_data_ptr(&self, entity_idx: usize) -> *const ProblemData {
124
+ self.vehicles[entity_idx]
125
+ .prepared_routing
126
+ .as_ref()
127
+ .and_then(|prepared| self.prepared_problem_data.get(prepared.problem_data_index))
128
+ .map(Arc::as_ptr)
129
+ .unwrap_or(std::ptr::null())
130
+ }
131
+
132
+ /// Reads the mutable list variable for one vehicle as visit ids.
133
+ fn vehicle_visits(&self, entity_idx: usize) -> &[usize] {
134
+ &self.vehicles[entity_idx].delivery_order
135
+ }
136
+
137
+ /// Lets CVRP construction and local-search hooks replace one vehicle route.
138
+ fn vehicle_visits_mut(&mut self, entity_idx: usize) -> &mut Vec<usize> {
139
+ &mut self.vehicles[entity_idx].delivery_order
140
+ }
141
+
142
+ /// Reports the number of route-owning planning entities.
143
+ fn vehicle_count(&self) -> usize {
144
+ self.vehicles.len()
145
+ }
146
+ }
147
+
148
+ #[cfg(test)]
149
+ impl Plan {
150
+ pub(crate) fn test_has_list_variable() -> bool {
151
+ Self::__solverforge_has_list_variable()
152
+ }
153
+
154
+ pub(crate) fn test_total_list_entities(plan: &Self) -> usize {
155
+ Self::__solverforge_total_list_entities(plan)
156
+ }
157
+
158
+ pub(crate) fn test_total_list_elements(plan: &Self) -> usize {
159
+ Self::__solverforge_total_list_elements(plan)
160
+ }
161
+
162
+ pub(crate) fn test_is_trivial(plan: &Self) -> bool {
163
+ Self::__solverforge_is_trivial(plan)
164
+ }
165
+
166
+ pub(crate) fn test_phase_count(config: &solverforge::SolverConfig) -> usize {
167
+ Self::__solverforge_build_phases(config).phases().len()
168
+ }
169
+ }
src/domain/plan_tests.rs ADDED
@@ -0,0 +1,243 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ use super::*;
2
+ use crate::data::{generate, DemoData};
3
+ use crate::domain::{prepare_plan, DeliveryKind, UNASSIGNED_DELIVERY_HARD_PENALTY};
4
+ use solverforge::{Director, ScoreDirector, SolverConfig, SolverEvent, SolverManager};
5
+
6
+ fn tiny_plan() -> Plan {
7
+ Plan::new(
8
+ "tiny",
9
+ vec![
10
+ Delivery::new(
11
+ 0,
12
+ "A",
13
+ DeliveryKind::Residential,
14
+ (39.9526, -75.1652),
15
+ 1,
16
+ (8 * 3600, 18 * 3600),
17
+ 10 * 60,
18
+ ),
19
+ Delivery::new(
20
+ 1,
21
+ "B",
22
+ DeliveryKind::Business,
23
+ (39.9626, -75.1752),
24
+ 1,
25
+ (8 * 3600, 18 * 3600),
26
+ 10 * 60,
27
+ ),
28
+ ],
29
+ vec![Vehicle::new(0, "Van 1", 4, 39.9526, -75.1652, 8 * 3600)],
30
+ )
31
+ }
32
+
33
+ fn prepared_tiny_plan_with_route() -> Plan {
34
+ let mut plan = tiny_plan();
35
+ plan.routing_mode = RoutingMode::StraightLine;
36
+ plan.vehicles[0].delivery_order = vec![0, 1];
37
+ tokio::runtime::Builder::new_current_thread()
38
+ .enable_all()
39
+ .build()
40
+ .expect("tokio runtime")
41
+ .block_on(async {
42
+ prepare_plan(&mut plan)
43
+ .await
44
+ .expect("plan preparation should work");
45
+ });
46
+ plan
47
+ }
48
+
49
+ #[test]
50
+ fn score_director_populates_vehicle_route_shadows() {
51
+ let plan = prepared_tiny_plan_with_route();
52
+ assert_eq!(
53
+ plan.vehicles[0].route_total_demand, 0,
54
+ "prepared transport data should not eagerly populate solver shadows"
55
+ );
56
+
57
+ let mut director = ScoreDirector::with_descriptor(
58
+ plan,
59
+ crate::constraints::create_constraints(),
60
+ Plan::descriptor(),
61
+ Plan::entity_count,
62
+ );
63
+ let score = director.calculate_score();
64
+ let vehicle = &director.working_solution().vehicles[0];
65
+
66
+ assert_eq!(vehicle.total_assigned_demand(), 2);
67
+ assert_eq!(vehicle.capacity_overage(), 0);
68
+ assert!(
69
+ vehicle.total_travel_seconds() > 0,
70
+ "route travel should be maintained as a shadow value"
71
+ );
72
+ assert_eq!(score.hard(), 0);
73
+ }
74
+
75
+ #[test]
76
+ fn vehicle_route_shadows_refresh_after_list_variable_changes() {
77
+ let plan = prepared_tiny_plan_with_route();
78
+ let mut director = ScoreDirector::with_descriptor(
79
+ plan,
80
+ crate::constraints::create_constraints(),
81
+ Plan::descriptor(),
82
+ Plan::entity_count,
83
+ );
84
+ director.calculate_score();
85
+ assert_eq!(
86
+ director.working_solution().vehicles[0].total_assigned_demand(),
87
+ 2
88
+ );
89
+
90
+ director.before_variable_changed(0, 0);
91
+ director.working_solution_mut().vehicles[0]
92
+ .delivery_order
93
+ .clear();
94
+ director.after_variable_changed(0, 0);
95
+ let score = director.calculate_score();
96
+
97
+ let vehicle = &director.working_solution().vehicles[0];
98
+ assert_eq!(vehicle.total_assigned_demand(), 0);
99
+ assert_eq!(vehicle.total_travel_seconds(), 0);
100
+ assert_eq!(vehicle.time_window_violation_seconds(), 0);
101
+ assert_eq!(score.hard(), -(2 * UNASSIGNED_DELIVERY_HARD_PENALTY));
102
+ }
103
+
104
+ #[test]
105
+ fn generated_list_runtime_is_non_trivial_and_builds_routes() {
106
+ static MANAGER: SolverManager<Plan> = SolverManager::new();
107
+
108
+ let mut plan = tiny_plan();
109
+ plan.routing_mode = RoutingMode::StraightLine;
110
+ tokio::runtime::Builder::new_current_thread()
111
+ .enable_all()
112
+ .build()
113
+ .expect("tokio runtime")
114
+ .block_on(async {
115
+ prepare_plan(&mut plan)
116
+ .await
117
+ .expect("plan preparation should work");
118
+ });
119
+
120
+ assert!(
121
+ Plan::test_has_list_variable(),
122
+ "delivery plan should expose a list variable"
123
+ );
124
+ assert_eq!(Plan::test_total_list_entities(&plan), 1);
125
+ assert_eq!(Plan::test_total_list_elements(&plan), 2);
126
+ assert!(
127
+ !Plan::test_is_trivial(&plan),
128
+ "prepared plan should not be trivial"
129
+ );
130
+
131
+ let config =
132
+ SolverConfig::from_toml_str(include_str!("../../solver.toml")).expect("valid config");
133
+ assert_eq!(
134
+ Plan::test_phase_count(&config),
135
+ 3,
136
+ "expected Clarke-Wright construction + list k-opt + local search"
137
+ );
138
+
139
+ let (job_id, mut receiver) = MANAGER.solve(plan).expect("solve should start");
140
+ let mut saw_non_empty_best = false;
141
+ loop {
142
+ match receiver
143
+ .blocking_recv()
144
+ .expect("event stream should reach a terminal event")
145
+ {
146
+ SolverEvent::BestSolution { solution, .. } => {
147
+ if solution
148
+ .vehicles
149
+ .iter()
150
+ .any(|vehicle| !vehicle.delivery_order.is_empty())
151
+ {
152
+ saw_non_empty_best = true;
153
+ MANAGER.cancel(job_id).expect("job cancel should succeed");
154
+ }
155
+ }
156
+ SolverEvent::Completed { .. } | SolverEvent::Cancelled { .. } => break,
157
+ SolverEvent::Failed { error, .. } => {
158
+ panic!("solve unexpectedly failed: {error}");
159
+ }
160
+ SolverEvent::Progress { .. }
161
+ | SolverEvent::PauseRequested { .. }
162
+ | SolverEvent::Paused { .. }
163
+ | SolverEvent::Resumed { .. } => {}
164
+ }
165
+ }
166
+ MANAGER
167
+ .delete(job_id)
168
+ .expect("completed test job should delete");
169
+
170
+ assert!(
171
+ saw_non_empty_best,
172
+ "expected a non-empty best solution before cancellation"
173
+ );
174
+ }
175
+
176
+ #[test]
177
+ fn seeded_philadelphia_plan_emits_a_non_empty_best_solution() {
178
+ static MANAGER: SolverManager<Plan> = SolverManager::new();
179
+
180
+ let mut plan = generate(DemoData::Philadelphia);
181
+ plan.routing_mode = RoutingMode::StraightLine;
182
+ tokio::runtime::Builder::new_current_thread()
183
+ .enable_all()
184
+ .build()
185
+ .expect("tokio runtime")
186
+ .block_on(async {
187
+ prepare_plan(&mut plan)
188
+ .await
189
+ .expect("plan preparation should work");
190
+ });
191
+
192
+ let (job_id, mut receiver) = MANAGER.solve(plan).expect("solve should start");
193
+ let mut saw_non_empty_best = false;
194
+ let mut first_non_empty_best: Option<Plan> = None;
195
+ loop {
196
+ match receiver
197
+ .blocking_recv()
198
+ .expect("event stream should reach a terminal event")
199
+ {
200
+ SolverEvent::BestSolution { solution, .. } => {
201
+ if solution
202
+ .vehicles
203
+ .iter()
204
+ .any(|vehicle| !vehicle.delivery_order.is_empty())
205
+ {
206
+ saw_non_empty_best = true;
207
+ first_non_empty_best.get_or_insert(solution.clone());
208
+ MANAGER.cancel(job_id).expect("job cancel should succeed");
209
+ }
210
+ }
211
+ SolverEvent::Completed { .. } | SolverEvent::Cancelled { .. } => break,
212
+ SolverEvent::Failed { error, .. } => {
213
+ panic!("solve unexpectedly failed: {error}");
214
+ }
215
+ SolverEvent::Progress { .. }
216
+ | SolverEvent::PauseRequested { .. }
217
+ | SolverEvent::Paused { .. }
218
+ | SolverEvent::Resumed { .. } => {}
219
+ }
220
+ }
221
+ MANAGER
222
+ .delete(job_id)
223
+ .expect("completed test job should delete");
224
+
225
+ assert!(
226
+ saw_non_empty_best,
227
+ "expected a non-empty best solution for the seeded Philadelphia plan"
228
+ );
229
+ let best = first_non_empty_best.expect("should retain the first non-empty best solution");
230
+ assert!(
231
+ best.vehicles
232
+ .iter()
233
+ .any(|vehicle| vehicle.delivery_order.len() > 1),
234
+ "expected at least one multi-stop route after construction"
235
+ );
236
+ let director = ScoreDirector::with_descriptor(
237
+ best.clone(),
238
+ crate::constraints::create_constraints(),
239
+ Plan::descriptor(),
240
+ Plan::entity_count,
241
+ );
242
+ assert_eq!(director.entity_count(0), Some(best.vehicles.len()));
243
+ }
src/domain/preview.rs ADDED
@@ -0,0 +1,105 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ //! Browser preview structs embedded in `Plan.view_state`.
2
+ //!
3
+ //! These values are derived from the domain plan before transport. They let the
4
+ //! frontend render route summaries and timelines without duplicating every
5
+ //! scoring rule.
6
+
7
+ use serde::{Deserialize, Serialize};
8
+
9
+ /// Chooses whether routing uses fast straight lines or real road-network data.
10
+ #[derive(Clone, Copy, Debug, Default, Eq, PartialEq, Serialize, Deserialize)]
11
+ #[serde(rename_all = "snake_case")]
12
+ pub enum RoutingMode {
13
+ StraightLine,
14
+ #[default]
15
+ RoadNetwork,
16
+ }
17
+
18
+ /// Selects which timeline rail the browser shows.
19
+ #[derive(Clone, Copy, Debug, Default, Eq, PartialEq, Serialize, Deserialize)]
20
+ #[serde(rename_all = "snake_case")]
21
+ pub enum TimelineView {
22
+ #[default]
23
+ ByVehicle,
24
+ ByDelivery,
25
+ }
26
+
27
+ /// UI-only state that travels with the plan between browser and backend.
28
+ #[derive(Clone, Debug, Default, Serialize, Deserialize)]
29
+ #[serde(rename_all = "camelCase")]
30
+ pub struct PlanViewState {
31
+ #[serde(default)]
32
+ pub timeline_view: TimelineView,
33
+ pub selected_vehicle_id: Option<usize>,
34
+ pub selected_delivery_id: Option<usize>,
35
+ #[serde(default)]
36
+ pub preview: Option<PlanPreview>,
37
+ }
38
+
39
+ /// Aggregate route and score preview for the full plan.
40
+ #[derive(Clone, Debug, Default, Serialize, Deserialize)]
41
+ #[serde(rename_all = "camelCase")]
42
+ pub struct PlanPreview {
43
+ pub hard_score: i64,
44
+ pub soft_score: i64,
45
+ pub unassigned_delivery_ids: Vec<usize>,
46
+ pub vehicles: Vec<VehiclePreview>,
47
+ pub deliveries: Vec<DeliveryPreview>,
48
+ }
49
+
50
+ /// Per-vehicle route summary used by cards, lists, and timelines.
51
+ #[derive(Clone, Debug, Default, Serialize, Deserialize)]
52
+ #[serde(rename_all = "camelCase")]
53
+ pub struct VehiclePreview {
54
+ pub vehicle_id: usize,
55
+ pub vehicle_name: String,
56
+ pub total_demand: i32,
57
+ pub capacity_overage: i32,
58
+ pub stop_count: usize,
59
+ pub total_travel_seconds: i64,
60
+ pub total_wait_seconds: i64,
61
+ pub total_service_seconds: i64,
62
+ pub total_late_seconds: i64,
63
+ pub start_time: i64,
64
+ pub end_time: i64,
65
+ pub stops: Vec<VehiclePreviewStop>,
66
+ }
67
+
68
+ /// One delivery stop on a vehicle timeline.
69
+ #[derive(Clone, Debug, Default, Serialize, Deserialize)]
70
+ #[serde(rename_all = "camelCase")]
71
+ pub struct VehiclePreviewStop {
72
+ pub delivery_id: usize,
73
+ pub label: String,
74
+ pub kind: String,
75
+ pub sequence: usize,
76
+ pub demand: i32,
77
+ pub min_start_time: i64,
78
+ pub max_end_time: i64,
79
+ pub arrival_time: i64,
80
+ pub service_start_time: i64,
81
+ pub departure_time: i64,
82
+ pub travel_seconds_from_previous: i64,
83
+ pub wait_seconds: i64,
84
+ pub late_seconds: i64,
85
+ }
86
+
87
+ /// Per-delivery assignment summary used by data tables and timelines.
88
+ #[derive(Clone, Debug, Default, Serialize, Deserialize)]
89
+ #[serde(rename_all = "camelCase")]
90
+ pub struct DeliveryPreview {
91
+ pub delivery_id: usize,
92
+ pub label: String,
93
+ pub kind: String,
94
+ pub demand: i32,
95
+ pub min_start_time: i64,
96
+ pub max_end_time: i64,
97
+ pub service_duration: i64,
98
+ pub assigned_vehicle_id: Option<usize>,
99
+ pub assigned_vehicle_name: Option<String>,
100
+ pub sequence: Option<usize>,
101
+ pub arrival_time: Option<i64>,
102
+ pub service_start_time: Option<i64>,
103
+ pub departure_time: Option<i64>,
104
+ pub late_seconds: Option<i64>,
105
+ }
src/domain/route_metrics/cvrp_hooks.rs ADDED
@@ -0,0 +1,185 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ use solverforge_maps::UNREACHABLE;
2
+
3
+ use crate::domain::Plan;
4
+
5
+ use super::helpers::{
6
+ fallback_delivery_to_delivery, fallback_delivery_to_vehicle, fallback_vehicle_to_delivery,
7
+ normalized_travel_time,
8
+ };
9
+ use super::types::DeliveryRoutingSolution;
10
+
11
+ /// Depot token used by Clarke-Wright before it commits visits to vehicle routes.
12
+ pub fn delivery_clarke_wright_depot<S: DeliveryRoutingSolution>(solution: &S) -> usize {
13
+ virtual_depot_value(solution.delivery_plan(), 0)
14
+ }
15
+
16
+ /// Per-vehicle depot token used by k-opt route reconnection.
17
+ pub fn delivery_k_opt_depot<S: DeliveryRoutingSolution>(solution: &S, entity_idx: usize) -> usize {
18
+ virtual_depot_value(solution.delivery_plan(), entity_idx)
19
+ }
20
+
21
+ /// Reads travel time between delivery ids and virtual depot ids.
22
+ ///
23
+ /// Virtual depot ids are `deliveries.len() + vehicle_idx`. Prepared route
24
+ /// matrices are preferred; straight-line fallback keeps previews and tests
25
+ /// usable before full map-backed preparation has run.
26
+ pub fn delivery_route_distance<S: DeliveryRoutingSolution>(
27
+ solution: &S,
28
+ from: usize,
29
+ to: usize,
30
+ ) -> i64 {
31
+ if from == to {
32
+ return 0;
33
+ }
34
+
35
+ let plan = solution.delivery_plan();
36
+ match (
37
+ virtual_depot_entity(plan, from),
38
+ virtual_depot_entity(plan, to),
39
+ plan.deliveries.get(from),
40
+ plan.deliveries.get(to),
41
+ ) {
42
+ (Some(_), Some(_), _, _) => 0,
43
+ (Some(vehicle_idx), None, _, Some(_)) => depot_to_delivery_seconds(plan, vehicle_idx, to),
44
+ (None, Some(vehicle_idx), Some(_), _) => delivery_to_depot_seconds(plan, vehicle_idx, from),
45
+ (None, None, Some(_), Some(_)) => delivery_to_delivery_seconds(plan, from, to),
46
+ _ => 0,
47
+ }
48
+ }
49
+
50
+ pub fn delivery_element_load<S: DeliveryRoutingSolution>(solution: &S, delivery_id: usize) -> i64 {
51
+ solution
52
+ .delivery_plan()
53
+ .deliveries
54
+ .get(delivery_id)
55
+ .map_or(0, |delivery| i64::from(delivery.demand))
56
+ }
57
+
58
+ /// Conservative route capacity used by list construction when assigning visits.
59
+ pub fn delivery_route_capacity<S: DeliveryRoutingSolution>(solution: &S) -> i64 {
60
+ let plan = solution.delivery_plan();
61
+ plan.deliveries
62
+ .iter()
63
+ .map(|delivery| i64::from(delivery.demand))
64
+ .sum::<i64>()
65
+ .max(1)
66
+ }
67
+
68
+ /// Gives list-aware move selectors an owned copy of one vehicle route.
69
+ pub fn get_delivery_route<S: DeliveryRoutingSolution>(solution: &S, entity_idx: usize) -> Vec<usize> {
70
+ solution.vehicle_visits(entity_idx).to_vec()
71
+ }
72
+
73
+ /// Replaces one route after construction or a k-opt/list move accepts it.
74
+ pub fn replace_delivery_route<S: DeliveryRoutingSolution>(
75
+ solution: &mut S,
76
+ entity_idx: usize,
77
+ route: Vec<usize>,
78
+ ) {
79
+ *solution.vehicle_visits_mut(entity_idx) = route;
80
+ }
81
+
82
+ /// Guards k-opt proposals with the same route feasibility signals the app scores.
83
+ ///
84
+ /// The hook rejects unknown delivery ids, unreachable legs, and routes that
85
+ /// would miss a delivery time window after travel, waiting, and service time.
86
+ pub fn delivery_k_opt_feasible<S: DeliveryRoutingSolution>(
87
+ solution: &S,
88
+ entity_idx: usize,
89
+ route: &[usize],
90
+ ) -> bool {
91
+ let plan = solution.delivery_plan();
92
+ let Some(prepared) = plan
93
+ .vehicles
94
+ .get(entity_idx)
95
+ .and_then(|vehicle| vehicle.prepared_routing.as_ref())
96
+ else {
97
+ return true;
98
+ };
99
+
100
+ let mut current = prepared.vehicle_departure_time;
101
+ let mut previous: Option<usize> = None;
102
+
103
+ for &delivery_id in route {
104
+ if delivery_id >= prepared.demands.len() {
105
+ return false;
106
+ }
107
+ let travel = match previous {
108
+ Some(previous_id) => prepared.travel_times[previous_id][delivery_id],
109
+ None => prepared.depot_to_delivery_seconds[delivery_id],
110
+ };
111
+ if travel == UNREACHABLE {
112
+ return false;
113
+ }
114
+
115
+ current += normalized_travel_time(travel);
116
+ let (min_start, max_end) = prepared.time_windows[delivery_id];
117
+ if current < min_start {
118
+ current = min_start;
119
+ }
120
+
121
+ current += prepared.service_durations[delivery_id];
122
+ if current > max_end {
123
+ return false;
124
+ }
125
+ previous = Some(delivery_id);
126
+ }
127
+
128
+ true
129
+ }
130
+
131
+ fn virtual_depot_value(plan: &Plan, entity_idx: usize) -> usize {
132
+ plan.deliveries.len().saturating_add(entity_idx)
133
+ }
134
+
135
+ fn virtual_depot_entity(plan: &Plan, value: usize) -> Option<usize> {
136
+ value
137
+ .checked_sub(plan.deliveries.len())
138
+ .filter(|&entity_idx| entity_idx < plan.vehicles.len())
139
+ }
140
+
141
+ fn depot_to_delivery_seconds(plan: &Plan, vehicle_idx: usize, delivery_id: usize) -> i64 {
142
+ plan.vehicles
143
+ .get(vehicle_idx)
144
+ .and_then(|vehicle| vehicle.prepared_routing.as_ref())
145
+ .and_then(|prepared| {
146
+ prepared
147
+ .depot_to_delivery_seconds
148
+ .get(delivery_id)
149
+ .copied()
150
+ })
151
+ .map(normalized_travel_time)
152
+ .or_else(|| fallback_vehicle_to_delivery(plan, vehicle_idx, delivery_id))
153
+ .unwrap_or(0)
154
+ }
155
+
156
+ fn delivery_to_depot_seconds(plan: &Plan, vehicle_idx: usize, delivery_id: usize) -> i64 {
157
+ plan.vehicles
158
+ .get(vehicle_idx)
159
+ .and_then(|vehicle| vehicle.prepared_routing.as_ref())
160
+ .and_then(|prepared| {
161
+ prepared
162
+ .delivery_to_depot_seconds
163
+ .get(delivery_id)
164
+ .copied()
165
+ })
166
+ .map(normalized_travel_time)
167
+ .or_else(|| fallback_delivery_to_vehicle(plan, vehicle_idx, delivery_id))
168
+ .unwrap_or(0)
169
+ }
170
+
171
+ fn delivery_to_delivery_seconds(plan: &Plan, from: usize, to: usize) -> i64 {
172
+ plan.vehicles
173
+ .iter()
174
+ .find_map(|vehicle| vehicle.prepared_routing.as_ref())
175
+ .and_then(|prepared| {
176
+ prepared
177
+ .travel_times
178
+ .get(from)
179
+ .and_then(|row| row.get(to))
180
+ .copied()
181
+ })
182
+ .map(normalized_travel_time)
183
+ .or_else(|| fallback_delivery_to_delivery(plan, from, to))
184
+ .unwrap_or(0)
185
+ }
src/domain/route_metrics/helpers.rs ADDED
@@ -0,0 +1,106 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ use solverforge_maps::{haversine_distance, BoundingBox, Coord, RoutingError, UNREACHABLE};
2
+
3
+ use crate::domain::{Delivery, Plan};
4
+
5
+ use super::types::RouteBounds;
6
+
7
+ const DEFAULT_SPEED_KMPH: f64 = 50.0;
8
+
9
+ pub(super) fn meters_to_seconds(meters: i64) -> i64 {
10
+ let meters_per_second = DEFAULT_SPEED_KMPH * 1000.0 / 3600.0;
11
+ (meters as f64 / meters_per_second).round() as i64
12
+ }
13
+
14
+ pub(super) fn normalized_travel_time(seconds: i64) -> i64 {
15
+ if seconds == UNREACHABLE {
16
+ super::types::UNREACHABLE_HARD_PENALTY
17
+ } else {
18
+ seconds.max(0)
19
+ }
20
+ }
21
+
22
+ pub(super) fn normalized_distance(meters: i64) -> i64 {
23
+ if meters == UNREACHABLE {
24
+ 0
25
+ } else {
26
+ meters.max(0)
27
+ }
28
+ }
29
+
30
+ pub(super) fn build_delivery_distance_matrix(delivery_coords: &[Coord]) -> Vec<Vec<i64>> {
31
+ delivery_coords
32
+ .iter()
33
+ .map(|from| {
34
+ delivery_coords
35
+ .iter()
36
+ .map(|to| haversine_distance(*from, *to).round() as i64)
37
+ .collect()
38
+ })
39
+ .collect()
40
+ }
41
+
42
+ pub(super) fn build_travel_time_matrix(delivery_coords: &[Coord]) -> Vec<Vec<i64>> {
43
+ delivery_coords
44
+ .iter()
45
+ .map(|from| {
46
+ delivery_coords
47
+ .iter()
48
+ .map(|to| meters_to_seconds(haversine_distance(*from, *to).round() as i64))
49
+ .collect()
50
+ })
51
+ .collect()
52
+ }
53
+
54
+ pub(super) fn delivery_coords(plan: &Plan) -> Result<Vec<Coord>, RoutingError> {
55
+ plan.deliveries
56
+ .iter()
57
+ .map(Delivery::coord)
58
+ .collect::<Result<Vec<_>, _>>()
59
+ }
60
+
61
+ pub(super) fn route_bounds(plan: &Plan) -> Result<Option<RouteBounds>, RoutingError> {
62
+ if plan.deliveries.is_empty() && plan.vehicles.is_empty() {
63
+ return Ok(None);
64
+ }
65
+
66
+ let mut coords = delivery_coords(plan)?;
67
+ for vehicle in &plan.vehicles {
68
+ coords.push(vehicle.depot_coord()?);
69
+ }
70
+ let bbox = BoundingBox::from_coords(&coords);
71
+ Ok(Some(RouteBounds {
72
+ south_west: [bbox.min_lat, bbox.min_lng],
73
+ north_east: [bbox.max_lat, bbox.max_lng],
74
+ }))
75
+ }
76
+
77
+ pub(super) fn straight_line_leg(from: Coord, to: Coord) -> (i64, i64) {
78
+ let meters = haversine_distance(from, to).round() as i64;
79
+ (meters_to_seconds(meters), meters)
80
+ }
81
+
82
+ pub(super) fn fallback_vehicle_to_delivery(
83
+ plan: &Plan,
84
+ vehicle_idx: usize,
85
+ delivery_id: usize,
86
+ ) -> Option<i64> {
87
+ let depot = plan.vehicles.get(vehicle_idx)?.depot_coord().ok()?;
88
+ let delivery = plan.deliveries.get(delivery_id)?.coord().ok()?;
89
+ Some(straight_line_leg(depot, delivery).0)
90
+ }
91
+
92
+ pub(super) fn fallback_delivery_to_vehicle(
93
+ plan: &Plan,
94
+ vehicle_idx: usize,
95
+ delivery_id: usize,
96
+ ) -> Option<i64> {
97
+ let delivery = plan.deliveries.get(delivery_id)?.coord().ok()?;
98
+ let depot = plan.vehicles.get(vehicle_idx)?.depot_coord().ok()?;
99
+ Some(straight_line_leg(delivery, depot).0)
100
+ }
101
+
102
+ pub(super) fn fallback_delivery_to_delivery(plan: &Plan, from: usize, to: usize) -> Option<i64> {
103
+ let from = plan.deliveries.get(from)?.coord().ok()?;
104
+ let to = plan.deliveries.get(to)?.coord().ok()?;
105
+ Some(straight_line_leg(from, to).0)
106
+ }
src/domain/route_metrics/insertions.rs ADDED
@@ -0,0 +1,66 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ use std::cmp::Ordering;
2
+
3
+ use solverforge_maps::RoutingError;
4
+
5
+ use crate::domain::Plan;
6
+
7
+ use super::preparation::prepare_plan;
8
+ use super::scoring::evaluate_plan;
9
+ use super::types::DeliveryInsertionCandidate;
10
+
11
+ /// Ranks interactive insertion positions without running a full solve.
12
+ ///
13
+ /// The selected delivery is removed from the current plan, the same routing
14
+ /// data used by solving is prepared, then every vehicle/position insertion is
15
+ /// preview-scored and sorted by feasibility first and travel quality second.
16
+ pub async fn rank_delivery_insertions(
17
+ plan: &Plan,
18
+ delivery_id: usize,
19
+ limit: usize,
20
+ ) -> Result<Vec<DeliveryInsertionCandidate>, RoutingError> {
21
+ let mut base_plan = plan.clone();
22
+ base_plan.normalize();
23
+ base_plan.remove_delivery_assignments(delivery_id);
24
+ prepare_plan(&mut base_plan).await?;
25
+
26
+ let base_score = evaluate_plan(&base_plan);
27
+ let mut candidates = Vec::new();
28
+
29
+ for vehicle_idx in 0..base_plan.vehicles.len() {
30
+ let vehicle_name = base_plan.vehicles[vehicle_idx].name.clone();
31
+ for insert_index in 0..=base_plan.vehicles[vehicle_idx].delivery_order.len() {
32
+ let mut candidate_plan = base_plan.clone();
33
+ candidate_plan.vehicles[vehicle_idx]
34
+ .delivery_order
35
+ .insert(insert_index, delivery_id);
36
+ let score = evaluate_plan(&candidate_plan);
37
+
38
+ candidates.push(DeliveryInsertionCandidate {
39
+ vehicle_id: candidate_plan.vehicles[vehicle_idx].id,
40
+ vehicle_name: vehicle_name.clone(),
41
+ insert_index,
42
+ hard_score: score.hard_score(),
43
+ soft_score: score.soft_score(),
44
+ delta_hard: score.hard_score() - base_score.hard_score(),
45
+ delta_soft: score.soft_score() - base_score.soft_score(),
46
+ preview_plan: candidate_plan,
47
+ });
48
+ }
49
+ }
50
+
51
+ candidates.sort_by(compare_candidates);
52
+ candidates.truncate(limit);
53
+ Ok(candidates)
54
+ }
55
+
56
+ fn compare_candidates(
57
+ left: &DeliveryInsertionCandidate,
58
+ right: &DeliveryInsertionCandidate,
59
+ ) -> Ordering {
60
+ right
61
+ .hard_score
62
+ .cmp(&left.hard_score)
63
+ .then_with(|| right.soft_score.cmp(&left.soft_score))
64
+ .then_with(|| left.insert_index.cmp(&right.insert_index))
65
+ .then_with(|| left.vehicle_id.cmp(&right.vehicle_id))
66
+ }
src/domain/route_metrics/metrics.rs ADDED
@@ -0,0 +1,144 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ use solverforge_maps::UNREACHABLE;
2
+
3
+ use crate::domain::{Plan, Vehicle};
4
+
5
+ use super::helpers::{normalized_distance, normalized_travel_time, straight_line_leg};
6
+ use super::types::{RouteStopMetrics, VehicleRouteMetrics};
7
+
8
+ pub(super) fn metrics_for_vehicle(plan: &Plan, vehicle: &Vehicle) -> VehicleRouteMetrics {
9
+ let mut metrics = VehicleRouteMetrics {
10
+ vehicle_id: vehicle.id,
11
+ start_time: vehicle.departure_time,
12
+ end_time: vehicle.departure_time,
13
+ ..VehicleRouteMetrics::default()
14
+ };
15
+
16
+ let mut current_time = vehicle.departure_time;
17
+ let mut previous_delivery_id = None;
18
+
19
+ for (sequence, &delivery_id) in vehicle.delivery_order.iter().enumerate() {
20
+ let Some(delivery) = plan.deliveries.get(delivery_id) else {
21
+ continue;
22
+ };
23
+
24
+ metrics.total_demand += delivery.demand;
25
+ let (travel_seconds, travel_meters, unreachable) =
26
+ leg_from_previous(plan, vehicle, previous_delivery_id, delivery_id);
27
+ metrics.total_travel_seconds += travel_seconds;
28
+ metrics.total_distance_meters += travel_meters;
29
+ metrics.unreachable_legs += usize::from(unreachable);
30
+
31
+ let arrival_time = current_time.saturating_add(travel_seconds);
32
+ let service_start_time = arrival_time.max(delivery.min_start_time);
33
+ let wait_seconds = service_start_time.saturating_sub(arrival_time);
34
+ let departure_time = service_start_time.saturating_add(delivery.service_duration);
35
+ let late_seconds = departure_time.saturating_sub(delivery.max_end_time).max(0);
36
+
37
+ metrics.total_wait_seconds += wait_seconds;
38
+ metrics.total_service_seconds += delivery.service_duration;
39
+ metrics.total_late_seconds += late_seconds;
40
+ metrics.end_time = departure_time;
41
+ metrics.stops.push(RouteStopMetrics {
42
+ delivery_id,
43
+ sequence,
44
+ arrival_time,
45
+ service_start_time,
46
+ departure_time,
47
+ travel_seconds_from_previous: travel_seconds,
48
+ wait_seconds,
49
+ late_seconds,
50
+ });
51
+
52
+ current_time = departure_time;
53
+ previous_delivery_id = Some(delivery_id);
54
+ }
55
+
56
+ if let Some(last_delivery_id) = previous_delivery_id {
57
+ let (return_seconds, return_meters, unreachable) =
58
+ leg_to_depot(plan, vehicle, last_delivery_id);
59
+ metrics.total_travel_seconds += return_seconds;
60
+ metrics.total_distance_meters += return_meters;
61
+ metrics.unreachable_legs += usize::from(unreachable);
62
+ metrics.end_time = metrics.end_time.saturating_add(return_seconds);
63
+ }
64
+
65
+ metrics.capacity_overage = (metrics.total_demand - vehicle.capacity).max(0);
66
+ metrics
67
+ }
68
+
69
+ fn leg_from_previous(
70
+ plan: &Plan,
71
+ vehicle: &Vehicle,
72
+ previous_delivery_id: Option<usize>,
73
+ current_delivery_id: usize,
74
+ ) -> (i64, i64, bool) {
75
+ match previous_delivery_id {
76
+ Some(previous_delivery_id) => prepared_or_straight_line_delivery_leg(
77
+ plan,
78
+ vehicle,
79
+ previous_delivery_id,
80
+ current_delivery_id,
81
+ ),
82
+ None => prepared_or_straight_line_depot_leg(plan, vehicle, current_delivery_id),
83
+ }
84
+ }
85
+
86
+ fn prepared_or_straight_line_delivery_leg(
87
+ plan: &Plan,
88
+ vehicle: &Vehicle,
89
+ from_delivery_id: usize,
90
+ to_delivery_id: usize,
91
+ ) -> (i64, i64, bool) {
92
+ if let Some(prepared) = &vehicle.prepared_routing {
93
+ let seconds = prepared.travel_times[from_delivery_id][to_delivery_id];
94
+ let meters = prepared.distance_matrix[from_delivery_id][to_delivery_id];
95
+ return (
96
+ normalized_travel_time(seconds),
97
+ normalized_distance(meters),
98
+ seconds == UNREACHABLE,
99
+ );
100
+ }
101
+
102
+ let from = plan.deliveries[from_delivery_id].coord().expect("valid coord");
103
+ let to = plan.deliveries[to_delivery_id].coord().expect("valid coord");
104
+ let (seconds, meters) = straight_line_leg(from, to);
105
+ (seconds, meters, false)
106
+ }
107
+
108
+ fn prepared_or_straight_line_depot_leg(
109
+ plan: &Plan,
110
+ vehicle: &Vehicle,
111
+ delivery_id: usize,
112
+ ) -> (i64, i64, bool) {
113
+ if let Some(prepared) = &vehicle.prepared_routing {
114
+ let seconds = prepared.depot_to_delivery_seconds[delivery_id];
115
+ let meters = prepared.depot_to_delivery_meters[delivery_id];
116
+ return (
117
+ normalized_travel_time(seconds),
118
+ normalized_distance(meters),
119
+ seconds == UNREACHABLE,
120
+ );
121
+ }
122
+
123
+ let from = vehicle.depot_coord().expect("valid coord");
124
+ let to = plan.deliveries[delivery_id].coord().expect("valid coord");
125
+ let (seconds, meters) = straight_line_leg(from, to);
126
+ (seconds, meters, false)
127
+ }
128
+
129
+ fn leg_to_depot(plan: &Plan, vehicle: &Vehicle, delivery_id: usize) -> (i64, i64, bool) {
130
+ if let Some(prepared) = &vehicle.prepared_routing {
131
+ let seconds = prepared.delivery_to_depot_seconds[delivery_id];
132
+ let meters = prepared.delivery_to_depot_meters[delivery_id];
133
+ return (
134
+ normalized_travel_time(seconds),
135
+ normalized_distance(meters),
136
+ seconds == UNREACHABLE,
137
+ );
138
+ }
139
+
140
+ let from = plan.deliveries[delivery_id].coord().expect("valid coord");
141
+ let to = vehicle.depot_coord().expect("valid coord");
142
+ let (seconds, meters) = straight_line_leg(from, to);
143
+ (seconds, meters, false)
144
+ }
src/domain/route_metrics/mod.rs ADDED
@@ -0,0 +1,26 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ mod cvrp_hooks;
2
+ mod helpers;
3
+ mod insertions;
4
+ mod metrics;
5
+ mod preparation;
6
+ mod routes;
7
+ mod scoring;
8
+ mod types;
9
+
10
+ pub use cvrp_hooks::{
11
+ delivery_clarke_wright_depot, delivery_element_load, delivery_k_opt_depot,
12
+ delivery_k_opt_feasible, delivery_route_capacity, delivery_route_distance, get_delivery_route,
13
+ replace_delivery_route,
14
+ };
15
+ pub use insertions::rank_delivery_insertions;
16
+ pub use preparation::prepare_plan;
17
+ pub use routes::build_routes_snapshot;
18
+ pub use scoring::{evaluate_plan, preview_for_plan};
19
+ pub use types::{
20
+ DeliveryInsertionCandidate, DeliveryRoutingSolution, PlanScoreComponents,
21
+ PreparedVehicleRouting, RouteLegGeometry, RouteLegSummary, RoutesSnapshot, VehicleRouteMetrics,
22
+ UNASSIGNED_DELIVERY_HARD_PENALTY,
23
+ };
24
+
25
+ #[cfg(test)]
26
+ mod tests;
src/domain/route_metrics/preparation.rs ADDED
@@ -0,0 +1,184 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ use std::sync::Arc;
2
+
3
+ use solverforge::cvrp::ProblemData;
4
+ use solverforge_maps::{
5
+ haversine_distance, BoundingBox, Coord, NetworkConfig, RoadNetwork, RoutingError, UNREACHABLE,
6
+ };
7
+
8
+ use crate::domain::{Plan, RoutingMode};
9
+
10
+ use super::helpers::{
11
+ build_delivery_distance_matrix, build_travel_time_matrix, delivery_coords, meters_to_seconds,
12
+ };
13
+ use super::types::PreparedVehicleRouting;
14
+
15
+ /// Builds the routing data SolverForge should read during a solve.
16
+ ///
17
+ /// This is the boundary between transport/domain data and the hot scoring path:
18
+ /// route ids are normalized, delivery matrices are computed once, per-vehicle
19
+ /// depot legs are attached, and the list-variable hooks can then score from
20
+ /// cached `ProblemData` instead of resolving maps during every move.
21
+ pub async fn prepare_plan(plan: &mut Plan) -> Result<(), RoutingError> {
22
+ plan.normalize();
23
+ let delivery_coords = delivery_coords(plan)?;
24
+ let delivery_distance_matrix = build_delivery_distance_matrix(&delivery_coords);
25
+ let delivery_demands: Vec<i32> = plan
26
+ .deliveries
27
+ .iter()
28
+ .map(|delivery| delivery.demand)
29
+ .collect();
30
+ let delivery_time_windows: Vec<(i64, i64)> = plan
31
+ .deliveries
32
+ .iter()
33
+ .map(|delivery| (delivery.min_start_time, delivery.max_end_time))
34
+ .collect();
35
+ let delivery_service_durations: Vec<i64> = plan
36
+ .deliveries
37
+ .iter()
38
+ .map(|delivery| delivery.service_duration)
39
+ .collect();
40
+
41
+ let depot_routing = match plan.routing_mode {
42
+ RoutingMode::StraightLine => DepotRoutingData::straight_line(plan, &delivery_coords)?,
43
+ RoutingMode::RoadNetwork => DepotRoutingData::road_network(plan, &delivery_coords).await?,
44
+ };
45
+
46
+ plan.prepared_problem_data.clear();
47
+ for (vehicle_idx, vehicle) in plan.vehicles.iter_mut().enumerate() {
48
+ plan.prepared_problem_data.push(Arc::new(ProblemData {
49
+ capacity: vehicle.capacity as i64,
50
+ depot: 0,
51
+ demands: delivery_demands.clone(),
52
+ distance_matrix: delivery_distance_matrix.clone(),
53
+ time_windows: delivery_time_windows.clone(),
54
+ service_durations: delivery_service_durations.clone(),
55
+ travel_times: depot_routing.delivery_travel_times.clone(),
56
+ vehicle_departure_time: vehicle.departure_time,
57
+ }));
58
+ vehicle.prepared_routing = Some(PreparedVehicleRouting {
59
+ problem_data_index: vehicle_idx,
60
+ capacity: vehicle.capacity as i64,
61
+ demands: delivery_demands.clone(),
62
+ distance_matrix: delivery_distance_matrix.clone(),
63
+ time_windows: delivery_time_windows.clone(),
64
+ service_durations: delivery_service_durations.clone(),
65
+ travel_times: depot_routing.delivery_travel_times.clone(),
66
+ vehicle_departure_time: vehicle.departure_time,
67
+ depot_to_delivery_seconds: depot_routing.depot_to_delivery_seconds[vehicle_idx].clone(),
68
+ delivery_to_depot_seconds: depot_routing.delivery_to_depot_seconds[vehicle_idx].clone(),
69
+ depot_to_delivery_meters: depot_routing.depot_to_delivery_meters[vehicle_idx].clone(),
70
+ delivery_to_depot_meters: depot_routing.delivery_to_depot_meters[vehicle_idx].clone(),
71
+ });
72
+ }
73
+
74
+ Ok(())
75
+ }
76
+
77
+ #[derive(Clone, Debug)]
78
+ struct DepotRoutingData {
79
+ delivery_travel_times: Vec<Vec<i64>>,
80
+ depot_to_delivery_seconds: Vec<Vec<i64>>,
81
+ delivery_to_depot_seconds: Vec<Vec<i64>>,
82
+ depot_to_delivery_meters: Vec<Vec<i64>>,
83
+ delivery_to_depot_meters: Vec<Vec<i64>>,
84
+ }
85
+
86
+ impl DepotRoutingData {
87
+ fn straight_line(plan: &Plan, delivery_coords: &[Coord]) -> Result<Self, RoutingError> {
88
+ let delivery_travel_times = build_travel_time_matrix(delivery_coords);
89
+ let mut depot_to_delivery_seconds = Vec::with_capacity(plan.vehicles.len());
90
+ let mut delivery_to_depot_seconds = Vec::with_capacity(plan.vehicles.len());
91
+ let mut depot_to_delivery_meters = Vec::with_capacity(plan.vehicles.len());
92
+ let mut delivery_to_depot_meters = Vec::with_capacity(plan.vehicles.len());
93
+
94
+ for vehicle in &plan.vehicles {
95
+ let depot = vehicle.depot_coord()?;
96
+ let mut outbound_seconds = Vec::with_capacity(delivery_coords.len());
97
+ let mut inbound_seconds = Vec::with_capacity(delivery_coords.len());
98
+ let mut outbound_meters = Vec::with_capacity(delivery_coords.len());
99
+ let mut inbound_meters = Vec::with_capacity(delivery_coords.len());
100
+ for coord in delivery_coords {
101
+ let meters = haversine_distance(depot, *coord).round() as i64;
102
+ let seconds = meters_to_seconds(meters);
103
+ outbound_seconds.push(seconds);
104
+ inbound_seconds.push(seconds);
105
+ outbound_meters.push(meters);
106
+ inbound_meters.push(meters);
107
+ }
108
+ depot_to_delivery_seconds.push(outbound_seconds);
109
+ delivery_to_depot_seconds.push(inbound_seconds);
110
+ depot_to_delivery_meters.push(outbound_meters);
111
+ delivery_to_depot_meters.push(inbound_meters);
112
+ }
113
+
114
+ Ok(Self {
115
+ delivery_travel_times,
116
+ depot_to_delivery_seconds,
117
+ delivery_to_depot_seconds,
118
+ depot_to_delivery_meters,
119
+ delivery_to_depot_meters,
120
+ })
121
+ }
122
+
123
+ async fn road_network(plan: &Plan, delivery_coords: &[Coord]) -> Result<Self, RoutingError> {
124
+ let mut all_coords = delivery_coords.to_vec();
125
+ for vehicle in &plan.vehicles {
126
+ all_coords.push(vehicle.depot_coord()?);
127
+ }
128
+
129
+ if all_coords.is_empty() {
130
+ return Ok(Self {
131
+ delivery_travel_times: Vec::new(),
132
+ depot_to_delivery_seconds: Vec::new(),
133
+ delivery_to_depot_seconds: Vec::new(),
134
+ depot_to_delivery_meters: Vec::new(),
135
+ delivery_to_depot_meters: Vec::new(),
136
+ });
137
+ }
138
+
139
+ let bbox = BoundingBox::from_coords(&all_coords).expand_for_routing(&all_coords);
140
+ let network = RoadNetwork::load_or_fetch(&bbox, &NetworkConfig::default(), None).await?;
141
+ let matrix = network.compute_matrix(&all_coords, None).await;
142
+
143
+ let delivery_count = delivery_coords.len();
144
+ let mut delivery_travel_times = vec![vec![0_i64; delivery_count]; delivery_count];
145
+ for (i, row) in delivery_travel_times.iter_mut().enumerate() {
146
+ for (j, cell) in row.iter_mut().enumerate() {
147
+ *cell = matrix.get(i, j).unwrap_or(UNREACHABLE);
148
+ }
149
+ }
150
+
151
+ let mut depot_to_delivery_seconds = Vec::with_capacity(plan.vehicles.len());
152
+ let mut delivery_to_depot_seconds = Vec::with_capacity(plan.vehicles.len());
153
+ let mut depot_to_delivery_meters = Vec::with_capacity(plan.vehicles.len());
154
+ let mut delivery_to_depot_meters = Vec::with_capacity(plan.vehicles.len());
155
+
156
+ for vehicle_idx in 0..plan.vehicles.len() {
157
+ let depot_idx = delivery_count + vehicle_idx;
158
+ let depot = plan.vehicles[vehicle_idx].depot_coord()?;
159
+ let mut outbound_seconds = Vec::with_capacity(delivery_count);
160
+ let mut inbound_seconds = Vec::with_capacity(delivery_count);
161
+ let mut outbound_meters = Vec::with_capacity(delivery_count);
162
+ let mut inbound_meters = Vec::with_capacity(delivery_count);
163
+ for (delivery_idx, delivery_coord) in delivery_coords.iter().enumerate() {
164
+ outbound_seconds.push(matrix.get(depot_idx, delivery_idx).unwrap_or(UNREACHABLE));
165
+ inbound_seconds.push(matrix.get(delivery_idx, depot_idx).unwrap_or(UNREACHABLE));
166
+ let meters = haversine_distance(depot, *delivery_coord).round() as i64;
167
+ outbound_meters.push(meters);
168
+ inbound_meters.push(meters);
169
+ }
170
+ depot_to_delivery_seconds.push(outbound_seconds);
171
+ delivery_to_depot_seconds.push(inbound_seconds);
172
+ depot_to_delivery_meters.push(outbound_meters);
173
+ delivery_to_depot_meters.push(inbound_meters);
174
+ }
175
+
176
+ Ok(Self {
177
+ delivery_travel_times,
178
+ depot_to_delivery_seconds,
179
+ delivery_to_depot_seconds,
180
+ depot_to_delivery_meters,
181
+ delivery_to_depot_meters,
182
+ })
183
+ }
184
+ }
src/domain/route_metrics/routes.rs ADDED
@@ -0,0 +1,168 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ use solverforge_maps::{
2
+ encode_polyline, haversine_distance, BoundingBox, NetworkConfig, RoadNetwork, RoutingError,
3
+ };
4
+
5
+ use crate::domain::{Plan, RoutingMode};
6
+
7
+ use super::helpers::{delivery_coords, meters_to_seconds, route_bounds};
8
+ use super::metrics::metrics_for_vehicle;
9
+ use super::types::{RouteLegGeometry, RouteLegSummary, RoutesSnapshot};
10
+
11
+ /// Builds browser route geometry for one already-selected solution snapshot.
12
+ ///
13
+ /// This is separate from `prepare_plan`: preparation builds matrices for
14
+ /// scoring, while `/jobs/{id}/routes` turns the retained snapshot into encoded
15
+ /// map geometry the UI can draw.
16
+ pub async fn build_routes_snapshot(plan: &Plan) -> Result<RoutesSnapshot, RoutingError> {
17
+ let bounds = route_bounds(plan)?;
18
+ let vehicles = match plan.routing_mode {
19
+ RoutingMode::StraightLine => build_straight_line_routes(plan)?,
20
+ RoutingMode::RoadNetwork => build_road_routes(plan).await?,
21
+ };
22
+
23
+ Ok(RoutesSnapshot {
24
+ routing_mode: match plan.routing_mode {
25
+ RoutingMode::StraightLine => "straight_line".to_string(),
26
+ RoutingMode::RoadNetwork => "road_network".to_string(),
27
+ },
28
+ bounds,
29
+ vehicles,
30
+ })
31
+ }
32
+
33
+ fn build_straight_line_routes(plan: &Plan) -> Result<Vec<RouteLegSummary>, RoutingError> {
34
+ let mut routes = Vec::with_capacity(plan.vehicles.len());
35
+ for vehicle in &plan.vehicles {
36
+ let metrics = metrics_for_vehicle(plan, vehicle);
37
+ let mut segments = Vec::new();
38
+ let mut previous_coord = vehicle.depot_coord()?;
39
+ let mut previous_id = None;
40
+ for &delivery_id in &vehicle.delivery_order {
41
+ let delivery = &plan.deliveries[delivery_id];
42
+ let coord = delivery.coord()?;
43
+ let meters = haversine_distance(previous_coord, coord).round() as i64;
44
+ let seconds = meters_to_seconds(meters);
45
+ segments.push(RouteLegGeometry {
46
+ vehicle_id: vehicle.id,
47
+ from_kind: if previous_id.is_some() { "delivery" } else { "depot" },
48
+ from_id: previous_id,
49
+ to_kind: "delivery",
50
+ to_id: Some(delivery_id),
51
+ duration_seconds: seconds,
52
+ distance_meters: meters,
53
+ encoded_polyline: encode_polyline(&[previous_coord, coord]),
54
+ });
55
+ previous_coord = coord;
56
+ previous_id = Some(delivery_id);
57
+ }
58
+ if let Some(last_delivery_id) = previous_id {
59
+ let depot = vehicle.depot_coord()?;
60
+ let meters = haversine_distance(previous_coord, depot).round() as i64;
61
+ let seconds = meters_to_seconds(meters);
62
+ segments.push(RouteLegGeometry {
63
+ vehicle_id: vehicle.id,
64
+ from_kind: "delivery",
65
+ from_id: Some(last_delivery_id),
66
+ to_kind: "depot",
67
+ to_id: None,
68
+ duration_seconds: seconds,
69
+ distance_meters: meters,
70
+ encoded_polyline: encode_polyline(&[previous_coord, depot]),
71
+ });
72
+ }
73
+ routes.push(RouteLegSummary {
74
+ vehicle_id: vehicle.id,
75
+ vehicle_name: vehicle.name.clone(),
76
+ total_travel_seconds: metrics.total_travel_seconds,
77
+ total_distance_meters: metrics.total_distance_meters,
78
+ total_demand: metrics.total_demand,
79
+ total_late_seconds: metrics.total_late_seconds,
80
+ stop_count: vehicle.delivery_order.len(),
81
+ segments,
82
+ });
83
+ }
84
+ Ok(routes)
85
+ }
86
+
87
+ async fn build_road_routes(plan: &Plan) -> Result<Vec<RouteLegSummary>, RoutingError> {
88
+ let mut coords = delivery_coords(plan)?;
89
+ for vehicle in &plan.vehicles {
90
+ coords.push(vehicle.depot_coord()?);
91
+ }
92
+
93
+ if coords.is_empty() {
94
+ return Ok(Vec::new());
95
+ }
96
+
97
+ let bbox = BoundingBox::from_coords(&coords).expand_for_routing(&coords);
98
+ let network = RoadNetwork::load_or_fetch(&bbox, &NetworkConfig::default(), None).await?;
99
+
100
+ let mut routes = Vec::with_capacity(plan.vehicles.len());
101
+ for vehicle in &plan.vehicles {
102
+ routes.push(build_vehicle_road_route(plan, &network, vehicle).await?);
103
+ }
104
+ Ok(routes)
105
+ }
106
+
107
+ async fn build_vehicle_road_route(
108
+ plan: &Plan,
109
+ network: &RoadNetwork,
110
+ vehicle: &crate::domain::Vehicle,
111
+ ) -> Result<RouteLegSummary, RoutingError> {
112
+ let metrics = metrics_for_vehicle(plan, vehicle);
113
+ let mut segments = Vec::new();
114
+ let mut total_distance_meters = 0_i64;
115
+ let mut total_travel_seconds = 0_i64;
116
+ let mut previous_coord = vehicle.depot_coord()?;
117
+ let mut previous_id = None;
118
+
119
+ for &delivery_id in &vehicle.delivery_order {
120
+ let delivery = &plan.deliveries[delivery_id];
121
+ let coord = delivery.coord()?;
122
+ let route = network.route(previous_coord, coord)?;
123
+ let distance_meters = route.distance_meters.round() as i64;
124
+ total_distance_meters += distance_meters;
125
+ total_travel_seconds += route.duration_seconds;
126
+ segments.push(RouteLegGeometry {
127
+ vehicle_id: vehicle.id,
128
+ from_kind: if previous_id.is_some() { "delivery" } else { "depot" },
129
+ from_id: previous_id,
130
+ to_kind: "delivery",
131
+ to_id: Some(delivery_id),
132
+ duration_seconds: route.duration_seconds,
133
+ distance_meters,
134
+ encoded_polyline: encode_polyline(&route.geometry),
135
+ });
136
+ previous_coord = coord;
137
+ previous_id = Some(delivery_id);
138
+ }
139
+
140
+ if let Some(last_delivery_id) = previous_id {
141
+ let depot = vehicle.depot_coord()?;
142
+ let route = network.route(previous_coord, depot)?;
143
+ let distance_meters = route.distance_meters.round() as i64;
144
+ total_distance_meters += distance_meters;
145
+ total_travel_seconds += route.duration_seconds;
146
+ segments.push(RouteLegGeometry {
147
+ vehicle_id: vehicle.id,
148
+ from_kind: "delivery",
149
+ from_id: Some(last_delivery_id),
150
+ to_kind: "depot",
151
+ to_id: None,
152
+ duration_seconds: route.duration_seconds,
153
+ distance_meters,
154
+ encoded_polyline: encode_polyline(&route.geometry),
155
+ });
156
+ }
157
+
158
+ Ok(RouteLegSummary {
159
+ vehicle_id: vehicle.id,
160
+ vehicle_name: vehicle.name.clone(),
161
+ total_travel_seconds,
162
+ total_distance_meters,
163
+ total_demand: metrics.total_demand,
164
+ total_late_seconds: metrics.total_late_seconds,
165
+ stop_count: vehicle.delivery_order.len(),
166
+ segments,
167
+ })
168
+ }
src/domain/route_metrics/scoring.rs ADDED
@@ -0,0 +1,186 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ use std::collections::HashSet;
2
+
3
+ use solverforge::prelude::HardSoftScore;
4
+ use crate::domain::{
5
+ Delivery, DeliveryKind, DeliveryPreview, Plan, PlanPreview, VehiclePreview, VehiclePreviewStop,
6
+ };
7
+
8
+ use super::metrics::metrics_for_vehicle;
9
+ use super::types::{
10
+ PlanScoreComponents, RouteStopMetrics, VehicleRouteMetrics, UNASSIGNED_DELIVERY_HARD_PENALTY,
11
+ UNREACHABLE_HARD_PENALTY,
12
+ };
13
+
14
+ pub fn preview_for_plan(plan: &Plan) -> PlanPreview {
15
+ let components = evaluate_plan(plan);
16
+ let vehicle_metrics: Vec<VehicleRouteMetrics> = plan
17
+ .vehicles
18
+ .iter()
19
+ .map(|vehicle| metrics_for_vehicle(plan, vehicle))
20
+ .collect();
21
+
22
+ let vehicles = plan
23
+ .vehicles
24
+ .iter()
25
+ .zip(vehicle_metrics.iter())
26
+ .map(|(vehicle, metrics)| {
27
+ let stops = metrics
28
+ .stops
29
+ .iter()
30
+ .map(|stop| vehicle_preview_stop(plan, stop))
31
+ .collect();
32
+
33
+ VehiclePreview {
34
+ vehicle_id: vehicle.id,
35
+ vehicle_name: vehicle.name.clone(),
36
+ total_demand: metrics.total_demand,
37
+ capacity_overage: metrics.capacity_overage,
38
+ stop_count: metrics.stops.len(),
39
+ total_travel_seconds: metrics.total_travel_seconds,
40
+ total_wait_seconds: metrics.total_wait_seconds,
41
+ total_service_seconds: metrics.total_service_seconds,
42
+ total_late_seconds: metrics.total_late_seconds,
43
+ start_time: metrics.start_time,
44
+ end_time: metrics.end_time,
45
+ stops,
46
+ }
47
+ })
48
+ .collect();
49
+
50
+ let mut deliveries = plan
51
+ .deliveries
52
+ .iter()
53
+ .map(delivery_preview)
54
+ .collect::<Vec<_>>();
55
+
56
+ for (vehicle, metrics) in plan.vehicles.iter().zip(vehicle_metrics.iter()) {
57
+ for stop in &metrics.stops {
58
+ let preview = &mut deliveries[stop.delivery_id];
59
+ preview.assigned_vehicle_id = Some(vehicle.id);
60
+ preview.assigned_vehicle_name = Some(vehicle.name.clone());
61
+ preview.sequence = Some(stop.sequence);
62
+ preview.arrival_time = Some(stop.arrival_time);
63
+ preview.service_start_time = Some(stop.service_start_time);
64
+ preview.departure_time = Some(stop.departure_time);
65
+ preview.late_seconds = Some(stop.late_seconds);
66
+ }
67
+ }
68
+
69
+ PlanPreview {
70
+ hard_score: components.hard_score(),
71
+ soft_score: components.soft_score(),
72
+ unassigned_delivery_ids: components.unassigned_delivery_ids(plan),
73
+ vehicles,
74
+ deliveries,
75
+ }
76
+ }
77
+
78
+ /// Mirrors the constraint model for UI previews and insertion recommendations.
79
+ ///
80
+ /// SolverForge constraints remain the authoritative score during solving. This
81
+ /// helper gives non-solver UI flows a cheap, explainable score breakdown from
82
+ /// the same route shadows and assignment coverage concepts.
83
+ pub fn evaluate_plan(plan: &Plan) -> PlanScoreComponents {
84
+ let assigned: HashSet<usize> = plan
85
+ .vehicles
86
+ .iter()
87
+ .flat_map(|vehicle| vehicle.delivery_order.iter().copied())
88
+ .collect();
89
+
90
+ let mut components = PlanScoreComponents {
91
+ unassigned_count: plan
92
+ .deliveries
93
+ .iter()
94
+ .filter(|delivery| !assigned.contains(&delivery.id))
95
+ .count(),
96
+ ..PlanScoreComponents::default()
97
+ };
98
+
99
+ for vehicle in &plan.vehicles {
100
+ let metrics = metrics_for_vehicle(plan, vehicle);
101
+ components.capacity_overage += i64::from(metrics.capacity_overage);
102
+ components.late_seconds += metrics.total_late_seconds;
103
+ components.unreachable_legs += metrics.unreachable_legs;
104
+ components.travel_seconds += metrics.total_travel_seconds;
105
+ }
106
+
107
+ components
108
+ }
109
+
110
+ impl PlanScoreComponents {
111
+ pub fn hard_score(&self) -> i64 {
112
+ -((self.unassigned_count as i64 * UNASSIGNED_DELIVERY_HARD_PENALTY)
113
+ + self.capacity_overage
114
+ + self.late_seconds
115
+ + (self.unreachable_legs as i64 * UNREACHABLE_HARD_PENALTY))
116
+ }
117
+
118
+ pub fn soft_score(&self) -> i64 {
119
+ -self.travel_seconds
120
+ }
121
+
122
+ pub fn score(&self) -> HardSoftScore {
123
+ HardSoftScore::of(self.hard_score(), self.soft_score())
124
+ }
125
+
126
+ pub fn unassigned_delivery_ids(&self, plan: &Plan) -> Vec<usize> {
127
+ let assigned: HashSet<usize> = plan
128
+ .vehicles
129
+ .iter()
130
+ .flat_map(|vehicle| vehicle.delivery_order.iter().copied())
131
+ .collect();
132
+ plan.deliveries
133
+ .iter()
134
+ .filter(|delivery| !assigned.contains(&delivery.id))
135
+ .map(|delivery| delivery.id)
136
+ .collect()
137
+ }
138
+ }
139
+
140
+ fn vehicle_preview_stop(plan: &Plan, stop: &RouteStopMetrics) -> VehiclePreviewStop {
141
+ let delivery = &plan.deliveries[stop.delivery_id];
142
+ VehiclePreviewStop {
143
+ delivery_id: delivery.id,
144
+ label: delivery.label.clone(),
145
+ kind: kind_name(delivery),
146
+ sequence: stop.sequence,
147
+ demand: delivery.demand,
148
+ min_start_time: delivery.min_start_time,
149
+ max_end_time: delivery.max_end_time,
150
+ arrival_time: stop.arrival_time,
151
+ service_start_time: stop.service_start_time,
152
+ departure_time: stop.departure_time,
153
+ travel_seconds_from_previous: stop.travel_seconds_from_previous,
154
+ wait_seconds: stop.wait_seconds,
155
+ late_seconds: stop.late_seconds,
156
+ }
157
+ }
158
+
159
+ fn delivery_preview(delivery: &Delivery) -> DeliveryPreview {
160
+ DeliveryPreview {
161
+ delivery_id: delivery.id,
162
+ label: delivery.label.clone(),
163
+ kind: kind_name(delivery),
164
+ demand: delivery.demand,
165
+ min_start_time: delivery.min_start_time,
166
+ max_end_time: delivery.max_end_time,
167
+ service_duration: delivery.service_duration,
168
+ assigned_vehicle_id: None,
169
+ assigned_vehicle_name: None,
170
+ sequence: None,
171
+ arrival_time: None,
172
+ service_start_time: None,
173
+ departure_time: None,
174
+ late_seconds: None,
175
+ }
176
+ }
177
+
178
+ fn kind_name(delivery: &Delivery) -> String {
179
+ match delivery.kind {
180
+ DeliveryKind::Residential => "residential",
181
+ DeliveryKind::Business => "business",
182
+ DeliveryKind::Restaurant => "restaurant",
183
+ DeliveryKind::Other => "other",
184
+ }
185
+ .to_string()
186
+ }
src/domain/route_metrics/tests.rs ADDED
@@ -0,0 +1,92 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ use super::*;
2
+ use crate::domain::{Delivery, DeliveryKind, Plan, RoutingMode, Vehicle};
3
+
4
+ fn sample_plan() -> Plan {
5
+ let deliveries = vec![
6
+ Delivery::new(
7
+ 0,
8
+ "A",
9
+ DeliveryKind::Business,
10
+ (43.7696, 11.2558),
11
+ 3,
12
+ (8 * 3600, 12 * 3600),
13
+ 10 * 60,
14
+ ),
15
+ Delivery::new(
16
+ 1,
17
+ "B",
18
+ DeliveryKind::Residential,
19
+ (43.7710, 11.2620),
20
+ 2,
21
+ (9 * 3600, 18 * 3600),
22
+ 5 * 60,
23
+ ),
24
+ Delivery::new(
25
+ 2,
26
+ "C",
27
+ DeliveryKind::Restaurant,
28
+ (43.7755, 11.2540),
29
+ 4,
30
+ (6 * 3600, 10 * 3600),
31
+ 15 * 60,
32
+ ),
33
+ ];
34
+ let mut vehicles = vec![
35
+ Vehicle::new(0, "Alpha", 10, 43.7696, 11.2558, 6 * 3600),
36
+ Vehicle::new(1, "Bravo", 8, 43.7745, 11.2487, 6 * 3600),
37
+ ];
38
+ vehicles[0].delivery_order = vec![0, 1];
39
+ vehicles[1].delivery_order = vec![2];
40
+ Plan::new("Sample", deliveries, vehicles)
41
+ }
42
+
43
+ #[tokio::test]
44
+ async fn prepare_plan_populates_vehicle_routing_data() {
45
+ let mut plan = sample_plan();
46
+ plan.routing_mode = RoutingMode::StraightLine;
47
+ prepare_plan(&mut plan)
48
+ .await
49
+ .expect("straight-line prep should succeed");
50
+ assert!(plan
51
+ .vehicles
52
+ .iter()
53
+ .all(|vehicle| vehicle.prepared_routing.is_some()));
54
+ }
55
+
56
+ #[test]
57
+ fn preview_reports_assignments() {
58
+ let plan = sample_plan();
59
+ let preview = preview_for_plan(&plan);
60
+ assert_eq!(preview.unassigned_delivery_ids.len(), 0);
61
+ assert_eq!(preview.vehicles.len(), 2);
62
+ assert_eq!(preview.deliveries[0].assigned_vehicle_id, Some(0));
63
+ }
64
+
65
+ #[test]
66
+ fn unassigned_deliveries_are_dominant_hard_penalties() {
67
+ let mut plan = sample_plan();
68
+ plan.vehicles[0].delivery_order.clear();
69
+ plan.vehicles[1].delivery_order.clear();
70
+
71
+ let components = evaluate_plan(&plan);
72
+
73
+ assert_eq!(components.unassigned_count, 3);
74
+ assert!(
75
+ components.hard_score() <= -(3 * UNASSIGNED_DELIVERY_HARD_PENALTY),
76
+ "unassigned deliveries must not be cheaper than seconds-based route violations"
77
+ );
78
+ }
79
+
80
+ #[tokio::test]
81
+ async fn empty_road_network_routes_have_no_bounds_or_vehicles() {
82
+ let mut plan = Plan::new("Empty", Vec::new(), Vec::new());
83
+ plan.routing_mode = RoutingMode::RoadNetwork;
84
+
85
+ let snapshot = build_routes_snapshot(&plan)
86
+ .await
87
+ .expect("empty road-network routes should not require a bounding box");
88
+
89
+ assert_eq!(snapshot.routing_mode, "road_network");
90
+ assert!(snapshot.bounds.is_none());
91
+ assert!(snapshot.vehicles.is_empty());
92
+ }
src/domain/route_metrics/types.rs ADDED
@@ -0,0 +1,121 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ use serde::Serialize;
2
+ use crate::domain::Plan;
3
+
4
+ pub const UNASSIGNED_DELIVERY_HARD_PENALTY: i64 = 1_000_000;
5
+ pub(super) const UNREACHABLE_HARD_PENALTY: i64 = 86_400;
6
+
7
+ #[derive(Clone, Debug, Default, PartialEq, Eq, Hash)]
8
+ pub struct PreparedVehicleRouting {
9
+ pub problem_data_index: usize,
10
+ pub capacity: i64,
11
+ pub demands: Vec<i32>,
12
+ pub distance_matrix: Vec<Vec<i64>>,
13
+ pub time_windows: Vec<(i64, i64)>,
14
+ pub service_durations: Vec<i64>,
15
+ pub travel_times: Vec<Vec<i64>>,
16
+ pub vehicle_departure_time: i64,
17
+ pub depot_to_delivery_seconds: Vec<i64>,
18
+ pub delivery_to_depot_seconds: Vec<i64>,
19
+ pub depot_to_delivery_meters: Vec<i64>,
20
+ pub delivery_to_depot_meters: Vec<i64>,
21
+ }
22
+
23
+ #[derive(Clone, Debug, Default)]
24
+ pub struct VehicleRouteMetrics {
25
+ pub vehicle_id: usize,
26
+ pub total_demand: i32,
27
+ pub capacity_overage: i32,
28
+ pub total_travel_seconds: i64,
29
+ pub total_wait_seconds: i64,
30
+ pub total_service_seconds: i64,
31
+ pub total_late_seconds: i64,
32
+ pub unreachable_legs: usize,
33
+ pub total_distance_meters: i64,
34
+ pub start_time: i64,
35
+ pub end_time: i64,
36
+ pub stops: Vec<RouteStopMetrics>,
37
+ }
38
+
39
+ #[derive(Clone, Debug, Default)]
40
+ pub struct RouteStopMetrics {
41
+ pub delivery_id: usize,
42
+ pub sequence: usize,
43
+ pub arrival_time: i64,
44
+ pub service_start_time: i64,
45
+ pub departure_time: i64,
46
+ pub travel_seconds_from_previous: i64,
47
+ pub wait_seconds: i64,
48
+ pub late_seconds: i64,
49
+ }
50
+
51
+ #[derive(Clone, Debug, Default, Serialize)]
52
+ #[serde(rename_all = "camelCase")]
53
+ pub struct RoutesSnapshot {
54
+ pub routing_mode: String,
55
+ pub bounds: Option<RouteBounds>,
56
+ pub vehicles: Vec<RouteLegSummary>,
57
+ }
58
+
59
+ #[derive(Clone, Debug, Default, Serialize)]
60
+ #[serde(rename_all = "camelCase")]
61
+ pub struct RouteBounds {
62
+ pub south_west: [f64; 2],
63
+ pub north_east: [f64; 2],
64
+ }
65
+
66
+ #[derive(Clone, Debug, Default, Serialize)]
67
+ #[serde(rename_all = "camelCase")]
68
+ pub struct RouteLegSummary {
69
+ pub vehicle_id: usize,
70
+ pub vehicle_name: String,
71
+ pub total_travel_seconds: i64,
72
+ pub total_distance_meters: i64,
73
+ pub total_demand: i32,
74
+ pub total_late_seconds: i64,
75
+ pub stop_count: usize,
76
+ pub segments: Vec<RouteLegGeometry>,
77
+ }
78
+
79
+ #[derive(Clone, Debug, Default, Serialize)]
80
+ #[serde(rename_all = "camelCase")]
81
+ pub struct RouteLegGeometry {
82
+ pub vehicle_id: usize,
83
+ pub from_kind: &'static str,
84
+ pub from_id: Option<usize>,
85
+ pub to_kind: &'static str,
86
+ pub to_id: Option<usize>,
87
+ pub duration_seconds: i64,
88
+ pub distance_meters: i64,
89
+ pub encoded_polyline: String,
90
+ }
91
+
92
+ #[derive(Clone, Debug, Default)]
93
+ pub struct PlanScoreComponents {
94
+ pub unassigned_count: usize,
95
+ pub capacity_overage: i64,
96
+ pub late_seconds: i64,
97
+ pub unreachable_legs: usize,
98
+ pub travel_seconds: i64,
99
+ }
100
+
101
+ #[derive(Clone, Debug)]
102
+ pub struct DeliveryInsertionCandidate {
103
+ pub vehicle_id: usize,
104
+ pub vehicle_name: String,
105
+ pub insert_index: usize,
106
+ pub hard_score: i64,
107
+ pub soft_score: i64,
108
+ pub delta_hard: i64,
109
+ pub delta_soft: i64,
110
+ pub preview_plan: Plan,
111
+ }
112
+
113
+ pub trait DeliveryRoutingSolution: solverforge::cvrp::VrpSolution {
114
+ fn delivery_plan(&self) -> &Plan;
115
+ }
116
+
117
+ impl DeliveryRoutingSolution for Plan {
118
+ fn delivery_plan(&self) -> &Plan {
119
+ self
120
+ }
121
+ }