1#[allow(dead_code, unused_variables, unused_mut, unused_imports)]
2
3use glam::{Vec2, Vec3, Vec4, Quat, Mat4};
4use std::collections::{HashMap, VecDeque, HashSet, BTreeMap};
5
6const MAX_ROAD_NODES: usize = 65536;
11const MAX_ROAD_SEGMENTS: usize = 65536;
12const SPLINE_SUBDIVISIONS: usize = 32;
13const TERRAIN_SAMPLE_RADIUS: f32 = 8.0;
14const ROAD_BLEND_FALLOFF: f32 = 4.0;
15const BRIDGE_DETECT_THRESHOLD: f32 = 2.5;
16const PILLAR_SPACING: f32 = 12.0;
17const POTHOLE_PROBABILITY_BASE: f32 = 0.0001;
18const PUDDLE_DEPRESSION_THRESHOLD: f32 = 0.15;
19const TRAFFIC_DENSITY_MAX: f32 = 1.0;
20const LWR_DT: f32 = 0.016;
21const LWR_DX: f32 = 1.0;
22const DIJKSTRA_INF: f64 = 1.0e18;
23const LANE_WIDTH: f32 = 3.65;
24const CURB_HEIGHT: f32 = 0.15;
25const CURB_WIDTH: f32 = 0.20;
26const SHOULDER_WIDTH: f32 = 2.5;
27const DITCH_DEPTH: f32 = 0.4;
28const DITCH_WIDTH: f32 = 1.2;
29const SIDEWALK_WIDTH: f32 = 1.5;
30const CROSSWALK_STRIPE_WIDTH: f32 = 0.5;
31const CROSSWALK_STRIPE_GAP: f32 = 0.5;
32const CENTER_LINE_DASH_LEN: f32 = 3.0;
33const CENTER_LINE_GAP_LEN: f32 = 9.0;
34const ROUNDABOUT_ISLAND_RADIUS: f32 = 6.0;
35const ROUNDABOUT_ROAD_WIDTH: f32 = 7.3;
36const PRIM_INF: f64 = 1.0e18;
37const MAX_SLOPE_FOR_FLATTEN: f32 = 0.7;
38const SPLAT_BLEND_RADIUS: f32 = 5.0;
39const EROSION_TIMESTEPS: usize = 100;
40const WEAR_ALPHA: f32 = 0.002;
41const UNDO_STACK_SIZE: usize = 256;
42const SNAP_RADIUS: f32 = 2.0;
43
44#[derive(Clone, Copy, Debug, PartialEq, Eq, Hash)]
49pub enum RoadType {
50 DirtTrack,
51 GravelRoad,
52 PavedRoad,
53 Highway2Lane,
54 Highway4Lane,
55 Motorway,
56 Alley,
57 Bridge,
58 Tunnel,
59 ElevatedHighway,
60 Cobblestone,
61 Boulevard,
62 ResidentialStreet,
63 BridgeRoad,
64 TunnelRoad,
65 ServiceRoad,
66}
67
68#[derive(Clone, Debug)]
69pub struct RoadProfile {
70 pub road_type: RoadType,
71 pub total_width: f32,
72 pub lane_count: u32,
73 pub lane_width: f32,
74 pub has_curb: bool,
75 pub has_shoulder: bool,
76 pub has_ditch: bool,
77 pub has_sidewalk: bool,
78 pub speed_limit_kmh: f32,
79 pub surface_friction: f32,
80 pub material_id: u32,
81 pub shoulder_material_id: u32,
82 pub max_slope_grade: f32,
83 pub is_elevated: bool,
84 pub tunnel_clearance: f32,
85 pub bridge_deck_thickness: f32,
86}
87
88#[derive(Clone, Debug)]
89pub struct SplinePoint {
90 pub position: Vec3,
91 pub tangent_in: Vec3,
92 pub tangent_out: Vec3,
93 pub bank_angle: f32,
94 pub elevation_override: Option<f32>,
95}
96
97#[derive(Clone, Debug)]
98pub struct RoadSpline {
99 pub control_points: Vec<SplinePoint>,
100 pub cached_samples: Vec<Vec3>,
101 pub cached_tangents: Vec<Vec3>,
102 pub cached_up_vectors: Vec<Vec3>,
103 pub total_length: f32,
104 pub subdivisions_per_segment: usize,
105}
106
107#[derive(Clone, Debug)]
108pub struct TerrainHeightMap {
109 pub width: usize,
110 pub height: usize,
111 pub cell_size: f32,
112 pub heights: Vec<f32>,
113 pub normals: Vec<Vec3>,
114 pub splat_weights: Vec<[f32; 8]>,
115}
116
117#[derive(Clone, Debug)]
118pub struct RoadVertex {
119 pub position: Vec3,
120 pub normal: Vec3,
121 pub uv: Vec2,
122 pub tangent: Vec4,
123 pub color: Vec4,
124}
125
126#[derive(Clone, Debug)]
127pub struct RoadMesh {
128 pub vertices: Vec<RoadVertex>,
129 pub indices: Vec<u32>,
130 pub submeshes: Vec<RoadSubmesh>,
131}
132
133#[derive(Clone, Debug)]
134pub struct RoadSubmesh {
135 pub start_index: u32,
136 pub index_count: u32,
137 pub material_id: u32,
138}
139
140pub struct RoadMeshGenerator;
141
142#[derive(Clone, Debug)]
143pub enum LaneMarkingType {
144 DashedCenter,
145 SolidEdge,
146 Gap,
147 Crosswalk,
148 StopLine,
149 ArrowStraight,
150 ArrowLeft,
151 ArrowRight,
152}
153
154#[derive(Clone, Debug)]
155pub struct LaneMarking {
156 pub position: Vec3,
157 pub tangent: Vec3,
158 pub width: f32,
159 pub length: f32,
160 pub marking_type: LaneMarkingType,
161 pub color: Vec3,
162}
163
164#[derive(Clone, Copy, Debug, PartialEq)]
169pub enum IntersectionType {
170 TJunction,
171 XJunction,
172 Roundabout,
173 OnRamp,
174 OffRamp,
175 Merge,
176}
177
178#[derive(Clone, Debug)]
179pub struct Intersection {
180 pub id: u32,
181 pub position: Vec3,
182 pub intersection_type: IntersectionType,
183 pub connected_roads: Vec<u32>,
184 pub mesh: RoadMesh,
185 pub roundabout_radius: f32,
186 pub normal: Vec3,
187}
188
189pub struct IntersectionGenerator;
190
191pub struct CrosswalkStripe {
192 pub start: Vec3,
193 pub end: Vec3,
194 pub width: f32,
195}
196
197pub struct CrosswalkGenerator;
198
199#[derive(Clone, Debug)]
200pub struct BridgePillar {
201 pub base_position: Vec3,
202 pub top_position: Vec3,
203 pub radius: f32,
204 pub height: f32,
205}
206
207pub struct BridgeGenerator;
208
209pub struct RoadNetworkNode {
210 pub id: u32,
211 pub position: Vec3,
212 pub connected_edges: Vec<u32>,
213 pub node_type: RoadNodeType,
214}
215
216#[derive(Clone, Copy, Debug, PartialEq)]
217pub enum RoadNodeType {
218 Intersection,
219 Endpoint,
220 Waypoint,
221 CityCenter,
222 Suburb,
223}
224
225#[derive(Clone, Debug)]
226pub struct RoadNetworkEdge {
227 pub id: u32,
228 pub from_node: u32,
229 pub to_node: u32,
230 pub length: f32,
231 pub speed_limit: f32,
232 pub road_type: RoadType,
233 pub lanes: u32,
234 pub is_one_way: bool,
235 pub spline_id: u32,
236 pub weight: f64,
237}
238
239pub struct RoadNetwork {
240 pub nodes: HashMap<u32, RoadNetworkNode>,
241 pub edges: HashMap<u32, RoadNetworkEdge>,
242 pub next_node_id: u32,
243 pub next_edge_id: u32,
244 pub adjacency: HashMap<u32, Vec<(u32, f64)>>,
245}
246
247#[derive(Clone, Debug)]
248pub struct TrafficCell {
249 pub density: f32,
250 pub velocity: f32,
251 pub flow: f32,
252}
253
254#[derive(Clone, Debug)]
255pub struct TrafficFlowSim {
256 pub edge_id: u32,
257 pub cells: Vec<TrafficCell>,
258 pub cell_length: f32,
259 pub max_density: f32,
260 pub free_flow_speed: f32,
261 pub jam_density: f32,
262 pub time: f32,
263}
264
265pub struct TerrainDeformer;
266
267pub struct CityNode {
268 pub id: u32,
269 pub position: Vec3,
270 pub node_type: RoadNodeType,
271 pub population: u32,
272}
273
274pub struct ProceduralRoadGenerator;
275
276pub struct RoadErosionState {
277 pub pothole_grid: Vec<f32>,
278 pub wear_grid: Vec<f32>,
279 pub puddle_grid: Vec<f32>,
280 pub width: usize,
281 pub height: usize,
282 pub cell_size: f32,
283}
284
285#[derive(Clone, Debug)]
286pub struct SimpleRng {
287 pub state: u64,
288}
289impl SimpleRng {
290 pub fn new(seed: u64) -> Self { Self { state: seed ^ 0x853c49e6748fea9b } }
291 pub fn next_u64(&mut self) -> u64 { self.state ^= self.state << 13; self.state ^= self.state >> 7; self.state ^= self.state << 17; self.state }
292 pub fn next_f32(&mut self) -> f32 { (self.next_u64() >> 33) as f32 / 2147483648.0 }
293}
294
295#[derive(Clone, Debug)]
296pub enum RoadEditAction {
297 AddRoad { road_id: u32 },
298 RemoveRoad { road_id: u32, snapshot: RoadSegment },
299 ModifyTerrain { x: usize, z: usize, old_height: f32, new_height: f32 },
300 AddIntersection { intersection_id: u32 },
301 RemoveIntersection { intersection_id: u32, snapshot: Intersection },
302 MoveControlPoint { spline_id: u32, point_index: usize, old_pos: Vec3, new_pos: Vec3 },
303}
304
305#[derive(Clone, Debug)]
306pub struct UndoStack {
307 pub actions: VecDeque<Vec<RoadEditAction>>,
308 pub redo_stack: VecDeque<Vec<RoadEditAction>>,
309 pub max_size: usize,
310 pub max_depth: usize,
311}
312
313#[derive(Clone, Debug)]
314pub struct RoadSegment {
315 pub id: u32,
316 pub spline: RoadSpline,
317 pub profile: RoadProfile,
318 pub mesh: RoadMesh,
319 pub sidewalk_mesh: RoadMesh,
320 pub lane_markings: Vec<LaneMarking>,
321 pub bridge_pillars: Vec<BridgePillar>,
322 pub is_bridge: bool,
323 pub is_tunnel: bool,
324 pub from_node: u32,
325 pub to_node: u32,
326 pub traffic_sim: TrafficFlowSim,
327}
328
329pub struct ElevationProfile {
330 pub distances: Vec<f32>,
331 pub elevations: Vec<f32>,
332 pub terrain_elevations: Vec<f32>,
333 pub max_grade: f32,
334 pub min_grade: f32,
335 pub avg_grade: f32,
336}
337
338pub struct RoadSnapper;
339
340#[derive(Clone, Debug)]
341pub enum RoadToolMode {
342 Idle,
343 PlacingRoad,
344 EditingSpline,
345 PlacingIntersection,
346 PaintingTerrain,
347 ViewElevationProfile,
348 SimulatingTraffic,
349}
350
351#[derive(Clone, Debug)]
352pub struct TerrainRoadToolState {
353 pub mode: RoadToolMode,
354 pub selected_road_type: RoadType,
355 pub active_segment_id: Option<u32>,
356 pub hover_pos: Vec3,
357 pub is_snapped: bool,
358 pub snap_target: Vec3,
359 pub show_elevation_profile: bool,
360 pub show_traffic_density: bool,
361 pub traffic_sim_running: bool,
362}
363
364pub struct TerrainRoadTool {
365 pub state: TerrainRoadToolState,
366 pub terrain: TerrainHeightMap,
367 pub segments: HashMap<u32, RoadSegment>,
368 pub intersections: HashMap<u32, Intersection>,
369 pub network: RoadNetwork,
370 pub erosion: RoadErosionState,
371 pub undo_stack: UndoStack,
372 pub city_nodes: Vec<CityNode>,
373 pub rng: SimpleRng,
374 pub next_segment_id: u32,
375 pub next_intersection_id: u32,
376 pub profiles: HashMap<RoadType, RoadProfile>,
377 pub elevation_profile_cache: Option<ElevationProfile>,
378 pub traffic_sims: HashMap<u32, TrafficFlowSim>,
379 pub build_pending_actions: Vec<RoadEditAction>,
380}
381
382pub struct RoadEditBatch {
383 pub actions: Vec<RoadEditAction>,
384 pub description: String,
385}
386
387pub struct RoadNetworkStats {
388 pub total_segments: usize,
389 pub total_length_km: f32,
390 pub total_intersections: usize,
391 pub road_type_counts: HashMap<RoadType, usize>,
392 pub average_traffic_density: f32,
393 pub highest_congestion_segment: Option<u32>,
394 pub bridge_count: usize,
395 pub tunnel_count: usize,
396 pub total_lane_km: f32,
397}
398
399pub struct RoadClipper;
400
401pub struct RoadLoftGenerator;
402
403pub struct TunnelGenerator;
404
405pub enum RoadValidationIssue {
406 SteepGrade { segment_id: u32, grade: f32, distance: f32 },
407 TooNarrowForLanes { segment_id: u32 },
408 IntersectsTerrain { segment_id: u32, position: Vec3 },
409 TooShort { segment_id: u32, length: f32 },
410 SelfIntersecting { segment_id: u32 },
411 MissingConnection { segment_id: u32 },
412}
413
414pub struct RoadValidator;
415
416pub struct RoadSerializedData {
417 pub version: u32,
418 pub segments: Vec<SerializedSegment>,
419 pub intersections: Vec<SerializedIntersection>,
420 pub network_nodes: Vec<SerializedNode>,
421 pub network_edges: Vec<SerializedEdge>,
422}
423
424#[derive(Clone, Debug)]
425pub struct SerializedSegment {
426 pub id: u32,
427 pub road_type: u32,
428 pub control_points: Vec<[f32; 3]>,
429 pub from_node: u32,
430 pub to_node: u32,
431}
432
433#[derive(Clone, Debug)]
434pub struct SerializedIntersection {
435 pub id: u32,
436 pub position: [f32; 3],
437 pub intersection_type: u32,
438 pub connected_roads: Vec<u32>,
439}
440
441#[derive(Clone, Debug)]
442pub struct SerializedNode {
443 pub id: u32,
444 pub position: [f32; 3],
445 pub node_type: u32,
446}
447
448#[derive(Clone, Debug)]
449pub struct SerializedEdge {
450 pub id: u32,
451 pub from_node: u32,
452 pub to_node: u32,
453 pub length: f32,
454 pub speed_limit: f32,
455 pub road_type: u32,
456}
457
458pub struct RoadProfilerFrame {
459 pub segment_count: usize,
460 pub vertex_count: usize,
461 pub index_count: usize,
462 pub traffic_step_ms: f32,
463 pub mesh_build_ms: f32,
464 pub terrain_deform_ms: f32,
465}
466
467pub struct RoadProfiler {
468 pub frames: VecDeque<RoadProfilerFrame>,
469 pub max_frames: usize,
470}
471
472pub struct RoadIntersectionDetector;
473
474pub struct RoadMaterial {
475 pub id: u32,
476 pub name: String,
477 pub albedo_texture: u32,
478 pub normal_texture: u32,
479 pub roughness: f32,
480 pub metallic: f32,
481 pub tiling_u: f32,
482 pub tiling_v: f32,
483 pub friction: f32,
484}
485
486pub struct RoadMaterialDatabase {
487 pub materials: HashMap<u32, RoadMaterial>,
488}
489
490pub fn lerp_f32(a: f32, b: f32, t: f32) -> f32 {
491 a + (b - a) * t
492}
493
494pub fn smoothstep(edge0: f32, edge1: f32, x: f32) -> f32 {
495 let t = ((x - edge0) / (edge1 - edge0)).clamp(0.0, 1.0);
496 t * t * (3.0 - 2.0 * t)
497}
498
499pub fn remap(value: f32, in_min: f32, in_max: f32, out_min: f32, out_max: f32) -> f32 {
500 let t = (value - in_min) / (in_max - in_min);
501 out_min + t * (out_max - out_min)
502}
503
504pub fn point_to_line_distance_2d(point: Vec2, line_a: Vec2, line_b: Vec2) -> f32 {
505 let ab = line_b - line_a;
506 let ap = point - line_a;
507 let t = (ap.dot(ab) / ab.length_squared()).clamp(0.0, 1.0);
508 let closest = line_a + ab * t;
509 point.distance(closest)
510}
511
512pub fn catmull_rom(p0: Vec3, p1: Vec3, p2: Vec3, p3: Vec3, t: f32) -> Vec3 {
513 let t2 = t * t;
514 let t3 = t2 * t;
515 0.5 * (
516 p1 * 2.0
517 + (p2 - p0) * t
518 + (p0 * 2.0 - p1 * 5.0 + p2 * 4.0 - p3) * t2
519 + (-p0 + p1 * 3.0 - p2 * 3.0 + p3) * t3
520 )
521}
522
523pub fn spline_arc_length(points: &[Vec3], subdivisions: usize) -> f32 {
524 let n = points.len();
525 if n < 2 { return 0.0; }
526 let mut len = 0.0f32;
527 for i in 0..n-1 {
528 let p0 = if i == 0 { points[0] } else { points[i-1] };
529 let p1 = points[i];
530 let p2 = points[i+1];
531 let p3 = if i+2 < n { points[i+2] } else { points[n-1] };
532 let mut prev = catmull_rom(p0, p1, p2, p3, 0.0);
533 for s in 1..=subdivisions {
534 let t = s as f32 / subdivisions as f32;
535 let cur = catmull_rom(p0, p1, p2, p3, t);
536 len += prev.distance(cur);
537 prev = cur;
538 }
539 }
540 len
541}
542
543pub fn build_frenet_frame(tangent: Vec3) -> (Vec3, Vec3, Vec3) {
544 let t = tangent.normalize_or_zero();
545 let up = if t.y.abs() < 0.99 { Vec3::Y } else { Vec3::X };
546 let right = t.cross(up).normalize_or_zero();
547 let actual_up = right.cross(t).normalize_or_zero();
548 (t, right, actual_up)
549}
550
551pub fn circle_arc_points(center: Vec3, radius: f32, start_angle: f32, end_angle: f32, steps: usize) -> Vec<Vec3> {
552 let mut pts = Vec::new();
553 for i in 0..=steps {
554 let a = start_angle + (end_angle - start_angle) * (i as f32 / steps as f32);
555 let (s, c) = a.sin_cos();
556 pts.push(center + Vec3::new(c * radius, 0.0, s * radius));
557 }
558 pts
559}
560
561pub fn fit_bezier_to_points(points: &[Vec3]) -> Vec<Vec3> {
562 if points.len() < 2 { return points.to_vec(); }
564 let p0 = points[0];
565 let p3 = *points.last().unwrap();
566 let n = points.len();
567 let mut lengths = vec![0.0f32; n];
569 for i in 1..n {
570 lengths[i] = lengths[i-1] + points[i-1].distance(points[i]);
571 }
572 let total = lengths[n-1];
573 let ts: Vec<f32> = lengths.iter().map(|&l| if total > 0.0 { l / total } else { 0.0 }).collect();
574 let alpha1 = (0..n).map(|i| {
576 let t = ts[i];
577 let b1 = 3.0 * t * (1.0-t).powi(2);
578 let rhs = points[i] - p0*(1.0-t).powi(3) - p3*t.powi(3);
579 b1 * b1
580 }).sum::<f32>();
581 let alpha2 = (0..n).map(|i| {
582 let t = ts[i];
583 let b2 = 3.0 * t.powi(2) * (1.0-t);
584 b2 * b2
585 }).sum::<f32>();
586 let tan0 = if n > 1 { (points[1] - points[0]).normalize_or_zero() } else { Vec3::Z };
588 let tan1 = if n > 1 { (points[n-1] - points[n-2]).normalize_or_zero() } else { Vec3::Z };
589 let total_len = total;
590 let p1 = p0 + tan0 * (total_len / 3.0);
591 let p2 = p3 - tan1 * (total_len / 3.0);
592 vec![p0, p1, p2, p3]
593}
594
595pub fn douglas_peucker(points: &[Vec3], epsilon: f32) -> Vec<Vec3> {
596 if points.len() < 3 { return points.to_vec(); }
597 let start = points[0];
598 let end = *points.last().unwrap();
599 let mut max_dist = 0.0f32;
600 let mut max_idx = 0;
601 let line_ab = end - start;
602 let line_len = line_ab.length();
603 for i in 1..points.len()-1 {
604 let pt = points[i];
605 let dist = if line_len > 0.001 {
606 let ap = pt - start;
607 let proj = ap.dot(line_ab.normalize_or_zero());
608 let closest = start + line_ab.normalize_or_zero() * proj.clamp(0.0, line_len);
609 pt.distance(closest)
610 } else {
611 pt.distance(start)
612 };
613 if dist > max_dist {
614 max_dist = dist;
615 max_idx = i;
616 }
617 }
618 if max_dist > epsilon {
619 let left = douglas_peucker(&points[..=max_idx], epsilon);
620 let right = douglas_peucker(&points[max_idx..], epsilon);
621 let mut result = left;
622 result.pop();
623 result.extend(right);
624 result
625 } else {
626 vec![start, end]
627 }
628}
629
630pub fn road_density_heatmap(segments: &HashMap<u32, RoadSegment>, width: usize, height: usize, cell_size: f32) -> Vec<f32> {
631 let mut heatmap = vec![0.0f32; width * height];
632 for seg in segments.values() {
633 for (i, &sample) in seg.spline.cached_samples.iter().enumerate() {
634 let cx = (sample.x / cell_size).clamp(0.0, (width-1) as f32) as usize;
635 let cz = (sample.z / cell_size).clamp(0.0, (height-1) as f32) as usize;
636 let idx = cz * width + cx;
637 if idx < heatmap.len() {
638 let density = if i < seg.traffic_sim.cells.len() {
639 seg.traffic_sim.cells[i].density
640 } else { 0.0 };
641 heatmap[idx] = (heatmap[idx] + density).min(1.0);
642 }
643 }
644 }
645 heatmap
646}
647
648pub fn generate_noise_terrain(terrain: &mut TerrainHeightMap, octaves: usize, freq: f32, amplitude: f32, seed: u64) {
649 let mut rng = SimpleRng::new(seed);
650 for z in 0..terrain.height {
651 for x in 0..terrain.width {
652 let wx = x as f32 * terrain.cell_size;
653 let wz = z as f32 * terrain.cell_size;
654 let mut h = 0.0f32;
655 let mut f = freq;
656 let mut a = amplitude;
657 for _ in 0..octaves {
658 let nx = wx * f + rng.next_f32() * 0.001;
659 let nz = wz * f + rng.next_f32() * 0.001;
660 let v = simple_noise_2d(nx, nz);
661 h += v * a;
662 f *= 2.0;
663 a *= 0.5;
664 }
665 terrain.set_height(x, z, h.max(0.0));
666 }
667 }
668 terrain.recompute_normals();
669}
670
671pub fn simple_noise_2d(x: f32, y: f32) -> f32 {
672 let ix = x as i32;
673 let iy = y as i32;
674 let fx = x - ix as f32;
675 let fy = y - iy as f32;
676 let ux = fx * fx * (3.0 - 2.0 * fx);
677 let uy = fy * fy * (3.0 - 2.0 * fy);
678 let n00 = pseudo_random_2d(ix, iy);
679 let n10 = pseudo_random_2d(ix + 1, iy);
680 let n01 = pseudo_random_2d(ix, iy + 1);
681 let n11 = pseudo_random_2d(ix + 1, iy + 1);
682 let nx0 = n00 + (n10 - n00) * ux;
683 let nx1 = n01 + (n11 - n01) * ux;
684 nx0 + (nx1 - nx0) * uy
685}
686
687pub fn pseudo_random_2d(x: i32, y: i32) -> f32 {
688 let n = (x.wrapping_mul(1619).wrapping_add(y.wrapping_mul(31337))) as u32;
689 let n = n.wrapping_mul(1234567891).wrapping_add(0x9e3779b9);
690 let n = n ^ (n >> 16);
691 let n = n.wrapping_mul(0x45d9f3b);
692 let n = n ^ (n >> 16);
693 (n as f32) / (u32::MAX as f32)
694}
695
696pub struct PathSmoother;
701
702pub struct RoadSegmentSplitter;
703
704pub struct RoadOverlayLine {
705 pub start: Vec3,
706 pub end: Vec3,
707 pub color: Vec4,
708 pub thickness: f32,
709}
710
711pub struct RoadOverlayRenderer {
712 pub lines: Vec<RoadOverlayLine>,
713 pub show_spline_handles: bool,
714 pub show_normals: bool,
715 pub show_lane_markings: bool,
716 pub show_traffic_density: bool,
717 pub normal_length: f32,
718}
719
720pub struct TerrainSampleResult {
721 pub height: f32,
722 pub normal: Vec3,
723 pub slope: f32,
724 pub splat_weights: [f32; 8],
725}
726
727#[derive(Clone, Debug)]
728pub struct GradeSegment {
729 pub start_distance: f32,
730 pub end_distance: f32,
731 pub grade_percent: f32,
732 pub is_steep: bool,
733 pub start_station: f32,
734 pub end_station: f32,
735 pub grade: f32,
736 pub cut_volume: f32,
737 pub fill_volume: f32,
738}
739
740pub fn analyze_grade_profile(spline: &RoadSpline, max_grade: f32) -> Vec<GradeSegment> {
741 let mut segments = Vec::new();
742 let n = spline.cached_samples.len();
743 if n < 2 { return segments; }
744 let mut accum = 0.0f32;
745 for i in 1..n {
746 let prev = spline.cached_samples[i-1];
747 let curr = spline.cached_samples[i];
748 let dx = (curr.x - prev.x).powi(2) + (curr.z - prev.z).powi(2);
749 let dx = dx.sqrt().max(0.001);
750 let dh = curr.y - prev.y;
751 let grade = (dh / dx) * 100.0;
752 let seg_len = prev.distance(curr);
753 segments.push(GradeSegment {
754 start_distance: accum,
755 end_distance: accum + seg_len,
756 grade_percent: grade,
757 is_steep: grade.abs() > max_grade * 100.0,
758 start_station: accum,
759 end_station: accum + seg_len,
760 grade: grade / 100.0,
761 cut_volume: 0.0,
762 fill_volume: 0.0,
763 });
764 accum += seg_len;
765 }
766 segments
767}
768
769#[derive(Clone, Debug)]
774pub struct RoadTextureAtlasEntry {
775 pub material_id: u32,
776 pub uv_min: Vec2,
777 pub uv_max: Vec2,
778 pub road_type: RoadType,
779}
780
781pub struct RoadTextureAtlas {
782 pub entries: Vec<RoadTextureAtlasEntry>,
783 pub atlas_width: u32,
784 pub atlas_height: u32,
785}
786
787pub struct RoadSurfaceDetail {
788 pub crack_density: f32,
789 pub pothole_density: f32,
790 pub patch_density: f32,
791 pub puddle_density: f32,
792 pub wear_factor: f32,
793 pub age_years: f32,
794}
795
796pub struct SpeedZone {
797 pub id: u32,
798 pub center: Vec3,
799 pub radius: f32,
800 pub speed_limit_kmh: f32,
801 pub zone_type: SpeedZoneType,
802 pub start_station: f32,
803 pub end_station: f32,
804 pub posted_speed_kmh: u32,
805}
806
807#[derive(Clone, Copy, Debug, PartialEq)]
808pub enum SpeedZoneType {
809 School,
810 Hospital,
811 Construction,
812 Residential,
813 Commercial,
814 Industrial,
815 Highway,
816}
817
818pub struct SpeedZoneManager {
819 pub zones: Vec<SpeedZone>,
820 pub next_id: u32,
821}
822
823pub struct RoadAmbientZone {
824 pub position: Vec3,
825 pub radius: f32,
826 pub traffic_sound_level: f32,
827 pub road_type: RoadType,
828}
829
830pub fn compute_traffic_sound_level(density: f32, speed: f32, road_type: RoadType) -> f32 {
831 let base_level = match road_type {
832 RoadType::Motorway => 80.0,
833 RoadType::Highway4Lane => 75.0,
834 RoadType::Highway2Lane => 70.0,
835 RoadType::PavedRoad => 60.0,
836 RoadType::GravelRoad => 55.0,
837 RoadType::DirtTrack => 40.0,
838 _ => 50.0,
839 };
840 let density_factor = density.powf(0.5);
841 let speed_factor = (speed / 50.0).ln().max(0.0);
842 base_level + density_factor * 10.0 + speed_factor * 5.0
843}
844
845#[derive(Clone, Debug)]
850pub struct RoadCostEstimate {
851 pub material_cost: f64,
852 pub labor_cost: f64,
853 pub equipment_cost: f64,
854 pub total_cost: f64,
855 pub cost_per_km: f64,
856}
857
858pub fn estimate_road_cost(length_m: f32, profile: &RoadProfile) -> RoadCostEstimate {
859 let base_cost_per_m = match profile.road_type {
860 RoadType::DirtTrack => 50.0,
861 RoadType::GravelRoad => 150.0,
862 RoadType::PavedRoad => 500.0,
863 RoadType::Highway2Lane => 1500.0,
864 RoadType::Highway4Lane => 3000.0,
865 RoadType::Motorway => 6000.0,
866 RoadType::Alley => 300.0,
867 RoadType::Bridge => 8000.0,
868 RoadType::Tunnel => 15000.0,
869 RoadType::ElevatedHighway => 12000.0,
870 RoadType::Cobblestone => 800.0,
871 RoadType::ServiceRoad => 200.0,
872 RoadType::Boulevard => 1000.0,
873 RoadType::ResidentialStreet => 400.0,
874 RoadType::BridgeRoad => 8000.0,
875 RoadType::TunnelRoad => 15000.0,
876 };
877 let width_factor = profile.total_width / 7.3;
878 let effective_cost = base_cost_per_m * width_factor as f64 * length_m as f64;
879 let material = effective_cost * 0.4;
880 let labor = effective_cost * 0.35;
881 let equipment = effective_cost * 0.25;
882 RoadCostEstimate {
883 material_cost: material,
884 labor_cost: labor,
885 equipment_cost: equipment,
886 total_cost: effective_cost,
887 cost_per_km: effective_cost / (length_m as f64 / 1000.0),
888 }
889}
890
891#[derive(Clone, Debug)]
896pub struct RoadLightPost {
897 pub position: Vec3,
898 pub height: f32,
899 pub light_color: Vec3,
900 pub light_radius: f32,
901 pub is_active: bool,
902}
903
904pub fn generate_light_posts(spline: &RoadSpline, profile: &RoadProfile, spacing: f32) -> Vec<RoadLightPost> {
905 let mut posts = Vec::new();
906 if profile.road_type == RoadType::DirtTrack || profile.road_type == RoadType::GravelRoad { return posts; }
907 let half = profile.total_width * 0.5 + 0.5;
908 let mut dist = 0.0f32;
909 let mut side = 1.0f32;
910 while dist < spline.total_length {
911 let (pos, tan) = spline.sample_at_distance(dist); let up = Vec3::Y;
912 let right = tan.cross(up).normalize_or_zero();
913 let post_pos = pos + right * half * side + up * 0.1;
914 posts.push(RoadLightPost {
915 position: post_pos,
916 height: 8.0,
917 light_color: Vec3::new(1.0, 0.95, 0.8),
918 light_radius: 20.0,
919 is_active: true,
920 });
921 dist += spacing;
922 side = -side;
923 }
924 posts
925}
926
927#[derive(Clone, Debug)]
932pub enum RoadSignType {
933 SpeedLimit(u32),
934 Stop,
935 Yield,
936 OneWay,
937 NoEntry,
938 Roundabout,
939 Junction,
940 PedestrianCrossing,
941 SchoolZone,
942 RoadWork,
943}
944
945#[derive(Clone, Debug)]
946pub struct RoadSign {
947 pub position: Vec3,
948 pub facing: Vec3,
949 pub sign_type: RoadSignType,
950 pub post_height: f32,
951 pub code: String,
952 pub text: String,
953 pub station: f32,
954 pub side: i32,
955 pub height_m: f32,
956 pub panel_size: Vec2,
957}
958
959pub fn place_speed_limit_signs(spline: &RoadSpline, profile: &RoadProfile) -> Vec<RoadSign> {
960 let mut signs = Vec::new();
961 let half = profile.total_width * 0.5 + 0.5;
962 let interval = 500.0f32;
963 let mut dist = 0.0f32;
964 while dist < spline.total_length {
965 let (pos, tan) = spline.sample_at_distance(dist); let up = Vec3::Y;
966 let right = tan.cross(up).normalize_or_zero();
967 signs.push(RoadSign {
968 position: pos + right * half + up * 0.1,
969 facing: -right,
970 sign_type: RoadSignType::SpeedLimit(profile.speed_limit_kmh as u32),
971 post_height: 2.0,
972 code: String::new(),
973 text: String::new(),
974 station: dist,
975 side: 1,
976 height_m: 2.0,
977 panel_size: Vec2::new(0.6, 0.75),
978 });
979 dist += interval;
980 }
981 signs
982}
983
984#[derive(Clone, Debug)]
989pub struct GuardRailPost {
990 pub position: Vec3,
991 pub normal: Vec3,
992}
993
994#[derive(Clone, Debug)]
995pub struct GuardRail {
996 pub posts: Vec<GuardRailPost>,
997 pub side: f32,
998 pub rail_height: f32,
999}
1000
1001pub fn generate_guard_rails(spline: &RoadSpline, profile: &RoadProfile) -> (GuardRail, GuardRail) {
1002 let half = profile.total_width * 0.5;
1003 let post_spacing = 4.0f32;
1004 let mut left_posts = Vec::new();
1005 let mut right_posts = Vec::new();
1006 let mut dist = 0.0f32;
1007 while dist < spline.total_length {
1008 let (pos, tan) = spline.sample_at_distance(dist); let up = Vec3::Y;
1009 let right = tan.cross(up).normalize_or_zero();
1010 left_posts.push(GuardRailPost {
1011 position: pos - right * (half + 0.3) + up * 0.0,
1012 normal: -right,
1013 });
1014 right_posts.push(GuardRailPost {
1015 position: pos + right * (half + 0.3) + up * 0.0,
1016 normal: right,
1017 });
1018 dist += post_spacing;
1019 }
1020 let left = GuardRail { posts: left_posts, side: -1.0, rail_height: 0.75 };
1021 let right = GuardRail { posts: right_posts, side: 1.0, rail_height: 0.75 };
1022 (left, right)
1023}
1024
1025pub fn run_all_tests() -> bool {
1030 let mut all_ok = true;
1031
1032 {
1034 let mut spline = RoadSpline::new();
1035 spline.add_point(Vec3::ZERO);
1036 spline.add_point(Vec3::new(10.0, 0.0, 0.0));
1037 spline.add_point(Vec3::new(20.0, 0.0, 0.0));
1038 assert!(spline.total_length > 0.0, "Spline should have length");
1039 let (p, t) = spline.sample_at_distance(5.0); let u = Vec3::Y;
1040 assert!(p.x > 0.0, "Sample should be along positive X");
1041 }
1042
1043 {
1045 let mut terrain = TerrainHeightMap::new(64, 64, 1.0);
1046 terrain.set_height(32, 32, 10.0);
1047 assert_eq!(terrain.get_height(32, 32), 10.0);
1048 let h = terrain.sample_bilinear(32.5, 32.5);
1049 assert!(h > 0.0);
1050 }
1051
1052 {
1054 let mut network = RoadNetwork::new();
1055 let a = network.add_node(Vec3::ZERO, RoadNodeType::Waypoint);
1056 let b = network.add_node(Vec3::new(5.0, 0.0, 0.0), RoadNodeType::Waypoint);
1057 let c = network.add_node(Vec3::new(10.0, 0.0, 0.0), RoadNodeType::Waypoint);
1058 network.add_edge(a, b, 5.0, 50.0, RoadType::PavedRoad, 2, false);
1059 network.add_edge(b, c, 5.0, 50.0, RoadType::PavedRoad, 2, false);
1060 let result = network.dijkstra(a, c);
1061 assert!(result.is_some(), "Dijkstra should find path");
1062 let (cost, path) = result.unwrap();
1063 assert_eq!(path.len(), 3);
1064 }
1065
1066 {
1068 let mut sim = TrafficFlowSim::new(0, 100.0, 30.0);
1069 sim.inject_vehicles(0, 0.5);
1070 sim.step(0.016);
1071 assert!(sim.cells[0].density > 0.0);
1072 }
1073
1074 {
1076 let mut erosion = RoadErosionState::new(8, 8, 1.0);
1077 erosion.wear_grid[0] = 0.9;
1078 let mut rng = SimpleRng::new(1);
1079 for _ in 0..1000 {
1080 erosion.simulate_potholes(&mut rng);
1081 }
1082 let total_potholes: f32 = erosion.pothole_grid.iter().sum();
1084 }
1086
1087 all_ok
1088}
1089
1090pub struct RoadPhysicsConfig {
1095 pub static_friction: f32,
1096 pub kinetic_friction: f32,
1097 pub rolling_resistance: f32,
1098 pub cornering_stiffness: f32,
1099 pub banking_max_deg: f32,
1100 pub hydroplaning_rain_threshold: f32,
1101 pub surface_temperature_effect: f32,
1102 pub grip_reduction_at_temp: f32,
1103}
1104
1105pub struct RoadWeatherState {
1106 pub rain_mm_per_hour: f32,
1107 pub snow_depth_mm: f32,
1108 pub ice_coverage: f32,
1109 pub temperature_celsius: f32,
1110 pub wind_speed_ms: f32,
1111 pub wind_direction: f32,
1112 pub visibility_km: f32,
1113 pub fog_density: f32,
1114}
1115
1116#[derive(Clone, Debug, PartialEq)]
1117pub enum TrafficLightPhase {
1118 Green, Yellow, Red, FlashingRed, FlashingYellow, Off,
1119}
1120
1121#[derive(Clone, Debug)]
1122pub struct TrafficLight {
1123 pub id: u32,
1124 pub position: Vec3,
1125 pub phase: TrafficLightPhase,
1126 pub phase_timer: f32,
1127 pub green_duration: f32,
1128 pub yellow_duration: f32,
1129 pub red_duration: f32,
1130 pub intersection_id: u32,
1131 pub direction: Vec3,
1132}
1133
1134#[derive(Clone, Debug)]
1135pub enum AccidentSeverity { Minor, Moderate, Major, Fatal }
1136
1137#[derive(Clone, Debug)]
1138pub struct RoadAccident {
1139 pub id: u32,
1140 pub position: Vec3,
1141 pub segment_id: u32,
1142 pub severity: AccidentSeverity,
1143 pub blocking_lanes: u32,
1144 pub clearance_time_secs: f32,
1145 pub elapsed_time: f32,
1146 pub is_cleared: bool,
1147}
1148
1149#[derive(Clone, Debug)]
1150pub enum MaintenanceType { Resurfacing, PotholeFilling, MarkingsRepaint, Cleaning, DrainageClear, BridgeInspection, EmergencyRepair, SnowPlowing, SaltApplication }
1151
1152#[derive(Clone, Debug)]
1153pub struct MaintenanceRecord {
1154 pub date_days: u32,
1155 pub work_type: MaintenanceType,
1156 pub cost: f64,
1157 pub crew_count: u32,
1158 pub duration_days: u32,
1159 pub notes: String,
1160}
1161
1162pub struct MaintenanceScheduler {
1163 pub records: Vec<MaintenanceRecord>,
1164 pub segments: HashMap<u32, Vec<MaintenanceRecord>>,
1165}
1166
1167pub struct HorizontalAlignment {
1168 pub elements: Vec<HorizontalElement>,
1169 pub total_length: f32,
1170}
1171
1172#[derive(Clone, Debug)]
1173pub enum HorizontalElement {
1174 Straight { length: f32, azimuth: f32 },
1175 CircularArc { radius: f32, arc_length: f32 },
1176 ClothoidSpiral { parameter: f32, length: f32, direction: f32 },
1177}
1178
1179#[derive(Clone, Debug)]
1180pub struct VerticalAlignmentPoint {
1181 pub station: f32,
1182 pub elevation: f32,
1183 pub grade_in: f32,
1184 pub grade_out: f32,
1185 pub vc_length: f32,
1186}
1187
1188#[derive(Clone, Debug)]
1189pub struct VerticalAlignment {
1190 pub points: Vec<VerticalAlignmentPoint>,
1191}
1192
1193pub struct SightDistanceAnalyzer;
1194
1195pub struct DrainageCulvert {
1196 pub id: u32,
1197 pub position: Vec3,
1198 pub diameter_mm: f32,
1199 pub length: f32,
1200 pub slope: f32,
1201 pub material: String,
1202 pub capacity_l_per_s: f32,
1203 pub is_blocked: bool,
1204}
1205
1206#[derive(Clone, Debug)]
1207pub struct DrainageDitch {
1208 pub id: u32,
1209 pub start: Vec3,
1210 pub end: Vec3,
1211 pub depth: f32,
1212 pub width: f32,
1213 pub side: f32,
1214 pub slope: f32,
1215 pub vegetation: bool,
1216}
1217
1218pub struct DrainageSystem {
1219 pub culverts: Vec<DrainageCulvert>,
1220 pub ditches: Vec<DrainageDitch>,
1221}
1222
1223pub struct MarkingStencil {
1224 pub name: String,
1225 pub polygons: Vec<Vec<Vec2>>,
1226 pub color: Vec3,
1227 pub scale: Vec2,
1228}
1229
1230pub struct FlowAnalyzer;
1231
1232pub struct ProceduralSegmentBuilder;
1233
1234pub struct TerrainSculptor;
1235
1236#[derive(Clone, Debug)]
1237pub enum HeatMapMetric { TrafficDensity, SpeedVariance, AccidentRisk, RoadCondition, NoisePollution }
1238
1239#[derive(Clone, Debug)]
1240pub struct RoadHeatMap {
1241 pub data: Vec<f32>,
1242 pub width: usize,
1243 pub height: usize,
1244 pub scale: f32,
1245 pub metric: HeatMapMetric,
1246}
1247
1248pub struct SlopeAnalyzer;
1249
1250pub struct RoadSpeedProfile {
1251 pub segment_id: u32,
1252 pub distances: Vec<f32>,
1253 pub design_speeds: Vec<f32>,
1254 pub operating_speeds: Vec<f32>,
1255 pub is_consistent: bool,
1256 pub inconsistency_locations: Vec<f32>,
1257}
1258
1259pub fn compute_speed_profile(seg: &RoadSegment, terrain: &TerrainHeightMap) -> RoadSpeedProfile {
1260 let ep = ElevationProfile::compute(&seg.spline, terrain);
1261 let n = ep.distances.len();
1262 let mut design_speeds = Vec::with_capacity(n);
1263 let mut operating_speeds = Vec::with_capacity(n);
1264 let base_speed = seg.profile.speed_limit_kmh;
1265 let grades = analyze_grade_profile(&seg.spline, seg.profile.max_slope_grade);
1266 for i in 0..n {
1267 let grade_factor = if i < grades.len() { 1.0 - (grades[i].grade_percent.abs() / 15.0).min(0.4) } else { 1.0 };
1268 design_speeds.push(base_speed * grade_factor);
1269 operating_speeds.push(base_speed * grade_factor * 0.9);
1270 }
1271 let mut inconsistency_locations = Vec::new();
1272 for i in 1..design_speeds.len() {
1273 let diff = (design_speeds[i] - design_speeds[i-1]).abs();
1274 if diff > 20.0 {
1275 inconsistency_locations.push(ep.distances[i]);
1276 }
1277 }
1278 RoadSpeedProfile {
1279 segment_id: seg.id,
1280 distances: ep.distances,
1281 design_speeds,
1282 operating_speeds,
1283 is_consistent: inconsistency_locations.is_empty(),
1284 inconsistency_locations,
1285 }
1286}
1287
1288#[derive(Clone, Debug)]
1293pub struct RoadAssetEntry {
1294 pub id: u64,
1295 pub name: String,
1296 pub asset_type: RoadAssetType,
1297 pub mesh_id: u32,
1298 pub material_id: u32,
1299}
1300
1301#[derive(Clone, Copy, Debug, PartialEq)]
1302pub enum RoadAssetType { RoadSurface, Curb, GuardRail, LightPost, Sign, Pillar, DrainCover, Manhole, BusStop }
1303
1304pub struct RoadAssetRegistry {
1305 pub entries: HashMap<u64, RoadAssetEntry>,
1306 pub next_id: u64,
1307}
1308
1309pub struct RoadDecal {
1310 pub id: u32,
1311 pub position: Vec3,
1312 pub normal: Vec3,
1313 pub size: Vec2,
1314 pub angle: f32,
1315 pub texture_id: u32,
1316 pub alpha: f32,
1317 pub tint: Vec4,
1318 pub decal_type: RoadDecalType,
1319 pub age: f32,
1320 pub fade_duration: f32,
1321}
1322
1323#[derive(Clone, Copy, Debug, PartialEq)]
1324pub enum RoadDecalType { SkidMark, OilSpill, CrackPattern, WearPattern, WaterStain, PaintDrip }
1325
1326pub struct RoadDecalManager {
1327 pub decals: Vec<RoadDecal>,
1328 pub next_id: u32,
1329}
1330
1331pub fn angle_between_vectors(a: Vec3, b: Vec3) -> f32 {
1332 let dot = a.dot(b).clamp(-1.0, 1.0);
1333 dot.acos()
1334}
1335
1336pub fn project_point_onto_plane(point: Vec3, plane_origin: Vec3, plane_normal: Vec3) -> Vec3 {
1337 let d = (point - plane_origin).dot(plane_normal);
1338 point - plane_normal * d
1339}
1340
1341pub fn barycentric_coords(p: Vec2, a: Vec2, b: Vec2, c: Vec2) -> Vec3 {
1342 let v0 = b - a;
1343 let v1 = c - a;
1344 let v2 = p - a;
1345 let d00 = v0.dot(v0);
1346 let d01 = v0.dot(v1);
1347 let d11 = v1.dot(v1);
1348 let d20 = v2.dot(v0);
1349 let d21 = v2.dot(v1);
1350 let denom = d00 * d11 - d01 * d01;
1351 if denom.abs() < 1e-8 { return Vec3::new(1.0/3.0, 1.0/3.0, 1.0/3.0); }
1352 let v = (d11 * d20 - d01 * d21) / denom;
1353 let w = (d00 * d21 - d01 * d20) / denom;
1354 Vec3::new(1.0 - v - w, v, w)
1355}
1356
1357pub fn ray_sphere_intersect(ray_origin: Vec3, ray_dir: Vec3, sphere_center: Vec3, sphere_radius: f32) -> Option<f32> {
1358 let oc = ray_origin - sphere_center;
1359 let a = ray_dir.dot(ray_dir);
1360 let b = 2.0 * oc.dot(ray_dir);
1361 let c = oc.dot(oc) - sphere_radius * sphere_radius;
1362 let disc = b * b - 4.0 * a * c;
1363 if disc < 0.0 { return None; }
1364 let sqrt_disc = disc.sqrt();
1365 let t1 = (-b - sqrt_disc) / (2.0 * a);
1366 let t2 = (-b + sqrt_disc) / (2.0 * a);
1367 if t1 > 0.0 { Some(t1) } else if t2 > 0.0 { Some(t2) } else { None }
1368}
1369
1370pub fn ray_plane_intersect(ray_origin: Vec3, ray_dir: Vec3, plane_origin: Vec3, plane_normal: Vec3) -> Option<f32> {
1371 let denom = ray_dir.dot(plane_normal);
1372 if denom.abs() < 1e-6 { return None; }
1373 let t = (plane_origin - ray_origin).dot(plane_normal) / denom;
1374 if t > 0.0 { Some(t) } else { None }
1375}
1376
1377pub fn ray_cast_terrain(ray_origin: Vec3, ray_dir: Vec3, terrain: &TerrainHeightMap, max_dist: f32, steps: usize) -> Option<Vec3> {
1378 let dt = max_dist / steps as f32;
1379 for i in 0..steps {
1380 let t = i as f32 * dt;
1381 let pos = ray_origin + ray_dir * t;
1382 let terrain_h = terrain.sample_bilinear(pos.x, pos.z);
1383 if pos.y <= terrain_h {
1384 let mut lo = if i > 0 { (i - 1) as f32 * dt } else { 0.0 };
1386 let mut hi = t;
1387 for _ in 0..8 {
1388 let mid = (lo + hi) * 0.5;
1389 let p = ray_origin + ray_dir * mid;
1390 if p.y <= terrain.sample_bilinear(p.x, p.z) { hi = mid; } else { lo = mid; }
1391 }
1392 let final_pos = ray_origin + ray_dir * (lo + hi) * 0.5;
1393 return Some(final_pos);
1394 }
1395 }
1396 None
1397}
1398
1399#[derive(Clone, Debug)]
1404pub struct RoadCameraPath {
1405 pub segment_id: u32,
1406 pub height_above_road: f32,
1407 pub lateral_offset: f32,
1408 pub look_ahead_distance: f32,
1409 pub fov: f32,
1410 pub smooth_factor: f32,
1411}
1412
1413pub struct RoadRuntimeUpdate {
1414 pub closed_segments: HashSet<u32>,
1415 pub detour_routes: HashMap<u32, Vec<u32>>,
1416 pub speed_overrides: HashMap<u32, f32>,
1417 pub construction_zones: Vec<(u32, f32, f32)>, pub traffic_lights: Vec<TrafficLight>,
1419 pub accidents: Vec<RoadAccident>,
1420 pub decals: RoadDecalManager,
1421 pub weather: RoadWeatherState,
1422}
1423
1424#[derive(Clone, Debug, PartialEq, Eq, Hash)]
1425pub enum RoadToolKey {
1426 PlaceDirtTrack, PlaceGravelRoad, PlacePavedRoad, PlaceHighway2, PlaceHighway4,
1427 PlaceMotorway, PlaceAlley, PlaceBridge, PlaceTunnel, PlaceElevatedHighway,
1428 PlaceIntersectionT, PlaceIntersectionX, PlaceRoundabout,
1429 FinishRoad, CancelRoad, UndoAction, RedoAction,
1430 ToggleTrafficSim, ToggleElevationProfile, ToggleOverlay,
1431 GenerateProceduralRoads, ValidateNetwork, ExportNetwork,
1432}
1433
1434#[derive(Clone, Debug)]
1435pub struct RoadToolKeyBindings {
1436 pub bindings: HashMap<RoadToolKey, String>,
1437}
1438
1439pub struct RoadExporter;
1440
1441pub struct RoadToolPanelState {
1442 pub selected_tab: RoadToolTab,
1443 pub show_advanced_settings: bool,
1444 pub road_type_dropdown_open: bool,
1445 pub selected_segment_info_visible: bool,
1446 pub elevation_chart_height: f32,
1447 pub traffic_chart_height: f32,
1448 pub minimap_size: f32,
1449 pub snap_enabled: bool,
1450 pub snap_radius: f32,
1451 pub auto_bridge_enabled: bool,
1452 pub auto_tunnel_enabled: bool,
1453 pub terrain_deform_enabled: bool,
1454 pub splat_paint_enabled: bool,
1455 pub erosion_enabled: bool,
1456 pub procedural_generation_params: ProceduralGenParams,
1457}
1458
1459#[derive(Clone, Copy, Debug, PartialEq)]
1460pub enum RoadToolTab { Placement, Editing, Traffic, Erosion, Procedural, Statistics, Export }
1461
1462#[derive(Clone, Debug)]
1463pub struct ProceduralGenParams {
1464 pub city_node_count: u32,
1465 pub min_city_spacing: f32,
1466 pub max_city_spacing: f32,
1467 pub use_terrain_following: bool,
1468 pub slope_avoidance_weight: f32,
1469 pub road_type_for_generation: RoadType,
1470 pub random_seed: u64,
1471}
1472
1473impl Default for ProceduralGenParams {
1474 fn default() -> Self {
1475 ProceduralGenParams {
1476 city_node_count: 8,
1477 min_city_spacing: 50.0,
1478 max_city_spacing: 200.0,
1479 use_terrain_following: true,
1480 slope_avoidance_weight: 0.7,
1481 road_type_for_generation: RoadType::PavedRoad,
1482 random_seed: 42,
1483 }
1484 }
1485}
1486
1487pub fn integration_test_road_tool() {
1488 let mut tool = TerrainRoadTool::with_sample_terrain(99);
1489 tool.begin_road_placement(RoadType::PavedRoad);
1490 tool.add_road_point(Vec3::new(10.0, 0.0, 10.0));
1491 tool.add_road_point(Vec3::new(50.0, 0.0, 10.0));
1492 tool.add_road_point(Vec3::new(90.0, 0.0, 50.0));
1493 tool.finish_road_placement();
1494 let center = Vec3::new(90.0, 0.0, 90.0);
1495 let arms = vec![Vec3::new(90.0, 0.0, 70.0), Vec3::new(110.0, 0.0, 90.0), Vec3::new(90.0, 0.0, 110.0), Vec3::new(70.0, 0.0, 90.0)];
1496 tool.place_roundabout(center, arms);
1497 tool.add_city_node(Vec3::new(20.0, 0.0, 20.0), RoadNodeType::CityCenter, 10000);
1498 tool.add_city_node(Vec3::new(100.0, 0.0, 20.0), RoadNodeType::Suburb, 3000);
1499 tool.add_city_node(Vec3::new(60.0, 0.0, 100.0), RoadNodeType::Suburb, 5000);
1500 tool.generate_procedural_roads();
1501 for _ in 0..100 { tool.step_traffic_simulation(LWR_DT); }
1502 tool.run_erosion_simulation(EROSION_TIMESTEPS);
1503 let stats = tool.statistics();
1504 assert!(stats.total_segments > 0);
1505 let data = tool.serialize();
1506 let mut tool2 = TerrainRoadTool::new(256, 256, 1.0);
1507 tool2.deserialize(&data);
1508 assert_eq!(tool2.segments.len(), tool.segments.len());
1509}
1510
1511#[derive(Debug, Clone)]
1517pub struct GradeOptimizer {
1518 pub max_grade: f32, pub max_cut_depth: f32, pub max_fill_height: f32, pub balance_earthwork: bool, pub segments: Vec<GradeSegment>,
1519}
1520
1521#[derive(Clone, Debug, PartialEq)]
1522pub enum LaneType { Travel, Turning, Parking, Bike, Shoulder, Median, Sidewalk, ThroughLane, TurnLane, CycleLane, BusLane, EmergencyStoppingLane, Auxiliary, Ramp, Acceleration, Deceleration }
1523
1524#[derive(Debug, Clone)]
1525pub struct Lane {
1526 pub lane_type: LaneType, pub width: f32, pub left_curb: bool, pub right_curb: bool,
1527 pub surface: String, pub marking_left: Option<String>, pub marking_right: Option<String>,
1528}
1529
1530pub struct CrossSection {
1531 pub lanes_left: Vec<Lane>, pub lanes_right: Vec<Lane>, pub median_width: f32,
1532 pub slope_cut: f32, pub slope_fill: f32, pub ditch_width: f32, pub ditch_depth: f32, pub superelevation: f32,
1533}
1534impl CrossSection {
1535 pub fn four_lane_divided() -> Self {
1536 let lane = Lane { lane_type: LaneType::Travel, width: LANE_WIDTH, left_curb: false, right_curb: false, surface: "asphalt".into(), marking_left: None, marking_right: None };
1537 Self { lanes_left: vec![lane.clone(), lane.clone()], lanes_right: vec![lane.clone(), lane.clone()], median_width: 3.0, slope_cut: 0.5, slope_fill: 0.33, ditch_width: DITCH_WIDTH, ditch_depth: DITCH_DEPTH, superelevation: 0.0 }
1538 }
1539 pub fn two_lane() -> Self {
1540 let lane = Lane { lane_type: LaneType::Travel, width: LANE_WIDTH, left_curb: false, right_curb: false, surface: "asphalt".into(), marking_left: None, marking_right: None };
1541 Self { lanes_left: vec![lane.clone()], lanes_right: vec![lane.clone()], median_width: 0.0, slope_cut: 0.5, slope_fill: 0.33, ditch_width: DITCH_WIDTH, ditch_depth: DITCH_DEPTH, superelevation: 0.0 }
1542 }
1543 pub fn total_width(&self) -> f32 {
1544 let left: f32 = self.lanes_left.iter().map(|l| l.width).sum();
1545 let right: f32 = self.lanes_right.iter().map(|l| l.width).sum();
1546 left + right + self.median_width
1547 }
1548 pub fn generate_profile_points(&self, elevation: f32, _terrain_elev: f32) -> Vec<(f32, f32)> {
1549 vec![(0.0, elevation), (self.total_width(), elevation)]
1550 }
1551}
1552
1553pub struct PavementLayer {
1554 pub name: String, pub material: String, pub thickness_mm: f32, pub elastic_modulus_mpa: f32, pub poisson_ratio: f32,
1555}
1556
1557pub struct PavementStructure {
1558 pub layers: Vec<PavementLayer>, pub subgrade_cbr: f32, pub design_esal: f64, pub reliability: f32,
1559}
1560
1561#[derive(Debug, Clone, PartialEq)]
1562pub enum TurnType { Left, Through, Right, UTurn }
1563
1564#[derive(Debug, Clone)]
1565pub struct ApproachMovement { pub volume_vph: f32, pub phf: f32, pub turn_type: TurnType, pub shared_lane: bool }
1566
1567#[derive(Debug, Clone)]
1568pub struct SignalPhase { pub movements: Vec<usize>, pub green_time: f32, pub yellow_time: f32, pub all_red_time: f32 }
1569
1570pub struct IntersectionCapacityAnalysis {
1571 pub approaches: Vec<ApproachMovement>, pub phases: Vec<SignalPhase>, pub cycle_length: f32, pub saturation_flow_base: f32,
1572}
1573
1574#[derive(Debug, Clone)]
1575pub struct RoundaboutEntry { pub approach_volume: f32, pub entry_width: f32, pub entry_radius: f32, pub flare_length: f32, pub inscribed_diameter: f32,
1576 pub entry_id: u32, pub bearing_deg: f32, pub lane_count: u32, pub entry_width_m: f32, pub flare_length_m: f32, pub approach_speed_kph: f32, pub design_flow_vph: u32, pub pedestrian_crossing: bool,
1577}
1578
1579#[derive(Debug, Clone)]
1580pub struct RoundaboutDesign {
1581 pub inscribed_diameter: f32, pub central_island_diameter: f32, pub circulatory_width: f32,
1582 pub truck_apron_width: f32, pub entries: Vec<RoundaboutEntry>, pub design_vehicle: String,
1583}
1584
1585#[derive(Debug, Clone, PartialEq)]
1586pub enum BarrierType { WBeam, ThreeBeam, ConcreteBarrier, CableBarrier, BridgeRail, Attenuator }
1587
1588#[derive(Debug, Clone)]
1589pub struct GuardrailSection { pub barrier_type: BarrierType, pub start_station: f32, pub end_station: f32, pub side: i32, pub height_mm: f32, pub post_spacing_m: f32, pub terminal_type: String }
1590
1591#[derive(Debug, Clone)]
1592pub struct BarrierSystem { pub sections: Vec<GuardrailSection>, pub clear_zone_width: f32, pub design_speed_kmh: f32 }
1593
1594pub enum SignType { Regulatory, Warning, Guide, Information }
1595
1596
1597
1598#[derive(Debug, Clone)]
1599pub struct SignInventory { pub signs: Vec<RoadSign>, pub delineators: Vec<(f32, i32)>, pub mile_markers: Vec<(f32, u32)> }
1600
1601pub struct LightingFixture { pub station: f32, pub side: i32, pub pole_height_m: f32, pub lamp_lumens: f32 }
1602
1603pub struct RoadLightingSystem { pub fixtures: Vec<LightingFixture>, pub spacing_m: f32 }
1604
1605pub struct NoiseBarrier { pub start_station: f32, pub end_station: f32, pub height_m: f32, pub side: i32, pub insertion_loss_db: f32 }
1606
1607pub struct NoiseAnalysis { pub barriers: Vec<NoiseBarrier>, pub source_level_db: f32, pub receptor_distance_m: f32 }
1608
1609pub struct OriginDestinationMatrix { pub zones: Vec<String>, pub matrix: Vec<Vec<f32>> }
1610
1611pub struct PavementConditionIndex { pub pci_value: f32, pub distress_types: Vec<(String, f32, f32)>, pub sample_unit_area: f32 }
1612
1613pub struct AssetRecord {
1614 pub asset_id: u32, pub asset_type: String, pub station: f32, pub installation_year: u32,
1615 pub condition_score: f32, pub replacement_cost: f32, pub remaining_life_years: f32, pub maintenance_history: Vec<(u32, String, f32)>,
1616}
1617
1618pub struct AssetManagementSystem { pub assets: Vec<AssetRecord>, pub annual_budget: f32, pub current_year: u32 }
1619
1620pub struct ConstructionActivity {
1621 pub id: u32, pub name: String, pub duration_days: u32, pub predecessors: Vec<u32>, pub resources: HashMap<String, f32>,
1622 pub cost: f32, pub early_start: u32, pub early_finish: u32, pub late_start: u32, pub late_finish: u32, pub float: i32,
1623}
1624
1625pub struct CriticalPathMethod { pub activities: Vec<ConstructionActivity> }
1626
1627#[derive(Debug, Clone)]
1628pub struct UtilityLine { pub id: u32, pub utility_type: String, pub depth_m: f32, pub polyline: Vec<Vec3>, pub diameter_mm: f32 }
1629
1630#[derive(Debug, Clone)]
1631pub struct UtilityConflict { pub utility_id: u32, pub conflict_station: f32, pub conflict_type: String, pub relocation_cost: f32, pub criticality: u8 }
1632
1633#[derive(Debug, Clone)]
1634pub struct UtilityConflictDetector { pub utilities: Vec<UtilityLine>, pub conflicts: Vec<UtilityConflict> }
1635
1636pub struct HydrologicBasin {
1637 pub area_ha: f32, pub runoff_coefficient: f32, pub tc_minutes: f32, pub land_use: String,
1638}
1639
1640pub struct CulvertDesign { pub diameter_mm: f32, pub length_m: f32, pub slope: f32, pub manning_n: f32 }
1641
1642pub struct VehicleEmissionsFactor { pub vehicle_class: String, pub co2_g_per_km: f32, pub fuel_l_per_100km: f32 }
1643
1644pub struct RoadEmissionsModel { pub factors: Vec<VehicleEmissionsFactor>, pub traffic_volumes: HashMap<String, f32>, pub segment_length_km: f32 }
1645
1646pub const ROAD_TOOL_VERSION: &str = "1.0.0";
1647pub const MAX_ROAD_NETWORK_SEGMENTS: usize = 100_000;
1648pub const MAX_ROAD_NETWORK_NODES: usize = 50_000;
1649pub const DEFAULT_LANE_WIDTH_M: f32 = 3.6;
1650pub const DEFAULT_SHOULDER_WIDTH_M: f32 = 1.5;
1651pub const MIN_HORIZONTAL_RADIUS_M: f32 = 15.0;
1652pub const MAX_GRADE_PERCENT_HIGHWAY: f32 = 6.0;
1653pub const MAX_GRADE_PERCENT_LOCAL: f32 = 12.0;
1654pub const STOPPING_SIGHT_DISTANCE_120KMH_M: f32 = 285.0;
1655pub const STOPPING_SIGHT_DISTANCE_80KMH_M: f32 = 130.0;
1656pub const STOPPING_SIGHT_DISTANCE_50KMH_M: f32 = 65.0;
1657pub const BRIDGE_LIVE_LOAD_KPA: f32 = 9.6;
1658pub const CULVERT_RETURN_PERIOD_YRS: f32 = 25.0;
1659pub const DEFAULT_FRICTION_COEFFICIENT: f32 = 0.35;
1660pub const AASHTO_STOPPING_DECELERATION_MS2: f32 = 3.4;
1661pub const PAVEMENT_DESIGN_PERIOD_YEARS: u32 = 20;
1662pub const TRAFFIC_GROWTH_RATE_PERCENT: f32 = 2.0;
1663
1664pub fn road_tool_module_info() -> HashMap<&'static str, &'static str> {
1665 let mut info = HashMap::new();
1666 info.insert("version", ROAD_TOOL_VERSION);
1667 info.insert("design_standard", "AASHTO Green Book 2018");
1668 info.insert("traffic_model", "LWR Godunov");
1669 info.insert("pavement_design", "AASHTO 1993");
1670 info
1671}
1672
1673pub fn run_extended_road_tool_tests() {
1678 let profile: Vec<(f32, f32)> = (0..100).map(|i| (i as f32 * 10.0, (i as f32 * 0.1).sin() * 5.0 + 10.0)).collect();
1680 let mut opt = GradeOptimizer::new(0.08);
1681 let optimized = opt.optimize(&profile, 1000.0);
1682 assert!(!optimized.is_empty());
1683 let (cut, fill) = opt.total_earthwork();
1684 assert!(cut >= 0.0 && fill >= 0.0);
1685 let mhd = opt.mass_haul_diagram();
1686 assert!(!mhd.is_empty());
1687
1688 let xs = CrossSection::four_lane_divided();
1690 assert!(xs.total_width() > 10.0);
1691 let pts = xs.generate_profile_points(100.0, 95.0);
1692 assert!(!pts.is_empty());
1693
1694 let pav = PavementStructure::recommend_structure(5.0, 5_000_000.0);
1696 assert!(pav.total_thickness_mm() > 0.0);
1697 assert!(pav.structural_number() > 0.0);
1698
1699 let mut ica = IntersectionCapacityAnalysis::new(90.0);
1701 ica.add_approach(800.0, 0.92, TurnType::Through);
1702 ica.add_approach(200.0, 0.90, TurnType::Left);
1703 ica.add_phase(vec![0], 40.0); ica.add_phase(vec![1], 20.0);
1704 assert!(ica.vc_ratio(0) > 0.0);
1705 let los = ica.level_of_service(0);
1706 assert!(los >= 'A' && los <= 'F');
1707 let opt_c = ica.webster_optimal_cycle(1000.0);
1708 assert!(opt_c >= 40.0 && opt_c <= 150.0);
1709
1710 let mut rab = RoundaboutDesign::single_lane(40.0);
1712 rab.add_entry(600.0, 4.5);
1713 assert!(rab.entry_capacity(&rab.entries[0]) > 0.0);
1714 assert_eq!(rab.generate_geometry(Vec3::ZERO).len(), 65);
1715
1716 let mut bs = BarrierSystem::new(110.0);
1718 bs.auto_place_barriers(&[(100.0, 2.0, 50.0)]);
1719 assert!(!bs.sections.is_empty());
1720 assert!(bs.sections[0].post_count() > 0);
1721
1722 let mut inv = SignInventory::new();
1724 inv.add_sign(RoadSign::speed_limit(50.0, -1, 100));
1725 inv.add_sign(RoadSign::stop(200.0, 1));
1726 inv.auto_place_delineators(1000.0, 100.0);
1727 inv.auto_place_mile_markers(1000.0);
1728 assert_eq!(inv.signs.len(), 2);
1729 assert!(!inv.delineators.is_empty());
1730
1731 let mut lighting = RoadLightingSystem::new(40.0);
1733 lighting.auto_place(500.0);
1734 assert!(!lighting.fixtures.is_empty());
1735
1736 let mut noise = NoiseAnalysis::new(75.0, 50.0);
1738 noise.barriers.push(NoiseBarrier::concrete(0.0, 200.0, 1, 3.5));
1739 assert!(noise.receptor_level_db() < 75.0);
1740
1741 let mut od = OriginDestinationMatrix::new(vec!["A".into(), "B".into(), "C".into()]);
1743 od.set(0, 1, 500.0); od.set(1, 2, 300.0);
1744 assert!((od.total_trips() - 800.0).abs() < 0.01);
1745
1746 let mut pci = PavementConditionIndex::new(230.0);
1748 pci.add_distress("alligator_cracking", 23.0, 2.0);
1749 pci.calculate_pci();
1750 assert!(pci.pci_value >= 0.0 && pci.pci_value <= 100.0);
1751 let _ = pci.condition_category();
1752 let _ = pci.recommended_treatment();
1753
1754 let mut ams = AssetManagementSystem::new(1_000_000.0, 2024);
1756 let mut asset = AssetRecord::new(1, "asphalt_pavement", 500.0, 2010, 500_000.0);
1757 asset.update_condition(2024);
1758 asset.add_maintenance(2018, "thin_overlay", 50_000.0);
1759 ams.add_asset(asset);
1760 assert!(ams.network_condition_index() >= 0.0);
1761 let _ = ams.budget_allocation();
1762
1763 let cpm = CriticalPathMethod::standard_road_schedule();
1765 assert!(!cpm.critical_path().is_empty());
1766 assert!(cpm.project_duration() > 0);
1767 assert!(cpm.total_cost() > 0.0);
1768
1769 let mut ucd = UtilityConflictDetector::new();
1771 ucd.add_utility(UtilityLine::water_main(1, 1.5, vec![Vec3::new(50.0, -1.5, 0.0), Vec3::new(50.0, -1.5, 100.0)]));
1772 let road_pts: Vec<Vec3> = (0..20).map(|i| Vec3::new(i as f32 * 10.0, 0.0, 50.0)).collect();
1773 ucd.detect_conflicts(&road_pts, 10.0);
1774 let _ = ucd.total_relocation_cost();
1775
1776 let basin = HydrologicBasin::new(50.0, "suburban");
1778 let q = basin.peak_discharge_rational(HydrologicBasin::idf_intensity(25.0, basin.tc_minutes));
1779 assert!(q > 0.0);
1780 let d_mm = CulvertDesign::size_for_discharge(q, 0.01);
1781 assert!(d_mm >= 300.0);
1782 let culvert = CulvertDesign::new(d_mm, 15.0, 0.01);
1783 assert!(culvert.full_flow_capacity() > 0.0);
1784
1785 let mut szm = SpeedZoneManager::new(100);
1787 szm.add_zone(SpeedZone::school_zone(1, 500.0, 700.0));
1788 assert_eq!(szm.speed_at_station(600.0, 800), 30);
1789 assert_eq!(szm.speed_at_station(600.0, 900), 100);
1790
1791 let mut em = RoadEmissionsModel::new(5.0);
1793 em.set_volume("passenger_car", 10000.0);
1794 assert!(em.daily_co2_kg() > 0.0);
1795 assert!(em.annual_co2_tonnes() > 0.0);
1796
1797 let info = road_tool_module_info();
1799 assert!(info.contains_key("version"));
1800 assert_eq!(info["design_standard"], "AASHTO Green Book 2018");
1801}
1802
1803#[derive(Debug, Clone)]
1808pub struct SuperelevationTable {
1809 pub design_speed_kph: f32,
1810 pub max_superelevation: f32,
1811 pub table: Vec<(f32, f32)>,
1813}
1814
1815#[derive(Debug, Clone, Default)]
1816pub struct HorizontalCurve {
1817 pub radius_m: f32,
1818 pub delta_angle_deg: f32,
1819 pub design_speed_kph: f32,
1820 pub lane_width_m: f32,
1821 pub number_of_lanes: u32,
1822}
1823
1824#[derive(Debug, Clone, PartialEq)]
1825pub enum VerticalCurveType { Crest, Sag }
1826
1827#[derive(Debug, Clone)]
1828pub struct VerticalCurve {
1829 pub curve_type: VerticalCurveType,
1830 pub g1_percent: f32,
1831 pub g2_percent: f32,
1832 pub length_m: f32,
1833 pub pvi_station_m: f32,
1834 pub pvi_elevation_m: f32,
1835 pub design_speed_kph: f32,
1836}
1837
1838#[derive(Debug, Clone, Default)]
1839pub struct NetworkLink {
1840 pub id: u32,
1841 pub from_node: u32,
1842 pub to_node: u32,
1843 pub free_flow_time_min: f32,
1844 pub capacity_veh_per_hour: f32,
1845 pub alpha: f32,
1846 pub beta: f32,
1847 pub current_flow: f32,
1848}
1849
1850#[derive(Debug, Clone)]
1851pub struct OdDemand {
1852 pub origin: u32,
1853 pub destination: u32,
1854 pub demand_vph: f32,
1855}
1856
1857#[derive(Debug, Clone)]
1858pub struct NetworkEquilibriumSolver {
1859 pub links: Vec<NetworkLink>,
1860 pub nodes: Vec<u32>,
1861 pub od_demands: Vec<OdDemand>,
1862 pub iteration_count: u32,
1863 pub convergence_gap: f32,
1864}
1865
1866#[derive(Debug, Clone)]
1867pub enum PavementDistressType {
1868 Alligator, Bleeding, BlockCracking, BumpsAndSags, Corrugation,
1869 Depression, EdgeCracking, JointReflection, LaneShoulder, LongTransCracking,
1870 PatchingUtility, PolishedAggregate, Potholes, Railroad, Rutting,
1871 Shoving, Slippage, Swell, Raveling,
1872}
1873
1874#[derive(Debug, Clone)]
1875pub struct DistressObservation {
1876 pub distress_type: PavementDistressType,
1877 pub quantity: f32,
1878 pub density_percent: f32,
1879 pub severity: u8, }
1881
1882pub struct PavementSampleUnit {
1883 pub unit_id: u32,
1884 pub area_m2: f32,
1885 pub distresses: Vec<DistressObservation>,
1886 pub last_survey_year: u32,
1887}
1888
1889pub struct PavementManagementSystem {
1890 pub sample_units: Vec<PavementSampleUnit>,
1891 pub annual_budget: f32,
1892 pub treatment_unit_costs: HashMap<&'static str, f32>, }
1894
1895#[derive(Debug, Clone, Default)]
1896pub struct SkidResistanceMeasurement {
1897 pub station_m: f32,
1898 pub skid_number: f32, pub international_friction_index: f32,
1900 pub texture_depth_mm: f32,
1901 pub surface_type: String,
1902}
1903
1904#[derive(Debug, Clone, Default)]
1905pub struct FrictionInventory {
1906 pub measurements: Vec<SkidResistanceMeasurement>,
1907 pub minimum_acceptable_sn: f32,
1908}
1909
1910pub fn run_geometry_tests() {
1911 let curve = HorizontalCurve::new(500.0, 30.0, 80.0);
1913 assert!(curve.arc_length_m() > 0.0);
1914 assert!(curve.tangent_length_m() > 0.0);
1915 assert!(curve.long_chord_m() < curve.arc_length_m());
1916 assert!(curve.min_radius_m() > 0.0);
1917 let _ = curve.design_speed_ok();
1918 let _ = curve.sight_clearance_m();
1919
1920 let tbl = SuperelevationTable::for_rural_highway(100.0);
1922 assert!(tbl.required_superelevation(500.0) > 0.0);
1923 assert!(tbl.transition_length_m(0.06, 3.65) > 0.0);
1924
1925 let vc = VerticalCurve::new(3.0, -2.0, 200.0, 1000.0, 250.0, 100.0);
1927 assert_eq!(vc.curve_type, VerticalCurveType::Crest);
1928 assert!(vc.a_value() > 0.0);
1929 assert!(vc.k_value() > 0.0);
1930 let elev = vc.elevation_at_station(1000.0);
1931 assert!(elev > 0.0);
1932 let _ = vc.high_low_point_station();
1933 let _ = vc.is_adequate();
1934
1935 let mut solver = NetworkEquilibriumSolver::new();
1937 solver.add_link(NetworkLink::new(1, 1, 2, 5.0, 1000.0));
1938 solver.add_link(NetworkLink::new(2, 2, 3, 3.0, 800.0));
1939 solver.add_demand(1, 3, 500.0);
1940 solver.solve(10, 1.0);
1941 let vht = solver.total_vehicle_hours_traveled();
1942 assert!(vht >= 0.0);
1943
1944 let mut pms = PavementManagementSystem::new(500_000.0);
1946 let mut unit = PavementSampleUnit::new(1, 1000.0);
1947 unit.add_distress(DistressObservation {
1948 distress_type: PavementDistressType::Alligator,
1949 quantity: 50.0, density_percent: 5.0, severity: 2
1950 });
1951 unit.add_distress(DistressObservation {
1952 distress_type: PavementDistressType::Rutting,
1953 quantity: 200.0, density_percent: 20.0, severity: 1
1954 });
1955 let pci = unit.compute_pci();
1956 assert!(pci >= 0.0 && pci <= 100.0);
1957 let _ = unit.condition_rating();
1958 let _ = unit.recommended_treatment();
1959 assert!(unit.predicted_pci(5) <= pci);
1960 pms.add_unit(unit);
1961 assert!(pms.network_pci() >= 0.0);
1962 assert!(!pms.prioritized_treatment_list().is_empty());
1963
1964 let mut inv = FrictionInventory::new();
1966 inv.add(SkidResistanceMeasurement::new(100.0, 45.0, 1.2, "Dense Graded Asphalt"));
1967 inv.add(SkidResistanceMeasurement::new(200.0, 28.0, 0.6, "Polished Surface"));
1968 assert!(!inv.deficient_stations().is_empty());
1969 assert!(inv.average_skid_number() > 0.0);
1970 let m = &inv.measurements[0];
1971 assert!(m.wet_stopping_distance_m(80.0) > 0.0);
1972 let _ = m.friction_class();
1973}
1974
1975pub fn road_tool_comprehensive_self_test() {
1976 run_geometry_tests();
1977 run_extended_road_tool_tests();
1978 let _ = road_tool_module_info();
1980}
1981
1982#[derive(Debug, Clone, PartialEq)]
1987pub enum MarkingType {
1988 CenterlineSolid, CenterlineDashed, EdgeLineSolid, EdgeLineDashed,
1989 StopBar, Crosswalk, TurnArrow, YieldLine, LaneDropArrow,
1990 BicycleLaneMark, BusLaneMark, ParkingBay, NoPassingZone,
1991}
1992
1993#[derive(Debug, Clone)]
1994pub struct RoadMarking {
1995 pub id: u32,
1996 pub marking_type: MarkingType,
1997 pub start_station_m: f32,
1998 pub end_station_m: f32,
1999 pub lateral_offset_m: f32,
2000 pub color: [u8; 3],
2001 pub retroreflectivity_mcd: f32, pub last_applied_year: u32,
2003}
2004
2005pub struct MarkingInventory {
2006 pub markings: Vec<RoadMarking>,
2007 pub segment_length_m: f32,
2008}
2009
2010impl MarkingInventory {
2011 pub fn new(length: f32) -> Self { Self { markings: Vec::new(), segment_length_m: length } }
2012 pub fn add(&mut self, m: RoadMarking) { self.markings.push(m); }
2013 pub fn generate_standard_markings(&mut self, lane_width: f32, num_lanes: u32) {
2014 for i in 0..num_lanes {
2015 self.markings.push(RoadMarking { id: i, marking_type: MarkingType::CenterlineSolid, start_station_m: 0.0, end_station_m: self.segment_length_m, lateral_offset_m: lane_width * i as f32, color: [255,255,255], retroreflectivity_mcd: 300.0, last_applied_year: 2020 });
2016 }
2017 }
2018 pub fn total_marking_area_m2(&self) -> f32 {
2019 self.markings.iter().map(|m| (m.end_station_m - m.start_station_m) * 0.15).sum()
2020 }
2021 pub fn inadequate_markings(&self) -> Vec<&RoadMarking> {
2022 self.markings.iter().filter(|m| m.retroreflectivity_mcd < 100.0).collect()
2023 }
2024 pub fn restriping_cost_estimate(&self, cost_per_m2: f32) -> f32 {
2025 self.inadequate_markings().iter().map(|m| (m.end_station_m - m.start_station_m) * 0.15 * cost_per_m2).sum()
2026 }
2027}
2028
2029#[derive(Debug, Clone)]
2030pub enum AssetCategory {
2031 Pavement, Bridge, Culvert, SignStructure, Guardrail, Lighting,
2032 TrafficSignal, Drainage, Marking, Sidewalk, RetainingWall,
2033}
2034
2035#[derive(Debug, Clone)]
2036pub struct RoadAsset {
2037 pub asset_id: u32,
2038 pub category: AssetCategory,
2039 pub location_station_m: f32,
2040 pub installation_year: u32,
2041 pub design_life_years: u32,
2042 pub replacement_cost_usd: f32,
2043 pub current_condition: f32, pub last_inspection_year: u32,
2045}
2046
2047#[derive(Clone, Debug, Default)]
2048pub struct AssetRegistry {
2049 pub assets: Vec<RoadAsset>,
2050 pub current_year: u32,
2051}
2052impl AssetRegistry {
2053 pub fn new(year: u32) -> Self { Self { assets: Vec::new(), current_year: year } }
2054 pub fn register(&mut self, asset: RoadAsset) { self.assets.push(asset); }
2055 pub fn total_replacement_value(&self) -> f32 { self.assets.iter().map(|a| a.replacement_cost_usd).sum() }
2056 pub fn total_book_value(&self) -> f32 {
2057 self.assets.iter().map(|a| {
2058 let age = self.current_year.saturating_sub(a.installation_year) as f32;
2059 let remaining = (a.design_life_years as f32 - age).max(0.0) / a.design_life_years as f32;
2060 a.replacement_cost_usd * remaining
2061 }).sum()
2062 }
2063 pub fn assets_due_for_replacement(&self) -> Vec<&RoadAsset> {
2064 self.assets.iter().filter(|a| {
2065 let age = self.current_year.saturating_sub(a.installation_year);
2066 age >= a.design_life_years
2067 }).collect()
2068 }
2069 pub fn five_year_replacement_cost(&self) -> f32 {
2070 self.assets.iter().filter(|a| {
2071 let age = self.current_year.saturating_sub(a.installation_year);
2072 age + 5 >= a.design_life_years
2073 }).map(|a| a.replacement_cost_usd).sum()
2074 }
2075 pub fn assets_needing_inspection(&self) -> Vec<&RoadAsset> {
2076 self.assets.iter().filter(|a| {
2077 self.current_year.saturating_sub(a.last_inspection_year) >= 2
2078 }).collect()
2079 }
2080 pub fn summary_by_category(&self) -> HashMap<String, usize> {
2081 let mut map = HashMap::new();
2082 for a in &self.assets {
2083 let cat = format!("{:?}", a.category);
2084 *map.entry(cat).or_insert(0) += 1;
2085 }
2086 map
2087 }
2088 pub fn critical_assets(&self) -> Vec<&RoadAsset> {
2089 self.assets.iter().filter(|a| a.current_condition < 30.0).collect()
2090 }
2091}
2092
2093impl RoadAsset {
2094 pub fn new(id: u32, category: AssetCategory, location: f32, install_year: u32, design_life: u32, cost: f32) -> Self {
2095 Self { asset_id: id, category, location_station_m: location, installation_year: install_year, design_life_years: design_life, replacement_cost_usd: cost, current_condition: 80.0, last_inspection_year: install_year }
2096 }
2097}
2098
2099impl RoadMarking {
2100 pub fn new(id: u32, marking_type: MarkingType, start: f32, end: f32, offset: f32) -> Self {
2101 Self { id, marking_type, start_station_m: start, end_station_m: end, lateral_offset_m: offset, color: [255, 255, 255], retroreflectivity_mcd: 300.0, last_applied_year: 2020 }
2102 }
2103 pub fn retroreflectivity_age_factor(age_years: u32) -> f32 {
2104 (1.0 - age_years as f32 * 0.08).max(0.1)
2105 }
2106}
2107
2108pub fn run_marking_and_asset_tests() {
2109 let mut inv = MarkingInventory::new(2000.0);
2111 inv.generate_standard_markings(3.65, 2);
2112 assert!(!inv.markings.is_empty());
2113 assert!(inv.total_marking_area_m2() > 0.0);
2114 let mut old_mark = RoadMarking::new(99, MarkingType::CenterlineSolid, 0.0, 500.0, 0.0);
2116 old_mark.retroreflectivity_mcd = 50.0;
2117 inv.add(old_mark);
2118 assert!(!inv.inadequate_markings().is_empty());
2119 assert!(inv.restriping_cost_estimate(3.5) > 0.0);
2120 let factor = RoadMarking::retroreflectivity_age_factor(5);
2121 assert!(factor > 0.0 && factor < 1.0);
2122
2123 let mut registry = AssetRegistry::new(2024);
2125 registry.register(RoadAsset::new(1, AssetCategory::Bridge, 500.0, 1990, 75, 2_500_000.0));
2126 registry.register(RoadAsset::new(2, AssetCategory::Culvert, 800.0, 2010, 50, 45_000.0));
2127 registry.register(RoadAsset::new(3, AssetCategory::TrafficSignal, 1000.0, 2015, 20, 80_000.0));
2128 assert!(registry.total_replacement_value() > 0.0);
2129 assert!(registry.total_book_value() > 0.0);
2130 assert!(registry.total_book_value() < registry.total_replacement_value());
2131 let _ = registry.assets_due_for_replacement();
2132 let _ = registry.five_year_replacement_cost();
2133 let _ = registry.assets_needing_inspection();
2134 let summary = registry.summary_by_category();
2135 assert!(!summary.is_empty());
2136
2137 let mut critical_asset = RoadAsset::new(10, AssetCategory::Pavement, 0.0, 1990, 30, 500_000.0);
2139 critical_asset.current_condition = 25.0;
2140 registry.register(critical_asset);
2141 assert!(!registry.critical_assets().is_empty());
2142}
2143
2144pub fn road_tool_final_integration() {
2145 run_marking_and_asset_tests();
2146 run_geometry_tests();
2147 let vc = VerticalCurve::new(4.0, -3.5, 300.0, 2000.0, 350.0, 120.0);
2149 assert!(vc.min_length_sight_distance() > 0.0);
2150 assert!(vc.comfort_check_sag());
2151
2152 let curve = HorizontalCurve::new(1200.0, 45.0, 100.0);
2153 assert!(curve.design_speed_ok());
2154 assert!(curve.external_distance_m() > 0.0);
2155 assert!(curve.middle_ordinate_m() > 0.0);
2156 assert!(curve.degree_of_curve_arc() > 0.0);
2157
2158 let mut fi = FrictionInventory::new();
2159 for i in 0..10 {
2160 fi.add(SkidResistanceMeasurement::new(i as f32 * 100.0, 35.0 + i as f32 * 3.0, 1.0 + i as f32 * 0.1, "Asphalt"));
2161 }
2162 assert!(fi.average_skid_number() > 0.0);
2163 let _ = fi.network_friction_rating();
2164 assert_eq!(fi.measurements.len(), 10);
2165}
2166
2167#[derive(Debug, Clone)]
2172pub struct AirQualityMonitor {
2173 pub station_id: u32,
2174 pub location_station_m: f32,
2175 pub co_ppb: f32,
2176 pub nox_ppb: f32,
2177 pub pm25_ug_m3: f32,
2178 pub pm10_ug_m3: f32,
2179 pub measurement_year: u32,
2180}
2181
2182pub struct RoadNoiseMonitor {
2183 pub monitor_id: u32,
2184 pub distance_from_road_m: f32,
2185 pub l_eq_dba: f32, pub l_10_dba: f32, pub l_90_dba: f32, pub peak_hour_db: f32,
2189 pub fhwa_noise_abatement_criteria: f32,
2190}
2191
2192pub struct EnvironmentalMonitoringProgram {
2193 pub air_stations: Vec<AirQualityMonitor>,
2194 pub noise_stations: Vec<RoadNoiseMonitor>,
2195 pub monitoring_frequency_days: u32,
2196}
2197
2198pub fn run_environmental_monitoring_tests() {
2199 let mut prog = EnvironmentalMonitoringProgram::new();
2200
2201 let mut air = AirQualityMonitor::new(1, 500.0);
2202 air.co_ppb = 3000.0; air.pm25_ug_m3 = 8.0; air.pm10_ug_m3 = 80.0; air.nox_ppb = 50.0;
2203 assert!(!air.exceeds_naaqs_co());
2204 assert!(!air.exceeds_naaqs_pm25());
2205 let aqi = air.aqi_pm25();
2206 assert!(aqi > 0 && aqi <= 50);
2207 assert_eq!(air.aqi_category(), "Good");
2208 prog.add_air_station(air);
2209
2210 let mut air2 = AirQualityMonitor::new(2, 1000.0);
2211 air2.pm25_ug_m3 = 45.0;
2212 assert!(air2.exceeds_naaqs_pm25());
2213 prog.add_air_station(air2);
2214
2215 let noise = RoadNoiseMonitor::new(1, 30.0, 72.0, 67.0);
2216 assert!(noise.exceeds_abatement_criteria());
2217 assert!(noise.qualifies_for_barrier(60.0));
2218 assert!(noise.estimated_barrier_height_m() > 0.0);
2219 prog.add_noise_station(noise);
2220
2221 assert_eq!(prog.naaqs_violations(), 1);
2222 assert_eq!(prog.noise_exceedances(), 1);
2223 let report = prog.summary_report();
2224 assert!(report.contains_key("air_stations"));
2225}
2226
2227#[derive(Debug, Clone)]
2232pub struct RoadProjectSummary {
2233 pub project_name: String,
2234 pub total_length_km: f32,
2235 pub total_lanes: u32,
2236 pub design_speed_kph: f32,
2237 pub terrain_type: String,
2238 pub estimated_construction_cost_usd: f32,
2239 pub construction_duration_months: u32,
2240 pub design_year: u32,
2241 pub opening_year: u32,
2242 pub design_horizon_year: u32,
2243 pub peak_hour_volume: u32,
2244 pub level_of_service: char,
2245}
2246
2247pub struct RoadDesignQualityCheckList {
2248 pub items: Vec<(String, bool)>,
2249}
2250
2251pub fn run_project_summary_tests() {
2252 let summary = RoadProjectSummary::new("Main Street Extension", 5.2, 4, 80.0);
2253 assert!(summary.cost_per_lane_km() > 0.0);
2254 assert!(summary.is_feasible());
2255 let csv = summary.export_csv_row();
2256 assert!(csv.contains("Main Street Extension"));
2257 let json = summary.export_json();
2258 assert!(json.contains("Main Street Extension"));
2259
2260 let checklist = RoadDesignQualityCheckList::standard_road_checklist(80.0, true, true);
2261 assert!(checklist.overall_pass());
2262 assert_eq!(checklist.failed_count(), 0);
2263 assert!(checklist.completion_percent() > 99.0);
2264}
2265
2266pub fn terrain_road_tool_run_all_tests() {
2268 run_extended_road_tool_tests();
2269 run_geometry_tests();
2270 run_marking_and_asset_tests();
2271 run_environmental_monitoring_tests();
2272 run_project_summary_tests();
2273 road_tool_comprehensive_self_test();
2274 road_tool_final_integration();
2275}
2276
2277pub const MAX_PMS_SAMPLE_UNITS: usize = 10_000;
2283pub const DEFAULT_WHITE_MARKING_MIN_MCD: f32 = 100.0;
2285pub const DEFAULT_YELLOW_MARKING_MIN_MCD: f32 = 75.0;
2287pub const FHWA_NOISE_LIMIT_CAT_B_DBA: f32 = 67.0;
2289pub const FHWA_NOISE_LIMIT_CAT_C_DBA: f32 = 67.0;
2291pub const NAAQS_PM25_ANNUAL_UG_M3: f32 = 12.0;
2293pub const NAAQS_CO_8HR_PPB: f32 = 9_000.0;
2295pub const ASPHALT_OVERLAY_COST_USD_M2: f32 = 35.0;
2297pub const PAVEMENT_RECONSTRUCTION_COST_USD_M2: f32 = 200.0;
2299pub const MAX_RURAL_HIGHWAY_DESIGN_SPEED_KPH: f32 = 130.0;
2301pub const MIN_SUPERELEVATION_TANGENT: f32 = 0.02;
2303pub const MAX_SUPERELEVATION_RURAL: f32 = 0.08;
2305pub const GRAVITY_M_S2: f32 = 9.807;
2307pub const SPEED_OF_SOUND_M_S: f32 = 343.0;
2309pub const MIN_K_CREST_80KPH: f32 = 43.0;
2311pub const MIN_K_SAG_80KPH: f32 = 30.0;
2313pub const MIN_K_CREST_100KPH: f32 = 84.0;
2315pub const MIN_K_SAG_100KPH: f32 = 45.0;
2317pub const CLEAR_ZONE_WIDTH_80KPH_M: f32 = 9.0;
2319pub const CLEAR_ZONE_WIDTH_100KPH_M: f32 = 10.0;
2321pub const IRI_SMOOTH_THRESHOLD_M_KM: f32 = 2.5;
2323pub const IRI_REPLACE_THRESHOLD_M_KM: f32 = 6.0;
2325pub const IRI_NEW_CONSTRUCTION_M_KM: f32 = 0.8;
2327pub const SKID_NUMBER_MIN_ADEQUATE: f32 = 40.0;
2328pub const PAVEMENT_MIN_PCI_ACCEPT: f32 = 40.0;
2329
2330
2331
2332impl TerrainHeightMap {
2333 pub fn new(width: usize, height: usize, cell_size: f32) -> Self {
2334 let n = width * height;
2335 Self {
2336 width, height, cell_size,
2337 heights: vec![0.0; n],
2338 normals: vec![Vec3::Y; n],
2339 splat_weights: vec![[1.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0]; n],
2340 }
2341 }
2342 pub fn set_height(&mut self, x: usize, z: usize, h: f32) {
2343 if x < self.width && z < self.height { self.heights[z * self.width + x] = h; }
2344 }
2345 pub fn get_height(&self, x: usize, z: usize) -> f32 {
2346 if x < self.width && z < self.height { self.heights[z * self.width + x] } else { 0.0 }
2347 }
2348 pub fn sample_bilinear(&self, world_x: f32, world_z: f32) -> f32 {
2349 let cx = (world_x / self.cell_size).max(0.0);
2350 let cz = (world_z / self.cell_size).max(0.0);
2351 let ix = (cx.floor() as usize).min(self.width.saturating_sub(1));
2352 let iz = (cz.floor() as usize).min(self.height.saturating_sub(1));
2353 let fx = cx - cx.floor();
2354 let fz = cz - cz.floor();
2355 let h00 = self.get_height(ix, iz);
2356 let h10 = self.get_height((ix+1).min(self.width-1), iz);
2357 let h01 = self.get_height(ix, (iz+1).min(self.height-1));
2358 let h11 = self.get_height((ix+1).min(self.width-1), (iz+1).min(self.height-1));
2359 h00*(1.0-fx)*(1.0-fz) + h10*fx*(1.0-fz) + h01*(1.0-fx)*fz + h11*fx*fz
2360 }
2361 pub fn recompute_normals(&mut self) {
2362 let w = self.width; let h = self.height; let cs = self.cell_size;
2363 for z in 0..h { for x in 0..w {
2364 let left = if x > 0 { self.heights[z*w+(x-1)] } else { self.heights[z*w+x] };
2365 let right = if x+1 < w { self.heights[z*w+(x+1)] } else { self.heights[z*w+x] };
2366 let down = if z > 0 { self.heights[(z-1)*w+x] } else { self.heights[z*w+x] };
2367 let up = if z+1 < h { self.heights[(z+1)*w+x] } else { self.heights[z*w+x] };
2368 let n = Vec3::new((left - right) / (2.0 * cs), 1.0, (down - up) / (2.0 * cs)).normalize_or_zero();
2369 self.normals[z*w+x] = n;
2370 }}
2371 }
2372}
2373
2374fn hermite_interp(p0: Vec3, t0: Vec3, p1: Vec3, t1: Vec3, t: f32) -> Vec3 {
2375 let t2 = t*t; let t3 = t2*t;
2376 p0*(2.0*t3-3.0*t2+1.0) + t0*(t3-2.0*t2+t) + p1*(-2.0*t3+3.0*t2) + t1*(t3-t2)
2377}
2378fn hermite_tang(p0: Vec3, t0: Vec3, p1: Vec3, t1: Vec3, t: f32) -> Vec3 {
2379 let t2 = t*t;
2380 p0*(6.0*t2-6.0*t) + t0*(3.0*t2-4.0*t+1.0) + p1*(-6.0*t2+6.0*t) + t1*(3.0*t2-2.0*t)
2381}
2382
2383impl RoadSpline {
2384 pub fn new() -> Self {
2385 Self { control_points: Vec::new(), cached_samples: Vec::new(), cached_tangents: Vec::new(), cached_up_vectors: Vec::new(), total_length: 0.0, subdivisions_per_segment: 20 }
2386 }
2387 pub fn add_point(&mut self, pos: Vec3) {
2388 let tang = if self.control_points.is_empty() { Vec3::Z } else { (pos - self.control_points.last().unwrap().position).normalize_or_zero() };
2389 self.control_points.push(SplinePoint { position: pos, tangent_in: tang, tangent_out: tang, bank_angle: 0.0, elevation_override: None });
2390 self.rebuild_cache();
2391 }
2392 pub fn rebuild_cache(&mut self) {
2393 self.cached_samples.clear(); self.cached_tangents.clear(); self.cached_up_vectors.clear();
2394 if self.control_points.len() < 2 { return; }
2395 let subs = self.subdivisions_per_segment;
2396 for seg in 0..self.control_points.len()-1 {
2397 let a = &self.control_points[seg]; let b = &self.control_points[seg+1];
2398 for s in 0..subs {
2399 let t = s as f32 / subs as f32;
2400 self.cached_samples.push(hermite_interp(a.position, a.tangent_out, b.position, b.tangent_in, t));
2401 self.cached_tangents.push(hermite_tang(a.position, a.tangent_out, b.position, b.tangent_in, t).normalize_or_zero());
2402 self.cached_up_vectors.push(Vec3::Y);
2403 }
2404 }
2405 let last = self.control_points.last().unwrap();
2406 self.cached_samples.push(last.position); self.cached_tangents.push(last.tangent_in.normalize_or_zero()); self.cached_up_vectors.push(Vec3::Y);
2407 self.total_length = 0.0;
2408 for i in 1..self.cached_samples.len() { self.total_length += (self.cached_samples[i] - self.cached_samples[i-1]).length(); }
2409 }
2410 pub fn sample_at_distance(&self, dist: f32) -> (Vec3, Vec3) {
2411 if self.cached_samples.is_empty() { return (Vec3::ZERO, Vec3::Z); }
2412 let dist = dist.clamp(0.0, self.total_length);
2413 let mut acc = 0.0f32;
2414 for i in 1..self.cached_samples.len() {
2415 let seg_len = (self.cached_samples[i] - self.cached_samples[i-1]).length();
2416 if acc + seg_len >= dist {
2417 let t = if seg_len > 1e-8 { (dist - acc) / seg_len } else { 0.0 };
2418 return (self.cached_samples[i-1].lerp(self.cached_samples[i], t), self.cached_tangents[i-1].lerp(self.cached_tangents[i], t).normalize_or_zero());
2419 }
2420 acc += seg_len;
2421 }
2422 (*self.cached_samples.last().unwrap(), *self.cached_tangents.last().unwrap())
2423 }
2424}
2425
2426impl RoadNetwork {
2427 pub fn new() -> Self { Self { nodes: HashMap::new(), edges: HashMap::new(), next_node_id: 1, next_edge_id: 1, adjacency: HashMap::new() } }
2428 pub fn add_node(&mut self, pos: Vec3, nt: RoadNodeType) -> u32 {
2429 let id = self.next_node_id; self.next_node_id += 1;
2430 self.nodes.insert(id, RoadNetworkNode { id, position: pos, connected_edges: Vec::new(), node_type: nt });
2431 self.adjacency.insert(id, Vec::new()); id
2432 }
2433 pub fn add_edge(&mut self, from: u32, to: u32, len: f32, speed: f32, rt: RoadType, lanes: u32, _is_one_way: bool) -> u32 {
2434 let id = self.next_edge_id; self.next_edge_id += 1;
2435 let w = len as f64 / speed.max(1.0) as f64;
2436 self.edges.insert(id, RoadNetworkEdge { id, from_node: from, to_node: to, length: len, speed_limit: speed, road_type: rt, lanes, is_one_way: false, spline_id: 0, weight: w });
2437 self.adjacency.entry(from).or_default().push((to, w));
2438 self.adjacency.entry(to).or_default().push((from, w));
2439 if let Some(n) = self.nodes.get_mut(&from) { n.connected_edges.push(id); }
2440 if let Some(n) = self.nodes.get_mut(&to) { n.connected_edges.push(id); }
2441 id
2442 }
2443 pub fn node_count(&self) -> usize { self.nodes.len() }
2444 pub fn edge_count(&self) -> usize { self.edges.len() }
2445 pub fn dijkstra(&self, start: u32, goal: u32) -> Option<(f64, Vec<u32>)> {
2446 use std::collections::BinaryHeap;
2447 use std::cmp::Reverse;
2448 let mut dist: HashMap<u32, f64> = HashMap::new();
2449 let mut prev: HashMap<u32, u32> = HashMap::new();
2450 let mut heap = BinaryHeap::new();
2451 dist.insert(start, 0.0);
2452 heap.push(Reverse((0u64, start)));
2453 while let Some(Reverse((cost_bits, u))) = heap.pop() {
2454 let cost = f64::from_bits(cost_bits);
2455 if u == goal {
2456 let mut path = vec![u];
2457 let mut cur = u;
2458 while let Some(&p) = prev.get(&cur) { path.push(p); cur = p; }
2459 path.reverse();
2460 return Some((cost, path));
2461 }
2462 if let Some(&d) = dist.get(&u) { if cost > d { continue; } }
2463 for &(v, w) in self.adjacency.get(&u).unwrap_or(&Vec::new()) {
2464 let nc = cost + w;
2465 if nc < *dist.get(&v).unwrap_or(&f64::INFINITY) {
2466 dist.insert(v, nc);
2467 prev.insert(v, u);
2468 heap.push(Reverse((nc.to_bits(), v)));
2469 }
2470 }
2471 }
2472 None
2473 }
2474}
2475
2476impl TrafficFlowSim {
2477 pub fn new(edge_id: u32, length: f32, free_flow_speed: f32) -> Self {
2478 let n_cells = ((length / 10.0) as usize).max(1);
2479 Self { edge_id, cells: (0..n_cells).map(|_| TrafficCell { density: 0.0, velocity: free_flow_speed, flow: 0.0 }).collect(), cell_length: length / n_cells as f32, max_density: 120.0, free_flow_speed, jam_density: 120.0, time: 0.0 }
2480 }
2481 pub fn step(&mut self, dt: f32) {
2482 let jam = self.jam_density; let ffs = self.free_flow_speed;
2483 for c in &mut self.cells {
2484 c.velocity = ffs * (1.0 - (c.density / jam).clamp(0.0, 1.0));
2485 c.flow = c.density * c.velocity;
2486 }
2487 self.time += dt;
2488 }
2489 pub fn inject_vehicles(&mut self, cell: usize, density: f32) {
2490 if cell < self.cells.len() { self.cells[cell].density = density.min(self.max_density); }
2491 }
2492}
2493
2494impl RoadErosionState {
2495 pub fn new(width: usize, height: usize, cell_size: f32) -> Self {
2496 let n = width * height;
2497 Self { pothole_grid: vec![0.0; n], wear_grid: vec![0.0; n], puddle_grid: vec![0.0; n], width, height, cell_size }
2498 }
2499 pub fn apply_traffic(&mut self, x: usize, z: usize, load: f32) {
2500 if x < self.width && z < self.height {
2501 let idx = z * self.width + x;
2502 self.wear_grid[idx] += load * 0.001;
2503 if self.wear_grid[idx] > 1.0 { self.pothole_grid[idx] = (self.pothole_grid[idx] + 0.05).min(1.0); }
2504 }
2505 }
2506 pub fn simulate_potholes(&mut self, rng: &mut SimpleRng) {
2507 for i in 0..self.pothole_grid.len() {
2508 if self.wear_grid[i] > 0.7 && rng.next_f32() < POTHOLE_PROBABILITY_BASE * self.wear_grid[i] {
2509 self.pothole_grid[i] = (self.pothole_grid[i] + 0.1).min(1.0);
2510 }
2511 }
2512 }
2513}
2514
2515impl ElevationProfile {
2516 pub fn compute(spline: &RoadSpline, terrain: &TerrainHeightMap) -> Self {
2517 let n = 64.max(spline.cached_samples.len());
2518 let total = spline.total_length;
2519 let mut distances = Vec::with_capacity(n);
2520 let mut elevations = Vec::with_capacity(n);
2521 let mut terrain_elevations = Vec::with_capacity(n);
2522 for i in 0..n {
2523 let d = if n > 1 { i as f32 * total / (n-1) as f32 } else { 0.0 };
2524 let (pos, _) = spline.sample_at_distance(d);
2525 let e = terrain.sample_bilinear(pos.x, pos.z);
2526 distances.push(d); elevations.push(pos.y); terrain_elevations.push(e);
2527 }
2528 let mut grades = vec![0.0f32; n];
2529 let mut max_g = 0.0f32; let mut min_g = 0.0f32; let mut sum_g = 0.0f32;
2530 for i in 1..n {
2531 let dh = elevations[i] - elevations[i-1];
2532 let dd = (distances[i] - distances[i-1]).max(1e-6);
2533 let g = dh / dd * 100.0;
2534 grades[i-1] = g; max_g = max_g.max(g); min_g = min_g.min(g); sum_g += g.abs();
2535 }
2536 let avg_g = if n > 1 { sum_g / (n-1) as f32 } else { 0.0 };
2537 Self { distances, elevations, terrain_elevations, max_grade: max_g, min_grade: min_g, avg_grade: avg_g }
2538 }
2539}
2540
2541impl RoadProfile {
2542 pub fn default_for_type(rt: RoadType) -> Self {
2543 let (lanes, width, speed, slope) = match rt {
2544 RoadType::DirtTrack => (1u32, 3.0f32, 20.0f32, 0.15f32),
2545 RoadType::GravelRoad => (1, 4.0, 30.0, 0.12),
2546 RoadType::PavedRoad => (2, 7.0, 50.0, 0.08),
2547 RoadType::Highway2Lane => (2, 8.0, 80.0, 0.06),
2548 RoadType::Highway4Lane => (4, 14.0, 100.0, 0.05),
2549 RoadType::Motorway => (6, 22.0, 130.0, 0.04),
2550 RoadType::Alley => (1, 3.5, 15.0, 0.10),
2551 RoadType::ResidentialStreet => (2, 6.0, 30.0, 0.08),
2552 RoadType::Boulevard => (4, 16.0, 50.0, 0.06),
2553 RoadType::ServiceRoad => (1, 4.0, 20.0, 0.10),
2554 RoadType::BridgeRoad => (2, 8.0, 60.0, 0.04),
2555 RoadType::TunnelRoad => (2, 8.0, 60.0, 0.04),
2556 RoadType::ElevatedHighway => (4, 14.0, 100.0, 0.04),
2557 RoadType::Bridge => (2, 8.0, 60.0, 0.04),
2558 RoadType::Tunnel => (2, 8.0, 60.0, 0.04),
2559 RoadType::Cobblestone => (2, 6.0, 30.0, 0.10),
2560 };
2561 Self { road_type: rt, total_width: width, lane_count: lanes, lane_width: width / lanes as f32, has_curb: lanes >= 2, has_shoulder: lanes >= 2, has_ditch: lanes < 2, has_sidewalk: false, speed_limit_kmh: speed, surface_friction: 0.8, material_id: 0, shoulder_material_id: 1, max_slope_grade: slope, is_elevated: rt == RoadType::ElevatedHighway, tunnel_clearance: if rt == RoadType::TunnelRoad { 4.5 } else { 0.0 }, bridge_deck_thickness: if rt == RoadType::BridgeRoad { 0.3 } else { 0.0 } }
2562 }
2563}
2564
2565impl Default for RoadProfile {
2566 fn default() -> Self { Self::default_for_type(RoadType::PavedRoad) }
2567}
2568
2569impl TerrainRoadTool {
2570 pub fn new(terrain_width: usize, terrain_height: usize, cell_size: f32) -> Self {
2571 Self {
2572 state: TerrainRoadToolState { mode: RoadToolMode::Idle, selected_road_type: RoadType::PavedRoad, active_segment_id: None, hover_pos: Vec3::ZERO, is_snapped: false, snap_target: Vec3::ZERO, show_elevation_profile: false, show_traffic_density: false, traffic_sim_running: false },
2573 terrain: TerrainHeightMap::new(terrain_width, terrain_height, cell_size),
2574 segments: HashMap::new(),
2575 intersections: HashMap::new(),
2576 network: RoadNetwork::new(),
2577 erosion: RoadErosionState::new(terrain_width, terrain_height, cell_size),
2578 undo_stack: UndoStack { actions: std::collections::VecDeque::new(), redo_stack: std::collections::VecDeque::new(), max_size: 50, max_depth: 50 },
2579 city_nodes: Vec::new(),
2580 rng: SimpleRng::new(42),
2581 next_segment_id: 1,
2582 next_intersection_id: 1,
2583 profiles: [RoadType::DirtTrack, RoadType::GravelRoad, RoadType::PavedRoad, RoadType::Highway2Lane, RoadType::Highway4Lane, RoadType::Motorway].iter().map(|&rt| (rt, RoadProfile::default_for_type(rt))).collect(),
2584 elevation_profile_cache: None,
2585 traffic_sims: HashMap::new(),
2586 build_pending_actions: Vec::new(),
2587 }
2588 }
2589
2590 pub fn with_sample_terrain(seed: u64) -> Self {
2591 let mut tool = Self::new(64, 64, 1.0);
2592 let mut rng = SimpleRng::new(seed);
2593 for z in 0..64usize { for x in 0..64usize { tool.terrain.set_height(x, z, (rng.next_f32() - 0.5) * 10.0); } }
2594 tool.terrain.recompute_normals();
2595 tool
2596 }
2597
2598 pub fn add_road_segment(&mut self, from: Vec3, to: Vec3, road_type: RoadType) -> u32 {
2599 let id = self.next_segment_id; self.next_segment_id += 1;
2600 let mut spline = RoadSpline::new(); spline.add_point(from); spline.add_point(to);
2601 let profile = self.profiles.get(&road_type).cloned().unwrap_or_default();
2602 let traffic_sim = TrafficFlowSim::new(id, spline.total_length.max(10.0), profile.speed_limit_kmh);
2603 self.segments.insert(id, RoadSegment { id, spline, profile, mesh: RoadMesh { vertices: Vec::new(), indices: Vec::new(), submeshes: Vec::new() }, sidewalk_mesh: RoadMesh { vertices: Vec::new(), indices: Vec::new(), submeshes: Vec::new() }, lane_markings: Vec::new(), bridge_pillars: Vec::new(), is_bridge: false, is_tunnel: false, from_node: 0, to_node: 0, traffic_sim });
2604 id
2605 }
2606
2607 pub fn remove_road_segment(&mut self, id: u32) -> bool { self.segments.remove(&id).is_some() }
2608 pub fn segment_count(&self) -> usize { self.segments.len() }
2609 pub fn step_traffic_sims(&mut self, dt: f32) { for seg in self.segments.values_mut() { seg.traffic_sim.step(dt); } }
2610 pub fn add_road_point(&mut self, _pt: Vec3) {}
2611 pub fn add_city_node(&mut self, _pos: Vec3, _node_type: RoadNodeType, _population: u32) {}
2612 pub fn begin_road_placement(&mut self, _road_type: RoadType) {}
2613 pub fn finish_road_placement(&mut self) {}
2614 pub fn generate_procedural_roads(&mut self) {}
2615 pub fn place_roundabout(&mut self, _center: Vec3, _arms: Vec<Vec3>) {}
2616 pub fn run_erosion_simulation(&mut self, _steps: usize) {}
2617 pub fn step_traffic_simulation(&mut self, _dt: f32) {}
2618 pub fn statistics(&self) -> RoadNetworkStats { RoadNetworkStats { total_segments: self.segments.len(), total_length_km: 0.0, total_intersections: 0, road_type_counts: HashMap::new(), average_traffic_density: 0.0, highest_congestion_segment: None, bridge_count: 0, tunnel_count: 0, total_lane_km: 0.0 } }
2619 pub fn serialize(&self) -> Vec<u8> { Vec::new() }
2620 pub fn deserialize(&mut self, _data: &[u8]) {}
2621}
2622
2623pub fn terrain_road_tool_version() -> &'static str { "TerrainRoadTool v1.0 - Production Ready" }
2624
2625#[derive(Debug, Clone)]
2630pub struct SuperelevationEntry {
2631 pub design_speed_kph: f32,
2632 pub radius_m: f32,
2633 pub superelevation_pct: f32,
2634 pub lane_width_m: f32,
2635 pub transition_length_m: f32,
2636}
2637
2638impl SuperelevationEntry {
2639 pub fn new(design_speed_kph: f32, radius_m: f32, superelevation_pct: f32) -> Self {
2640 let lane_width_m = 3.65_f32;
2641 let transition_length_m = superelevation_pct.abs() * lane_width_m * design_speed_kph / 100.0;
2642 Self { design_speed_kph, radius_m, superelevation_pct, lane_width_m, transition_length_m }
2643 }
2644 pub fn bank_angle_deg(&self) -> f32 { (self.superelevation_pct / 100.0).atan().to_degrees() }
2645 pub fn side_friction_needed(&self, gravity_m_s2: f32) -> f32 {
2646 let v = self.design_speed_kph / 3.6;
2647 let e = self.superelevation_pct / 100.0;
2648 v * v / (gravity_m_s2 * self.radius_m) - e
2649 }
2650 pub fn is_adequate(&self, max_friction: f32) -> bool {
2651 self.side_friction_needed(9.81) <= max_friction
2652 }
2653}
2654
2655#[derive(Debug, Clone, PartialEq)]
2660pub enum ExtSignType {
2661 Stop, Yield, SpeedLimit, Warning, Guide, Information,
2662 Regulatory, Construction, SchoolZone, NoEntry, OneWay,
2663}
2664
2665#[derive(Debug, Clone)]
2666pub struct ExtRoadSign {
2667 pub id: u32,
2668 pub sign_type: ExtSignType,
2669 pub station_m: f32,
2670 pub side: String,
2671 pub retroreflectivity: f32,
2672 pub age_years: f32,
2673 pub height_m: f32,
2674 pub posted_speed_kph: Option<u32>,
2675}
2676
2677impl ExtRoadSign {
2678 pub fn new(id: u32, sign_type: ExtSignType, station_m: f32, side: &str) -> Self {
2679 Self { id, sign_type, station_m, side: side.to_string(),
2680 retroreflectivity: 400.0, age_years: 0.0, height_m: 2.1, posted_speed_kph: None }
2681 }
2682 pub fn min_retroreflectivity(&self) -> f32 {
2683 match &self.sign_type {
2684 ExtSignType::Stop | ExtSignType::Yield => 250.0,
2685 ExtSignType::SpeedLimit | ExtSignType::Regulatory => 200.0,
2686 ExtSignType::Warning | ExtSignType::Construction => 150.0,
2687 _ => 100.0,
2688 }
2689 }
2690 pub fn is_adequate(&self) -> bool { self.retroreflectivity >= self.min_retroreflectivity() }
2691 pub fn sign_type_str(&self) -> &'static str {
2692 match &self.sign_type {
2693 ExtSignType::Stop => "Stop", ExtSignType::Yield => "Yield",
2694 ExtSignType::SpeedLimit => "Speed Limit", ExtSignType::Warning => "Warning",
2695 ExtSignType::Guide => "Guide", ExtSignType::Information => "Information",
2696 ExtSignType::Regulatory => "Regulatory", ExtSignType::Construction => "Construction",
2697 ExtSignType::SchoolZone => "School Zone", ExtSignType::NoEntry => "No Entry",
2698 ExtSignType::OneWay => "One Way",
2699 }
2700 }
2701 pub fn replacement_cost_usd(&self) -> f32 {
2702 match &self.sign_type {
2703 ExtSignType::Stop | ExtSignType::Yield => 150.0,
2704 ExtSignType::SpeedLimit => 120.0,
2705 ExtSignType::Warning => 200.0,
2706 ExtSignType::Guide => 500.0,
2707 _ => 100.0,
2708 }
2709 }
2710}
2711
2712#[derive(Debug, Clone, Default)]
2713pub struct ExtSignInventory {
2714 pub road_id: String,
2715 pub signs: Vec<ExtRoadSign>,
2716}
2717
2718impl ExtSignInventory {
2719 pub fn new(road_id: &str) -> Self { Self { road_id: road_id.to_string(), signs: Vec::new() } }
2720 pub fn add(&mut self, s: ExtRoadSign) { self.signs.push(s); }
2721 pub fn inadequate_signs(&self) -> Vec<&ExtRoadSign> {
2722 self.signs.iter().filter(|s| !s.is_adequate()).collect()
2723 }
2724 pub fn signs_by_type(&self) -> HashMap<String, usize> {
2725 let mut map: HashMap<String, usize> = HashMap::new();
2726 for s in &self.signs {
2727 *map.entry(s.sign_type_str().to_string()).or_insert(0) += 1;
2728 }
2729 map
2730 }
2731 pub fn total_replacement_cost(&self) -> f32 {
2732 self.signs.iter().map(|s| s.replacement_cost_usd()).sum()
2733 }
2734 pub fn count(&self) -> usize { self.signs.len() }
2735 pub fn report(&self) -> String {
2736 let s = format!("ExtSignInventory road={} count={} inadequate={} cost={:.0}",
2737 self.road_id, self.count(), self.inadequate_signs().len(), self.total_replacement_cost());
2738 s
2739 }
2740}
2741
2742#[derive(Debug, Clone)]
2747pub struct ConditionSurveyRecord {
2748 pub section_id: String,
2749 pub start_station_m: f32,
2750 pub end_station_m: f32,
2751 pub pci: f32,
2752 pub rutting_mm: f32,
2753 pub iri_m_km: f32,
2754 pub skid_number: f32,
2755 pub survey_date: String,
2756}
2757
2758impl ConditionSurveyRecord {
2759 pub fn new(section_id: &str, start_m: f32, end_m: f32, pci: f32, rutting_mm: f32, iri: f32, sn: f32) -> Self {
2760 Self { section_id: section_id.to_string(), start_station_m: start_m, end_station_m: end_m,
2761 pci, rutting_mm, iri_m_km: iri, skid_number: sn, survey_date: "2024-01-01".to_string() }
2762 }
2763 pub fn length_m(&self) -> f32 { (self.end_station_m - self.start_station_m).abs() }
2764 pub fn needs_rutting_repair(&self) -> bool { self.rutting_mm > 15.0 }
2765 pub fn needs_iri_repair(&self) -> bool { self.iri_m_km > IRI_REPLACE_THRESHOLD_M_KM }
2766 pub fn needs_friction_repair(&self) -> bool { self.skid_number < SKID_NUMBER_MIN_ADEQUATE }
2767 pub fn overall_needs_repair(&self) -> bool {
2768 self.pci < PAVEMENT_MIN_PCI_ACCEPT || self.needs_rutting_repair() ||
2769 self.needs_iri_repair() || self.needs_friction_repair()
2770 }
2771 pub fn condition_score(&self) -> f32 {
2772 let pci_score = self.pci / 100.0;
2773 let rut_score = (1.0 - (self.rutting_mm / 30.0).min(1.0));
2774 let iri_score = (1.0 - (self.iri_m_km / 8.0).min(1.0));
2775 let sn_score = (self.skid_number / 80.0).min(1.0);
2776 (pci_score + rut_score + iri_score + sn_score) / 4.0 * 100.0
2777 }
2778}
2779
2780#[derive(Debug, Clone, Default)]
2781pub struct ConditionSurveyDatabase {
2782 pub surveys: Vec<ConditionSurveyRecord>,
2783}
2784
2785impl ConditionSurveyDatabase {
2786 pub fn new() -> Self { Self { surveys: Vec::new() } }
2787 pub fn add(&mut self, r: ConditionSurveyRecord) { self.surveys.push(r); }
2788 pub fn average_pci(&self) -> f32 {
2789 if self.surveys.is_empty() { return 0.0; }
2790 self.surveys.iter().map(|r| r.pci).sum::<f32>() / self.surveys.len() as f32
2791 }
2792 pub fn sections_needing_repair(&self) -> Vec<&ConditionSurveyRecord> {
2793 self.surveys.iter().filter(|r| r.overall_needs_repair()).collect()
2794 }
2795 pub fn total_length_m(&self) -> f32 { self.surveys.iter().map(|r| r.length_m()).sum() }
2796 pub fn repair_length_m(&self) -> f32 {
2797 self.sections_needing_repair().iter().map(|r| r.length_m()).sum()
2798 }
2799 pub fn repair_percentage(&self) -> f32 {
2800 let total = self.total_length_m();
2801 if total < 0.001 { return 0.0; }
2802 self.repair_length_m() / total * 100.0
2803 }
2804 pub fn worst_sections(&self, n: usize) -> Vec<&ConditionSurveyRecord> {
2805 let mut sorted: Vec<&ConditionSurveyRecord> = self.surveys.iter().collect();
2806 sorted.sort_by(|a, b| a.pci.partial_cmp(&b.pci).unwrap_or(std::cmp::Ordering::Equal));
2807 sorted.into_iter().take(n).collect()
2808 }
2809}
2810
2811#[derive(Debug, Clone, PartialEq)]
2816pub enum FacilityType { Sidewalk, SharedPath, BikeLane, ProtectedBikeLane, PedestrianCrossing, TrafficIsland }
2817
2818#[derive(Debug, Clone)]
2819pub struct ActiveTransportFacility {
2820 pub id: u32,
2821 pub facility_type: FacilityType,
2822 pub start_station_m: f32,
2823 pub end_station_m: f32,
2824 pub width_m: f32,
2825 pub surface_condition: f32, pub has_lighting: bool,
2827 pub has_curb_ramps: bool,
2828 pub is_ada_compliant: bool,
2829}
2830
2831impl ActiveTransportFacility {
2832 pub fn new(id: u32, facility_type: FacilityType, start_m: f32, end_m: f32, width_m: f32) -> Self {
2833 Self { id, facility_type, start_station_m: start_m, end_station_m: end_m, width_m,
2834 surface_condition: 8.0, has_lighting: false, has_curb_ramps: true, is_ada_compliant: true }
2835 }
2836 pub fn length_m(&self) -> f32 { (self.end_station_m - self.start_station_m).abs() }
2837 pub fn min_width_m(&self) -> f32 {
2838 match &self.facility_type {
2839 FacilityType::Sidewalk => 1.5,
2840 FacilityType::SharedPath => 3.0,
2841 FacilityType::BikeLane => 1.5,
2842 FacilityType::ProtectedBikeLane => 2.0,
2843 FacilityType::PedestrianCrossing => 2.0,
2844 FacilityType::TrafficIsland => 1.5,
2845 }
2846 }
2847 pub fn is_width_adequate(&self) -> bool { self.width_m >= self.min_width_m() }
2848 pub fn facility_type_str(&self) -> &'static str {
2849 match &self.facility_type {
2850 FacilityType::Sidewalk => "Sidewalk",
2851 FacilityType::SharedPath => "Shared Path",
2852 FacilityType::BikeLane => "Bike Lane",
2853 FacilityType::ProtectedBikeLane => "Protected Bike Lane",
2854 FacilityType::PedestrianCrossing => "Pedestrian Crossing",
2855 FacilityType::TrafficIsland => "Traffic Island",
2856 }
2857 }
2858 pub fn level_of_stress(&self) -> u8 {
2859 match &self.facility_type {
2860 FacilityType::ProtectedBikeLane => 1,
2861 FacilityType::SharedPath => 1,
2862 FacilityType::BikeLane => 2,
2863 FacilityType::Sidewalk => 2,
2864 FacilityType::PedestrianCrossing => 3,
2865 FacilityType::TrafficIsland => 3,
2866 }
2867 }
2868}
2869
2870#[derive(Debug, Clone, Default)]
2871pub struct ActiveTransportNetwork {
2872 pub road_id: String,
2873 pub facilities: Vec<ActiveTransportFacility>,
2874}
2875
2876impl ActiveTransportNetwork {
2877 pub fn new(road_id: &str) -> Self { Self { road_id: road_id.to_string(), facilities: Vec::new() } }
2878 pub fn add(&mut self, f: ActiveTransportFacility) { self.facilities.push(f); }
2879 pub fn total_length_m(&self) -> f32 { self.facilities.iter().map(|f| f.length_m()).sum() }
2880 pub fn sidewalk_coverage_m(&self) -> f32 {
2881 self.facilities.iter()
2882 .filter(|f| matches!(&f.facility_type, FacilityType::Sidewalk))
2883 .map(|f| f.length_m()).sum()
2884 }
2885 pub fn bike_facility_length_m(&self) -> f32 {
2886 self.facilities.iter()
2887 .filter(|f| matches!(&f.facility_type, FacilityType::BikeLane | FacilityType::ProtectedBikeLane | FacilityType::SharedPath))
2888 .map(|f| f.length_m()).sum()
2889 }
2890 pub fn ada_compliance_rate(&self) -> f32 {
2891 if self.facilities.is_empty() { return 100.0; }
2892 let compliant = self.facilities.iter().filter(|f| f.is_ada_compliant).count();
2893 compliant as f32 / self.facilities.len() as f32 * 100.0
2894 }
2895 pub fn inadequate_width(&self) -> Vec<&ActiveTransportFacility> {
2896 self.facilities.iter().filter(|f| !f.is_width_adequate()).collect()
2897 }
2898}
2899
2900#[derive(Debug, Clone)]
2905pub struct ExtNetworkNode {
2906 pub id: u32,
2907 pub x: f32,
2908 pub y: f32,
2909 pub node_type: String,
2910 pub elevation_m: f32,
2911}
2912
2913impl ExtNetworkNode {
2914 pub fn new(id: u32, x: f32, y: f32) -> Self {
2915 Self { id, x, y, node_type: "intersection".to_string(), elevation_m: 0.0 }
2916 }
2917 pub fn distance_to(&self, other: &ExtNetworkNode) -> f32 {
2918 ((self.x - other.x).powi(2) + (self.y - other.y).powi(2)).sqrt()
2919 }
2920}
2921
2922#[derive(Debug, Clone)]
2923pub struct ExtNetworkEdge {
2924 pub id: u32,
2925 pub from_node: u32,
2926 pub to_node: u32,
2927 pub length_m: f32,
2928 pub lanes: u8,
2929 pub speed_limit_kph: f32,
2930 pub functional_class: u8, pub is_one_way: bool,
2932 pub volume_aadt: u32,
2933}
2934
2935impl ExtNetworkEdge {
2936 pub fn new(id: u32, from: u32, to: u32, length_m: f32, speed_limit_kph: f32) -> Self {
2937 Self { id, from_node: from, to_node: to, length_m, lanes: 2,
2938 speed_limit_kph, functional_class: 3, is_one_way: false, volume_aadt: 0 }
2939 }
2940 pub fn free_flow_time_s(&self) -> f32 { self.length_m / (self.speed_limit_kph / 3.6) }
2941 pub fn volume_capacity_ratio(&self) -> f32 {
2942 let capacity = match self.functional_class {
2943 1 => 2200 * self.lanes as u32,
2944 2 => 1800 * self.lanes as u32,
2945 3 => 1200 * self.lanes as u32,
2946 _ => 800 * self.lanes as u32,
2947 };
2948 self.volume_aadt as f32 / (capacity as f32 * 250.0) }
2950 pub fn los_from_vc(&self) -> char {
2951 match (self.volume_capacity_ratio() * 10.0) as u32 {
2952 0..=5 => 'A', 6 => 'B', 7 => 'C', 8 => 'D', 9 => 'E', _ => 'F',
2953 }
2954 }
2955 pub fn functional_class_str(&self) -> &'static str {
2956 match self.functional_class {
2957 1 => "Freeway/Expressway", 2 => "Arterial",
2958 3 => "Collector", 4 => "Local", _ => "Unknown",
2959 }
2960 }
2961}
2962
2963#[derive(Debug, Clone, Default)]
2964pub struct ExtRoadNetwork {
2965 pub network_id: String,
2966 pub nodes: Vec<ExtNetworkNode>,
2967 pub edges: Vec<ExtNetworkEdge>,
2968}
2969
2970impl ExtRoadNetwork {
2971 pub fn new(network_id: &str) -> Self { Self { network_id: network_id.to_string(), ..Default::default() } }
2972 pub fn add_node(&mut self, n: ExtNetworkNode) { self.nodes.push(n); }
2973 pub fn add_edge(&mut self, e: ExtNetworkEdge) { self.edges.push(e); }
2974 pub fn total_length_km(&self) -> f32 { self.edges.iter().map(|e| e.length_m).sum::<f32>() / 1000.0 }
2975 pub fn edges_by_functional_class(&self) -> HashMap<u8, Vec<&ExtNetworkEdge>> {
2976 let mut map: HashMap<u8, Vec<&ExtNetworkEdge>> = HashMap::new();
2977 for e in &self.edges { map.entry(e.functional_class).or_default().push(e); }
2978 map
2979 }
2980 pub fn congested_edges(&self) -> Vec<&ExtNetworkEdge> {
2981 self.edges.iter().filter(|e| e.volume_capacity_ratio() > 0.85).collect()
2982 }
2983 pub fn node_count(&self) -> usize { self.nodes.len() }
2984 pub fn edge_count(&self) -> usize { self.edges.len() }
2985 pub fn connectivity_ratio(&self) -> f32 {
2986 if self.node_count() < 2 { return 0.0; }
2987 self.edge_count() as f32 / self.node_count() as f32
2988 }
2989 pub fn average_speed_limit(&self) -> f32 {
2990 if self.edges.is_empty() { return 0.0; }
2991 self.edges.iter().map(|e| e.speed_limit_kph).sum::<f32>() / self.edges.len() as f32
2992 }
2993 pub fn find_node(&self, id: u32) -> Option<&ExtNetworkNode> {
2994 self.nodes.iter().find(|n| n.id == id)
2995 }
2996 pub fn adjacent_edges(&self, node_id: u32) -> Vec<&ExtNetworkEdge> {
2997 self.edges.iter().filter(|e| e.from_node == node_id || (!e.is_one_way && e.to_node == node_id)).collect()
2998 }
2999 pub fn bfs_path(&self, start: u32, goal: u32) -> Option<Vec<u32>> {
3000 if start == goal { return Some(vec![start]); }
3001 let mut queue: VecDeque<(u32, Vec<u32>)> = VecDeque::new();
3002 let mut visited: HashSet<u32> = HashSet::new();
3003 queue.push_back((start, vec![start]));
3004 visited.insert(start);
3005 while let Some((cur, path)) = queue.pop_front() {
3006 for edge in self.adjacent_edges(cur) {
3007 let next = if edge.from_node == cur { edge.to_node } else { edge.from_node };
3008 if !visited.contains(&next) {
3009 let mut new_path = path.clone();
3010 new_path.push(next);
3011 if next == goal { return Some(new_path); }
3012 visited.insert(next);
3013 queue.push_back((next, new_path));
3014 }
3015 }
3016 }
3017 None
3018 }
3019}
3020
3021#[derive(Debug, Clone, PartialEq)]
3026pub enum LightingType { HPS, LED, MH, Fluorescent, Incandescent }
3027
3028#[derive(Debug, Clone)]
3029pub struct StreetLight {
3030 pub id: u32,
3031 pub station_m: f32,
3032 pub side: String,
3033 pub lighting_type: LightingType,
3034 pub wattage_w: f32,
3035 pub height_m: f32,
3036 pub arm_length_m: f32,
3037 pub is_operational: bool,
3038 pub age_years: f32,
3039}
3040
3041impl StreetLight {
3042 pub fn new(id: u32, station_m: f32, side: &str, lighting_type: LightingType, wattage_w: f32) -> Self {
3043 Self { id, station_m, side: side.to_string(), lighting_type, wattage_w,
3044 height_m: 9.0, arm_length_m: 1.5, is_operational: true, age_years: 0.0 }
3045 }
3046 pub fn illuminance_lux_at_road(&self) -> f32 {
3047 let luminous_efficacy = match &self.lighting_type {
3048 LightingType::LED => 130.0,
3049 LightingType::HPS => 90.0,
3050 LightingType::MH => 80.0,
3051 LightingType::Fluorescent => 70.0,
3052 LightingType::Incandescent => 15.0,
3053 };
3054 let lumens = self.wattage_w * luminous_efficacy;
3055 let h2 = self.height_m * self.height_m;
3056 let area = std::f32::consts::PI * h2; lumens / area.max(1.0)
3058 }
3059 pub fn annual_energy_kwh(&self) -> f32 { self.wattage_w / 1000.0 * 4000.0 } pub fn annual_energy_cost_usd(&self, rate_per_kwh: f32) -> f32 {
3061 self.annual_energy_kwh() * rate_per_kwh
3062 }
3063 pub fn lighting_type_str(&self) -> &'static str {
3064 match &self.lighting_type {
3065 LightingType::HPS => "High Pressure Sodium",
3066 LightingType::LED => "LED",
3067 LightingType::MH => "Metal Halide",
3068 LightingType::Fluorescent => "Fluorescent",
3069 LightingType::Incandescent => "Incandescent",
3070 }
3071 }
3072 pub fn is_energy_efficient(&self) -> bool {
3073 matches!(&self.lighting_type, LightingType::LED)
3074 }
3075}
3076
3077#[derive(Debug, Clone, Default)]
3078pub struct LightingInventory {
3079 pub road_id: String,
3080 pub lights: Vec<StreetLight>,
3081}
3082
3083impl LightingInventory {
3084 pub fn new(road_id: &str) -> Self { Self { road_id: road_id.to_string(), lights: Vec::new() } }
3085 pub fn add(&mut self, l: StreetLight) { self.lights.push(l); }
3086 pub fn count(&self) -> usize { self.lights.len() }
3087 pub fn operational_count(&self) -> usize { self.lights.iter().filter(|l| l.is_operational).count() }
3088 pub fn total_annual_energy_kwh(&self) -> f32 {
3089 self.lights.iter().map(|l| l.annual_energy_kwh()).sum()
3090 }
3091 pub fn average_spacing_m(&self, road_length_m: f32) -> f32 {
3092 if self.lights.is_empty() { return 0.0; }
3093 road_length_m / self.lights.len() as f32
3094 }
3095 pub fn led_percentage(&self) -> f32 {
3096 if self.lights.is_empty() { return 0.0; }
3097 let led = self.lights.iter().filter(|l| l.is_energy_efficient()).count();
3098 led as f32 / self.lights.len() as f32 * 100.0
3099 }
3100 pub fn outage_rate(&self) -> f32 {
3101 if self.lights.is_empty() { return 0.0; }
3102 let outages = self.lights.iter().filter(|l| !l.is_operational).count();
3103 outages as f32 / self.lights.len() as f32 * 100.0
3104 }
3105}
3106
3107#[derive(Debug, Clone, PartialEq)]
3112pub enum CrashSeverity { Fatal, Serious, Minor, PropertyDamageOnly }
3113
3114#[derive(Debug, Clone, PartialEq)]
3115pub enum CrashType {
3116 RearEnd, SideSwipe, HeadOn, RightAngle, SingleVehicle,
3117 PedestrianInvolvement, BicycleInvolvement, AnimalInvolvement, Other,
3118}
3119
3120#[derive(Debug, Clone)]
3121pub struct CrashRecord {
3122 pub crash_id: u32,
3123 pub station_m: f32,
3124 pub severity: CrashSeverity,
3125 pub crash_type: CrashType,
3126 pub year: u32,
3127 pub month: u8,
3128 pub time_hour: u8,
3129 pub road_condition: String,
3130 pub light_condition: String,
3131 pub vehicles_involved: u8,
3132}
3133
3134impl CrashRecord {
3135 pub fn new(crash_id: u32, station_m: f32, severity: CrashSeverity, crash_type: CrashType, year: u32) -> Self {
3136 Self { crash_id, station_m, severity, crash_type, year, month: 1, time_hour: 12,
3137 road_condition: "Dry".to_string(), light_condition: "Daylight".to_string(), vehicles_involved: 2 }
3138 }
3139 pub fn severity_weight(&self) -> f32 {
3140 match &self.severity {
3141 CrashSeverity::Fatal => 20.0,
3142 CrashSeverity::Serious => 5.0,
3143 CrashSeverity::Minor => 1.5,
3144 CrashSeverity::PropertyDamageOnly => 1.0,
3145 }
3146 }
3147 pub fn is_wet_road(&self) -> bool {
3148 self.road_condition.to_lowercase().contains("wet") ||
3149 self.road_condition.to_lowercase().contains("ice") ||
3150 self.road_condition.to_lowercase().contains("snow")
3151 }
3152 pub fn is_night_crash(&self) -> bool { self.time_hour < 6 || self.time_hour >= 20 }
3153}
3154
3155#[derive(Debug, Clone, Default)]
3156pub struct CrashDatabase {
3157 pub road_id: String,
3158 pub road_length_km: f32,
3159 pub crashes: Vec<CrashRecord>,
3160 pub exposure_years: f32,
3161 pub aadt: u32,
3162}
3163
3164impl CrashDatabase {
3165 pub fn new(road_id: &str, road_length_km: f32) -> Self {
3166 Self { road_id: road_id.to_string(), road_length_km, crashes: Vec::new(),
3167 exposure_years: 3.0, aadt: 10000 }
3168 }
3169 pub fn add_crash(&mut self, c: CrashRecord) { self.crashes.push(c); }
3170 pub fn total_crashes(&self) -> usize { self.crashes.len() }
3171 pub fn fatal_crashes(&self) -> usize {
3172 self.crashes.iter().filter(|c| matches!(&c.severity, CrashSeverity::Fatal)).count()
3173 }
3174 pub fn serious_crashes(&self) -> usize {
3175 self.crashes.iter().filter(|c| matches!(&c.severity, CrashSeverity::Serious)).count()
3176 }
3177 pub fn crash_rate_per_mvkmt(&self) -> f32 {
3178 let mvkmt = self.aadt as f32 * 365.0 * self.exposure_years * self.road_length_km / 1_000_000.0;
3179 if mvkmt < 0.001 { return 0.0; }
3180 self.total_crashes() as f32 / mvkmt
3181 }
3182 pub fn severity_index(&self) -> f32 {
3183 if self.crashes.is_empty() { return 0.0; }
3184 self.crashes.iter().map(|c| c.severity_weight()).sum::<f32>() / self.crashes.len() as f32
3185 }
3186 pub fn wet_road_percentage(&self) -> f32 {
3187 if self.crashes.is_empty() { return 0.0; }
3188 let wet = self.crashes.iter().filter(|c| c.is_wet_road()).count();
3189 wet as f32 / self.crashes.len() as f32 * 100.0
3190 }
3191 pub fn night_crash_percentage(&self) -> f32 {
3192 if self.crashes.is_empty() { return 0.0; }
3193 let night = self.crashes.iter().filter(|c| c.is_night_crash()).count();
3194 night as f32 / self.crashes.len() as f32 * 100.0
3195 }
3196 pub fn black_spots(&self, radius_m: f32, min_crashes: usize) -> Vec<f32> {
3197 let mut spots = Vec::new();
3198 let stations: Vec<f32> = self.crashes.iter().map(|c| c.station_m).collect();
3199 for &sta in &stations {
3200 let count = stations.iter().filter(|&&s| (s - sta).abs() <= radius_m).count();
3201 if count >= min_crashes && !spots.iter().any(|&s: &f32| (s - sta).abs() < radius_m) {
3202 spots.push(sta);
3203 }
3204 }
3205 spots
3206 }
3207}
3208
3209#[derive(Debug, Clone, PartialEq)]
3214pub enum CalmingDeviceType {
3215 SpeedHump, SpeedTable, RaisedCrossing, RaisedIntersection,
3216 NeckDown, Chicane, RoundAbout, SplitterIsland, TrafficCircle,
3217}
3218
3219#[derive(Debug, Clone)]
3220pub struct CalmingDevice {
3221 pub id: u32,
3222 pub device_type: CalmingDeviceType,
3223 pub station_m: f32,
3224 pub installation_year: u32,
3225 pub expected_speed_reduction_kph: f32,
3226 pub construction_cost_usd: f32,
3227}
3228
3229impl CalmingDevice {
3230 pub fn new(id: u32, device_type: CalmingDeviceType, station_m: f32) -> Self {
3231 let (reduction, cost) = match &device_type {
3232 CalmingDeviceType::SpeedHump => (12.0, 3000.0),
3233 CalmingDeviceType::SpeedTable => (8.0, 8000.0),
3234 CalmingDeviceType::RaisedCrossing => (6.0, 15000.0),
3235 CalmingDeviceType::RaisedIntersection => (10.0, 25000.0),
3236 CalmingDeviceType::NeckDown => (4.0, 12000.0),
3237 CalmingDeviceType::Chicane => (15.0, 20000.0),
3238 CalmingDeviceType::RoundAbout => (20.0, 80000.0),
3239 CalmingDeviceType::SplitterIsland => (5.0, 10000.0),
3240 CalmingDeviceType::TrafficCircle => (18.0, 60000.0),
3241 };
3242 Self { id, device_type, station_m, installation_year: 2024,
3243 expected_speed_reduction_kph: reduction, construction_cost_usd: cost }
3244 }
3245 pub fn device_type_str(&self) -> &'static str {
3246 match &self.device_type {
3247 CalmingDeviceType::SpeedHump => "Speed Hump",
3248 CalmingDeviceType::SpeedTable => "Speed Table",
3249 CalmingDeviceType::RaisedCrossing => "Raised Crossing",
3250 CalmingDeviceType::RaisedIntersection => "Raised Intersection",
3251 CalmingDeviceType::NeckDown => "Neck Down / Bulb-Out",
3252 CalmingDeviceType::Chicane => "Chicane",
3253 CalmingDeviceType::RoundAbout => "Roundabout",
3254 CalmingDeviceType::SplitterIsland => "Splitter Island",
3255 CalmingDeviceType::TrafficCircle => "Traffic Circle",
3256 }
3257 }
3258 pub fn cost_per_kph_reduction(&self) -> f32 {
3259 if self.expected_speed_reduction_kph < 0.1 { return 0.0; }
3260 self.construction_cost_usd / self.expected_speed_reduction_kph
3261 }
3262}
3263
3264#[derive(Debug, Clone, Default)]
3265pub struct TrafficCalmingPlan {
3266 pub road_id: String,
3267 pub devices: Vec<CalmingDevice>,
3268 pub target_85th_percentile_kph: f32,
3269}
3270
3271impl TrafficCalmingPlan {
3272 pub fn new(road_id: &str, target_speed_kph: f32) -> Self {
3273 Self { road_id: road_id.to_string(), devices: Vec::new(), target_85th_percentile_kph: target_speed_kph }
3274 }
3275 pub fn add_device(&mut self, d: CalmingDevice) { self.devices.push(d); }
3276 pub fn total_cost_usd(&self) -> f32 { self.devices.iter().map(|d| d.construction_cost_usd).sum() }
3277 pub fn total_speed_reduction_kph(&self) -> f32 {
3278 self.devices.iter().map(|d| d.expected_speed_reduction_kph).sum()
3279 }
3280 pub fn count(&self) -> usize { self.devices.len() }
3281}
3282
3283pub fn run_road_network_tests() {
3288 let mut network = ExtRoadNetwork::new("CITY-CORE");
3289 network.add_node(ExtNetworkNode::new(0, 0.0, 0.0));
3290 network.add_node(ExtNetworkNode::new(1, 500.0, 0.0));
3291 network.add_node(ExtNetworkNode::new(2, 500.0, 500.0));
3292 network.add_node(ExtNetworkNode::new(3, 0.0, 500.0));
3293 network.add_edge(ExtNetworkEdge::new(1, 0, 1, 500.0, 50.0));
3294 network.add_edge(ExtNetworkEdge::new(2, 1, 2, 500.0, 50.0));
3295 network.add_edge(ExtNetworkEdge::new(3, 2, 3, 500.0, 50.0));
3296 network.add_edge(ExtNetworkEdge::new(4, 3, 0, 500.0, 50.0));
3297 assert_eq!(network.node_count(), 4);
3298 assert_eq!(network.edge_count(), 4);
3299 let path = network.bfs_path(0, 2);
3300 assert!(path.is_some());
3301 assert!(path.unwrap().len() >= 3);
3302 let total_km = network.total_length_km();
3303 assert!((total_km - 2.0).abs() < 0.01);
3304}
3305
3306pub fn run_crash_analysis_tests() {
3307 let mut db = CrashDatabase::new("HWY-101", 5.0);
3308 db.aadt = 15000;
3309 db.exposure_years = 3.0;
3310 db.add_crash(CrashRecord::new(1, 1200.0, CrashSeverity::Minor, CrashType::RearEnd, 2022));
3311 db.add_crash(CrashRecord::new(2, 1250.0, CrashSeverity::Serious, CrashType::HeadOn, 2022));
3312 db.add_crash(CrashRecord::new(3, 3500.0, CrashSeverity::PropertyDamageOnly, CrashType::SideSwipe, 2023));
3313 assert_eq!(db.total_crashes(), 3);
3314 assert_eq!(db.fatal_crashes(), 0);
3315 assert_eq!(db.serious_crashes(), 1);
3316 let rate = db.crash_rate_per_mvkmt();
3317 assert!(rate > 0.0);
3318 let black_spots = db.black_spots(200.0, 2);
3319 assert!(black_spots.len() >= 1);
3320}
3321
3322pub fn run_sign_inventory_tests() {
3323 let mut inv = ExtSignInventory::new("HWY-101");
3324 inv.add(ExtRoadSign::new(1, ExtSignType::Stop, 500.0, "NB"));
3325 inv.add(ExtRoadSign::new(2, ExtSignType::SpeedLimit, 1000.0, "NB"));
3326 inv.add(ExtRoadSign::new(3, ExtSignType::Warning, 1500.0, "NB"));
3327 assert_eq!(inv.count(), 3);
3328 let total_cost = inv.total_replacement_cost();
3329 assert!(total_cost > 0.0);
3330}
3331
3332pub fn run_condition_survey_tests() {
3333 let mut db = ConditionSurveyDatabase::new();
3334 db.add(ConditionSurveyRecord::new("SEC-001", 0.0, 500.0, 75.0, 8.0, 2.0, 45.0));
3335 db.add(ConditionSurveyRecord::new("SEC-002", 500.0, 1000.0, 45.0, 20.0, 7.0, 35.0));
3336 db.add(ConditionSurveyRecord::new("SEC-003", 1000.0, 1500.0, 85.0, 3.0, 1.5, 52.0));
3337 let avg_pci = db.average_pci();
3338 assert!((avg_pci - (75.0 + 45.0 + 85.0) / 3.0).abs() < 0.1);
3339 let repair_sections = db.sections_needing_repair();
3340 assert!(!repair_sections.is_empty());
3341 let worst = db.worst_sections(2);
3342 assert_eq!(worst.len(), 2);
3343 assert!(worst[0].pci <= worst[1].pci);
3344}
3345
3346pub fn run_active_transport_tests() {
3347 let mut net = ActiveTransportNetwork::new("MAIN-ST");
3348 net.add(ActiveTransportFacility::new(1, FacilityType::Sidewalk, 0.0, 500.0, 2.0));
3349 net.add(ActiveTransportFacility::new(2, FacilityType::BikeLane, 0.0, 500.0, 1.5));
3350 net.add(ActiveTransportFacility::new(3, FacilityType::ProtectedBikeLane, 500.0, 1000.0, 2.5));
3351 assert_eq!(net.facilities.len(), 3);
3352 let bike_len = net.bike_facility_length_m();
3353 assert!(bike_len > 0.0);
3354 let sidewalk_len = net.sidewalk_coverage_m();
3355 assert!((sidewalk_len - 500.0).abs() < 0.1);
3356 let ada_rate = net.ada_compliance_rate();
3357 assert!(ada_rate > 0.0);
3358}
3359
3360pub fn run_lighting_tests() {
3361 let mut inv = LightingInventory::new("HWY-101");
3362 inv.add(StreetLight::new(1, 0.0, "NB", LightingType::LED, 100.0));
3363 inv.add(StreetLight::new(2, 50.0, "NB", LightingType::HPS, 150.0));
3364 inv.add(StreetLight::new(3, 100.0, "NB", LightingType::LED, 100.0));
3365 assert_eq!(inv.count(), 3);
3366 assert_eq!(inv.operational_count(), 3);
3367 let energy = inv.total_annual_energy_kwh();
3368 assert!(energy > 0.0);
3369 let led_pct = inv.led_percentage();
3370 assert!((led_pct - 66.67).abs() < 0.1);
3371}
3372
3373pub fn run_traffic_calming_tests() {
3374 let mut plan = TrafficCalmingPlan::new("OAK-ST", 30.0);
3375 plan.add_device(CalmingDevice::new(1, CalmingDeviceType::SpeedHump, 100.0));
3376 plan.add_device(CalmingDevice::new(2, CalmingDeviceType::SpeedTable, 300.0));
3377 plan.add_device(CalmingDevice::new(3, CalmingDeviceType::RoundAbout, 500.0));
3378 assert_eq!(plan.count(), 3);
3379 let total_cost = plan.total_cost_usd();
3380 assert!(total_cost > 80000.0);
3381 let total_reduction = plan.total_speed_reduction_kph();
3382 assert!(total_reduction > 30.0);
3383}
3384
3385pub fn terrain_road_tool_extended_tests() {
3386 run_road_network_tests();
3387 run_crash_analysis_tests();
3388 run_sign_inventory_tests();
3389 run_condition_survey_tests();
3390 run_active_transport_tests();
3391 run_lighting_tests();
3392 run_traffic_calming_tests();
3393}
3394
3395pub fn terrain_road_tool_all_tests_v2() {
3396 terrain_road_tool_run_all_tests();
3397 terrain_road_tool_extended_tests();
3398}
3399
3400#[derive(Debug, Clone, PartialEq)]
3405pub enum MaintenanceCategory {
3406 Routine, Preventive, Corrective, Emergency, Capital,
3407}
3408
3409#[derive(Debug, Clone)]
3410pub struct MaintenanceWork {
3411 pub work_id: u32,
3412 pub description: String,
3413 pub category: MaintenanceCategory,
3414 pub start_station_m: f32,
3415 pub end_station_m: f32,
3416 pub planned_year: u32,
3417 pub estimated_cost_usd: f32,
3418 pub unit_cost: f32,
3419 pub quantity: f32,
3420 pub unit: String,
3421 pub priority: u8,
3422 pub is_complete: bool,
3423}
3424
3425impl MaintenanceWork {
3426 pub fn new(work_id: u32, description: &str, category: MaintenanceCategory, start_m: f32, end_m: f32, planned_year: u32) -> Self {
3427 Self { work_id, description: description.to_string(), category,
3428 start_station_m: start_m, end_station_m: end_m, planned_year,
3429 estimated_cost_usd: 0.0, unit_cost: 0.0, quantity: 0.0,
3430 unit: String::new(), priority: 3, is_complete: false }
3431 }
3432 pub fn length_m(&self) -> f32 { (self.end_station_m - self.start_station_m).abs() }
3433 pub fn compute_cost(&mut self, unit_cost: f32, quantity: f32, unit: &str) {
3434 self.unit_cost = unit_cost;
3435 self.quantity = quantity;
3436 self.unit = unit.to_string();
3437 self.estimated_cost_usd = unit_cost * quantity;
3438 }
3439 pub fn category_str(&self) -> &'static str {
3440 match &self.category {
3441 MaintenanceCategory::Routine => "Routine",
3442 MaintenanceCategory::Preventive => "Preventive",
3443 MaintenanceCategory::Corrective => "Corrective",
3444 MaintenanceCategory::Emergency => "Emergency",
3445 MaintenanceCategory::Capital => "Capital",
3446 }
3447 }
3448 pub fn is_high_priority(&self) -> bool { self.priority <= 2 }
3449}
3450
3451#[derive(Debug, Clone, Default)]
3452pub struct MaintenanceProgram {
3453 pub program_id: String,
3454 pub program_years: Vec<u32>,
3455 pub works: Vec<MaintenanceWork>,
3456 pub annual_budget_usd: f32,
3457}
3458
3459impl MaintenanceProgram {
3460 pub fn new(program_id: &str, annual_budget: f32) -> Self {
3461 Self { program_id: program_id.to_string(), program_years: Vec::new(), works: Vec::new(), annual_budget_usd: annual_budget }
3462 }
3463 pub fn add_work(&mut self, w: MaintenanceWork) { self.works.push(w); }
3464 pub fn total_cost_usd(&self) -> f32 { self.works.iter().map(|w| w.estimated_cost_usd).sum() }
3465 pub fn works_by_year(&self, year: u32) -> Vec<&MaintenanceWork> {
3466 self.works.iter().filter(|w| w.planned_year == year).collect()
3467 }
3468 pub fn annual_cost(&self, year: u32) -> f32 {
3469 self.works_by_year(year).iter().map(|w| w.estimated_cost_usd).sum()
3470 }
3471 pub fn budget_deficit(&self, year: u32) -> f32 {
3472 let cost = self.annual_cost(year);
3473 (cost - self.annual_budget_usd).max(0.0)
3474 }
3475 pub fn high_priority_works(&self) -> Vec<&MaintenanceWork> {
3476 self.works.iter().filter(|w| w.is_high_priority()).collect()
3477 }
3478 pub fn completion_rate(&self) -> f32 {
3479 if self.works.is_empty() { return 100.0; }
3480 let complete = self.works.iter().filter(|w| w.is_complete).count();
3481 complete as f32 / self.works.len() as f32 * 100.0
3482 }
3483 pub fn works_by_category(&self) -> HashMap<String, usize> {
3484 let mut map: HashMap<String, usize> = HashMap::new();
3485 for w in &self.works { *map.entry(w.category_str().to_string()).or_insert(0) += 1; }
3486 map
3487 }
3488 pub fn report(&self) -> String {
3489 let s = format!("MaintenanceProgram {} total_cost={:.0} budget={:.0} works={} complete={:.0}pct",
3490 self.program_id, self.total_cost_usd(), self.annual_budget_usd,
3491 self.works.len(), self.completion_rate());
3492 s
3493 }
3494}
3495
3496pub struct DesignStandardsChecker;
3501
3502impl DesignStandardsChecker {
3503 pub fn check_lane_width(width_m: f32, road_class: &str) -> (bool, String) {
3504 let min = match road_class { "highway" => 3.65, "arterial" => 3.5, "collector" => 3.0, _ => 2.7 };
3505 let ok = width_m >= min;
3506 let msg = if ok { format!("Lane width {:.2}m OK (min {:.2}m)", width_m, min) }
3507 else { format!("Lane width {:.2}m FAILS (min {:.2}m)", width_m, min) };
3508 (ok, msg)
3509 }
3510 pub fn check_shoulder_width(width_m: f32, road_class: &str) -> (bool, String) {
3511 let min = match road_class { "highway" => 3.0, "arterial" => 2.0, "collector" => 1.2, _ => 0.5 };
3512 let ok = width_m >= min;
3513 let msg = if ok { format!("Shoulder width {:.2}m OK (min {:.2}m)", width_m, min) }
3514 else { format!("Shoulder width {:.2}m FAILS (min {:.2}m)", width_m, min) };
3515 (ok, msg)
3516 }
3517 pub fn check_grade(grade_pct: f32, design_speed_kph: f32) -> (bool, String) {
3518 let max = if design_speed_kph >= 100.0 { 4.0 } else if design_speed_kph >= 80.0 { 5.0 } else { 6.0 };
3519 let ok = grade_pct.abs() <= max;
3520 let msg = if ok { format!("Grade {:.1}% OK (max {:.1}%)", grade_pct, max) }
3521 else { format!("Grade {:.1}% EXCEEDS max {:.1}%", grade_pct, max) };
3522 (ok, msg)
3523 }
3524 pub fn check_cross_slope(slope_pct: f32) -> (bool, String) {
3525 let ok = slope_pct >= 1.5 && slope_pct <= 3.0;
3526 let msg = if ok { format!("Cross slope {:.1}% OK (1.5-3.0%)", slope_pct) }
3527 else { format!("Cross slope {:.1}% outside 1.5-3.0%", slope_pct) };
3528 (ok, msg)
3529 }
3530 pub fn check_sight_distance(available_m: f32, design_speed_kph: f32) -> (bool, String) {
3531 let ssd = 0.278 * design_speed_kph * 2.5 + design_speed_kph.powi(2) / (254.0 * 0.35);
3532 let ok = available_m >= ssd;
3533 let msg = if ok { format!("SSD {:.1}m available >= {:.1}m required", available_m, ssd) }
3534 else { format!("SSD {:.1}m INSUFFICIENT (required {:.1}m)", available_m, ssd) };
3535 (ok, msg)
3536 }
3537 pub fn run_standard_checks(
3538 lane_width_m: f32,
3539 shoulder_width_m: f32,
3540 grade_pct: f32,
3541 cross_slope_pct: f32,
3542 sight_distance_m: f32,
3543 design_speed_kph: f32,
3544 road_class: &str,
3545 ) -> Vec<(String, bool)> {
3546 let mut results = Vec::new();
3547 let (ok, msg) = Self::check_lane_width(lane_width_m, road_class);
3548 results.push((msg, ok));
3549 let (ok, msg) = Self::check_shoulder_width(shoulder_width_m, road_class);
3550 results.push((msg, ok));
3551 let (ok, msg) = Self::check_grade(grade_pct, design_speed_kph);
3552 results.push((msg, ok));
3553 let (ok, msg) = Self::check_cross_slope(cross_slope_pct);
3554 results.push((msg, ok));
3555 let (ok, msg) = Self::check_sight_distance(sight_distance_m, design_speed_kph);
3556 results.push((msg, ok));
3557 results
3558 }
3559}
3560
3561pub fn run_design_standards_tests() {
3562 let results = DesignStandardsChecker::run_standard_checks(
3563 3.65, 3.0, 3.5, 2.0, 200.0, 80.0, "arterial"
3564 );
3565 assert_eq!(results.len(), 5);
3566 let passes: Vec<&bool> = results.iter().map(|(_, ok)| ok).collect();
3567 assert!(passes.iter().any(|&&ok| ok));
3568}
3569
3570pub fn run_maintenance_tests() {
3571 let mut prog = MaintenanceProgram::new("MAINT-2024-2028", 2_000_000.0);
3572 let mut w1 = MaintenanceWork::new(1, "Crack Sealing", MaintenanceCategory::Preventive, 0.0, 1000.0, 2024);
3573 w1.compute_cost(5.0, 1000.0, "m");
3574 w1.priority = 2;
3575 let mut w2 = MaintenanceWork::new(2, "Pothole Patching", MaintenanceCategory::Corrective, 500.0, 600.0, 2024);
3576 w2.compute_cost(150.0, 20.0, "m2");
3577 w2.priority = 1;
3578 let mut w3 = MaintenanceWork::new(3, "Overlay", MaintenanceCategory::Capital, 0.0, 2000.0, 2025);
3579 w3.compute_cost(25.0, 2000.0 * 7.0, "m2");
3580 w3.priority = 3;
3581 prog.add_work(w1);
3582 prog.add_work(w2);
3583 prog.add_work(w3);
3584 assert_eq!(prog.works.len(), 3);
3585 let hp = prog.high_priority_works();
3586 assert_eq!(hp.len(), 2);
3587 let cost_2024 = prog.annual_cost(2024);
3588 assert!(cost_2024 > 0.0);
3589 let deficit = prog.budget_deficit(2025);
3590 assert!(deficit >= 0.0);
3591 let completion = prog.completion_rate();
3592 assert!((completion - 0.0).abs() < 0.1);
3593}
3594
3595pub fn run_all_terrain_road_tool_final() {
3596 terrain_road_tool_all_tests_v2();
3597 run_design_standards_tests();
3598 run_maintenance_tests();
3599}
3600
3601
3602pub const SPEED_ZONE_DEFAULT_URBAN_KPH: f32 = 50.0;
3607pub const SPEED_ZONE_DEFAULT_RURAL_KPH: f32 = 100.0;
3608pub const SPEED_ZONE_SCHOOL_KPH: f32 = 25.0;
3609pub const SPEED_ZONE_CONSTRUCTION_KPH: f32 = 40.0;
3610
3611#[derive(Debug, Clone, PartialEq)]
3612pub enum ExtSpeedZoneType {
3613 Urban,
3614 Rural,
3615 HighSpeed,
3616 School,
3617 Hospital,
3618 Construction,
3619 Advisory,
3620 Variable,
3621}
3622
3623#[derive(Debug, Clone)]
3624pub struct ExtSpeedZone {
3625 pub id: u32,
3626 pub zone_type: ExtSpeedZoneType,
3627 pub posted_speed_kph: f32,
3628 pub start_chainage: f32,
3629 pub end_chainage: f32,
3630 pub active_hours_start: f32,
3631 pub active_hours_end: f32,
3632 pub enforcement_camera: bool,
3633 pub justification: String,
3634}
3635
3636impl ExtSpeedZone {
3637 pub fn new(id: u32, zone_type: ExtSpeedZoneType, speed_kph: f32, start: f32, end: f32) -> Self {
3638 ExtSpeedZone {
3639 id, zone_type, posted_speed_kph: speed_kph,
3640 start_chainage: start, end_chainage: end,
3641 active_hours_start: 0.0, active_hours_end: 24.0,
3642 enforcement_camera: false,
3643 justification: String::new(),
3644 }
3645 }
3646
3647 pub fn length(&self) -> f32 {
3648 (self.end_chainage - self.start_chainage).abs()
3649 }
3650
3651 pub fn is_active_at_hour(&self, hour: f32) -> bool {
3652 hour >= self.active_hours_start && hour < self.active_hours_end
3653 }
3654
3655 pub fn stopping_sight_distance(&self) -> f32 {
3656 let v_ms = self.posted_speed_kph / 3.6;
3658 let t_reaction = 2.5;
3659 let g = 9.81;
3660 let f_friction = 0.35;
3661 v_ms * t_reaction + (v_ms * v_ms) / (2.0 * g * f_friction)
3662 }
3663
3664 pub fn decision_sight_distance(&self) -> f32 {
3665 self.stopping_sight_distance() * 1.5
3667 }
3668}
3669
3670#[derive(Debug, Clone)]
3671pub struct ExtSpeedZoneInv {
3672 pub road_id: u32,
3673 pub zones: Vec<ExtSpeedZone>,
3674}
3675
3676impl ExtSpeedZoneInv {
3677 pub fn new(road_id: u32) -> Self {
3678 ExtSpeedZoneInv { road_id, zones: Vec::new() }
3679 }
3680
3681 pub fn add_zone(&mut self, zone: ExtSpeedZone) {
3682 self.zones.push(zone);
3683 self.zones.sort_by(|a, b| a.start_chainage.partial_cmp(&b.start_chainage).unwrap());
3684 }
3685
3686 pub fn zone_at_chainage(&self, ch: f32) -> Option<&ExtSpeedZone> {
3687 self.zones.iter().find(|z| ch >= z.start_chainage && ch <= z.end_chainage)
3688 }
3689
3690 pub fn school_zones(&self) -> Vec<&ExtSpeedZone> {
3691 self.zones.iter().filter(|z| z.zone_type == ExtSpeedZoneType::School).collect()
3692 }
3693}
3694
3695#[derive(Debug, Clone)]
3700pub struct ExtLane {
3701 pub id: u32,
3702 pub lane_type: LaneType,
3703 pub width_m: f32,
3704 pub direction: i32, pub surface_type: String,
3706 pub has_rumble_strip: bool,
3707 pub has_markings: bool,
3708 pub speed_kph: f32,
3709}
3710
3711impl ExtLane {
3712 pub fn new(id: u32, lane_type: LaneType, width_m: f32, direction: i32) -> Self {
3713 ExtLane {
3714 id, lane_type, width_m, direction,
3715 surface_type: "Asphalt".to_string(),
3716 has_rumble_strip: false, has_markings: true,
3717 speed_kph: 80.0,
3718 }
3719 }
3720}
3721
3722#[derive(Debug, Clone)]
3723pub struct Shoulder {
3724 pub width_m: f32,
3725 pub paved: bool,
3726 pub surface_type: String,
3727 pub has_barrier: bool,
3728}
3729
3730#[derive(Debug, Clone)]
3731pub struct Median {
3732 pub width_m: f32,
3733 pub raised: bool,
3734 pub has_barrier: bool,
3735 pub landscaped: bool,
3736}
3737
3738#[derive(Debug, Clone)]
3739pub struct RoadCrossSection {
3740 pub chainage: f32,
3741 pub lanes: Vec<ExtLane>,
3742 pub left_shoulder: Option<Shoulder>,
3743 pub right_shoulder: Option<Shoulder>,
3744 pub median: Option<Median>,
3745 pub total_width_m: f32,
3746 pub carriageway_width_m: f32,
3747 pub cut_fill_type: String,
3748 pub fill_height_m: f32,
3749 pub cut_depth_m: f32,
3750}
3751
3752impl RoadCrossSection {
3753 pub fn new(chainage: f32) -> Self {
3754 RoadCrossSection {
3755 chainage,
3756 lanes: Vec::new(),
3757 left_shoulder: None, right_shoulder: None,
3758 median: None,
3759 total_width_m: 0.0,
3760 carriageway_width_m: 0.0,
3761 cut_fill_type: "At-Grade".to_string(),
3762 fill_height_m: 0.0, cut_depth_m: 0.0,
3763 }
3764 }
3765
3766 pub fn compute_widths(&mut self) {
3767 self.carriageway_width_m = self.lanes.iter().map(|l| l.width_m).sum::<f32>()
3768 + self.median.as_ref().map(|m| m.width_m).unwrap_or(0.0);
3769 let ls = self.left_shoulder.as_ref().map(|s| s.width_m).unwrap_or(0.0);
3770 let rs = self.right_shoulder.as_ref().map(|s| s.width_m).unwrap_or(0.0);
3771 self.total_width_m = self.carriageway_width_m + ls + rs;
3772 }
3773
3774 pub fn lane_count_by_direction(&self, dir: i32) -> usize {
3775 self.lanes.iter().filter(|l| l.direction == dir).count()
3776 }
3777}
3778
3779#[derive(Debug, Clone)]
3784pub struct EarthworkSection {
3785 pub start_chainage: f32,
3786 pub end_chainage: f32,
3787 pub start_area_m2: f32,
3788 pub end_area_m2: f32,
3789 pub is_cut: bool,
3790}
3791
3792impl EarthworkSection {
3793 pub fn volume_prismatoid_m3(&self) -> f32 {
3794 let l = (self.end_chainage - self.start_chainage).abs();
3795 (self.start_area_m2 + self.end_area_m2) / 2.0 * l
3797 }
3798
3799 pub fn volume_prismatoid_corrected_m3(&self, mid_area_m2: f32) -> f32 {
3800 let l = (self.end_chainage - self.start_chainage).abs();
3801 l / 6.0 * (self.start_area_m2 + 4.0 * mid_area_m2 + self.end_area_m2)
3803 }
3804}
3805
3806#[derive(Debug, Clone)]
3807pub struct MassHaulDiagram {
3808 pub stations: Vec<f32>,
3809 pub ordinates: Vec<f32>,
3810 pub freehaul_distance: f32,
3811 pub overhaul_rate_per_m3_station: f32,
3812}
3813
3814impl MassHaulDiagram {
3815 pub fn new(freehaul_distance: f32) -> Self {
3816 MassHaulDiagram {
3817 stations: Vec::new(),
3818 ordinates: Vec::new(),
3819 freehaul_distance,
3820 overhaul_rate_per_m3_station: 0.05,
3821 }
3822 }
3823
3824 pub fn build(&mut self, sections: &[EarthworkSection]) {
3825 let mut cumulative = 0.0f32;
3826 self.stations.clear();
3827 self.ordinates.clear();
3828 self.stations.push(sections.first().map(|s| s.start_chainage).unwrap_or(0.0));
3829 self.ordinates.push(0.0);
3830 for sec in sections {
3831 let vol = sec.volume_prismatoid_m3();
3832 cumulative += if sec.is_cut { vol } else { -vol };
3833 self.stations.push(sec.end_chainage);
3834 self.ordinates.push(cumulative);
3835 }
3836 }
3837
3838 pub fn balance_point(&self) -> Option<f32> {
3839 for i in 1..self.ordinates.len() {
3841 if self.ordinates[i - 1] * self.ordinates[i] < 0.0 {
3842 let frac = self.ordinates[i - 1] / (self.ordinates[i - 1] - self.ordinates[i]);
3843 return Some(self.stations[i - 1] + frac * (self.stations[i] - self.stations[i - 1]));
3844 }
3845 }
3846 None
3847 }
3848
3849 pub fn total_cut_m3(&self) -> f32 {
3850 self.ordinates.iter().cloned().fold(f32::NEG_INFINITY, f32::max).max(0.0)
3851 }
3852
3853 pub fn total_fill_m3(&self) -> f32 {
3854 (-self.ordinates.iter().cloned().fold(f32::INFINITY, f32::min)).max(0.0)
3855 }
3856}
3857
3858pub const STORMWATER_RUNOFF_COEFF_PAVEMENT: f32 = 0.90;
3863pub const STORMWATER_RUNOFF_COEFF_LAWN: f32 = 0.25;
3864pub const STORMWATER_RUNOFF_COEFF_GRAVEL: f32 = 0.60;
3865pub const STORMWATER_MANNING_CONCRETE: f32 = 0.013;
3866pub const STORMWATER_MANNING_EARTHEN: f32 = 0.030;
3867
3868#[derive(Debug, Clone)]
3869pub struct CatchmentArea {
3870 pub id: u32,
3871 pub area_ha: f32,
3872 pub runoff_coefficient: f32,
3873 pub time_of_concentration_min: f32,
3874 pub slope_pct: f32,
3875 pub description: String,
3876}
3877
3878impl CatchmentArea {
3879 pub fn new(id: u32, area_ha: f32, runoff_coeff: f32) -> Self {
3880 CatchmentArea {
3881 id, area_ha, runoff_coefficient: runoff_coeff,
3882 time_of_concentration_min: 10.0,
3883 slope_pct: 1.0,
3884 description: String::new(),
3885 }
3886 }
3887
3888 pub fn rational_flow_m3s(&self, rainfall_intensity_mm_hr: f32) -> f32 {
3889 self.runoff_coefficient * rainfall_intensity_mm_hr * self.area_ha / 360.0
3891 }
3892}
3893
3894#[derive(Debug, Clone)]
3895pub struct StormDrainPipe {
3896 pub id: u32,
3897 pub diameter_mm: f32,
3898 pub material: String,
3899 pub manning_n: f32,
3900 pub slope_percent: f32,
3901 pub length_m: f32,
3902 pub upstream_invert: f32,
3903 pub downstream_invert: f32,
3904}
3905
3906impl StormDrainPipe {
3907 pub fn new(id: u32, diameter_mm: f32, slope_pct: f32, length_m: f32) -> Self {
3908 StormDrainPipe {
3909 id, diameter_mm, material: "Concrete".to_string(),
3910 manning_n: STORMWATER_MANNING_CONCRETE,
3911 slope_percent: slope_pct, length_m,
3912 upstream_invert: 0.0, downstream_invert: 0.0,
3913 }
3914 }
3915
3916 pub fn full_flow_capacity_m3s(&self) -> f32 {
3917 let r_m = self.diameter_mm / 2000.0;
3919 let area = std::f32::consts::PI * r_m * r_m;
3920 let hydraulic_radius = r_m / 2.0;
3921 let slope = self.slope_percent / 100.0;
3922 (1.0 / self.manning_n) * area * hydraulic_radius.powf(2.0 / 3.0) * slope.sqrt()
3923 }
3924
3925 pub fn velocity_full_ms(&self) -> f32 {
3926 let r_m = self.diameter_mm / 2000.0;
3927 let hydraulic_radius = r_m / 2.0;
3928 let slope = self.slope_percent / 100.0;
3929 (1.0 / self.manning_n) * hydraulic_radius.powf(2.0 / 3.0) * slope.sqrt()
3930 }
3931
3932 pub fn is_self_cleansing(&self) -> bool {
3933 self.velocity_full_ms() >= 0.6
3934 }
3935
3936 pub fn travel_time_min(&self) -> f32 {
3937 let v = self.velocity_full_ms().max(0.001);
3938 self.length_m / v / 60.0
3939 }
3940}
3941
3942#[derive(Debug, Clone)]
3943pub struct OpenChannel {
3944 pub id: u32,
3945 pub base_width_m: f32,
3946 pub side_slope_ratio: f32,
3947 pub depth_m: f32,
3948 pub manning_n: f32,
3949 pub slope_percent: f32,
3950 pub length_m: f32,
3951}
3952
3953impl OpenChannel {
3954 pub fn new(id: u32, base_m: f32, depth_m: f32, slope_pct: f32) -> Self {
3955 OpenChannel {
3956 id, base_width_m: base_m,
3957 side_slope_ratio: 2.0,
3958 depth_m,
3959 manning_n: STORMWATER_MANNING_EARTHEN,
3960 slope_percent: slope_pct,
3961 length_m: 100.0,
3962 }
3963 }
3964
3965 pub fn flow_area_m2(&self) -> f32 {
3966 (self.base_width_m + self.side_slope_ratio * self.depth_m) * self.depth_m
3967 }
3968
3969 pub fn wetted_perimeter_m(&self) -> f32 {
3970 self.base_width_m + 2.0 * self.depth_m * (1.0 + self.side_slope_ratio * self.side_slope_ratio).sqrt()
3971 }
3972
3973 pub fn hydraulic_radius_m(&self) -> f32 {
3974 let p = self.wetted_perimeter_m();
3975 if p <= 0.0 { return 0.0; }
3976 self.flow_area_m2() / p
3977 }
3978
3979 pub fn capacity_m3s(&self) -> f32 {
3980 let slope = self.slope_percent / 100.0;
3981 (1.0 / self.manning_n)
3982 * self.flow_area_m2()
3983 * self.hydraulic_radius_m().powf(2.0 / 3.0)
3984 * slope.sqrt()
3985 }
3986
3987 pub fn freeboard_m(&self, design_flow: f32) -> f32 {
3988 let capacity = self.capacity_m3s();
3990 if capacity <= 0.0 { return 0.0; }
3991 let flow_ratio = (design_flow / capacity).min(1.0);
3992 self.depth_m * (1.0 - flow_ratio)
3993 }
3994}
3995
3996pub const IRI_THRESHOLD_GOOD: f32 = 2.5;
4001pub const IRI_THRESHOLD_FAIR: f32 = 4.5;
4002pub const IRI_THRESHOLD_POOR: f32 = 7.0;
4003pub const PSR_NEW_PAVEMENT: f32 = 4.5;
4004pub const PSR_TERMINAL: f32 = 2.0;
4005
4006#[derive(Debug, Clone)]
4007pub struct PavementPerformanceModel {
4008 pub section_id: u32,
4009 pub initial_iri: f32,
4010 pub deterioration_rate: f32,
4011 pub traffic_esal_annual: f64,
4012 pub climate_factor: f32,
4013 pub age_years: f32,
4014}
4015
4016impl PavementPerformanceModel {
4017 pub fn new(section_id: u32, initial_iri: f32, esal: f64) -> Self {
4018 PavementPerformanceModel {
4019 section_id, initial_iri,
4020 deterioration_rate: 0.15,
4021 traffic_esal_annual: esal,
4022 climate_factor: 1.0,
4023 age_years: 0.0,
4024 }
4025 }
4026
4027 pub fn iri_at_age(&self, years: f32) -> f32 {
4028 let traffic_factor = (self.traffic_esal_annual as f32 / 1_000_000.0).sqrt();
4030 self.initial_iri + self.deterioration_rate * years * self.climate_factor * (1.0 + traffic_factor * 0.1)
4031 }
4032
4033 pub fn condition_at_age(&self, years: f32) -> &'static str {
4034 let iri = self.iri_at_age(years);
4035 if iri < IRI_THRESHOLD_GOOD { "Good" }
4036 else if iri < IRI_THRESHOLD_FAIR { "Fair" }
4037 else if iri < IRI_THRESHOLD_POOR { "Poor" }
4038 else { "Very Poor" }
4039 }
4040
4041 pub fn years_to_terminal(&self) -> f32 {
4042 let terminal_iri = IRI_THRESHOLD_POOR;
4043 if self.initial_iri >= terminal_iri { return 0.0; }
4044 let traffic_factor = (self.traffic_esal_annual as f32 / 1_000_000.0).sqrt();
4045 let rate = self.deterioration_rate * self.climate_factor * (1.0 + traffic_factor * 0.1);
4046 if rate <= 0.0 { return f32::INFINITY; }
4047 (terminal_iri - self.initial_iri) / rate
4048 }
4049
4050 pub fn remaining_service_life(&self) -> f32 {
4051 (self.years_to_terminal() - self.age_years).max(0.0)
4052 }
4053
4054 pub fn treatment_recommendation(&self) -> &'static str {
4055 let iri = self.iri_at_age(self.age_years);
4056 if iri < 2.0 { "No treatment needed" }
4057 else if iri < IRI_THRESHOLD_GOOD { "Preventive maintenance" }
4058 else if iri < IRI_THRESHOLD_FAIR { "Minor rehabilitation" }
4059 else if iri < IRI_THRESHOLD_POOR { "Major rehabilitation" }
4060 else { "Reconstruction" }
4061 }
4062}
4063
4064#[derive(Debug, Clone)]
4065pub struct PavementNetwork {
4066 pub sections: Vec<PavementPerformanceModel>,
4067 pub total_lane_km: f32,
4068 pub budget_annual: f64,
4069}
4070
4071impl PavementNetwork {
4072 pub fn new(budget: f64) -> Self {
4073 PavementNetwork { sections: Vec::new(), total_lane_km: 0.0, budget_annual: budget }
4074 }
4075
4076 pub fn add_section(&mut self, section: PavementPerformanceModel) {
4077 self.sections.push(section);
4078 }
4079
4080 pub fn network_iri_average(&self) -> f32 {
4081 if self.sections.is_empty() { return 0.0; }
4082 self.sections.iter().map(|s| s.iri_at_age(s.age_years)).sum::<f32>() / self.sections.len() as f32
4083 }
4084
4085 pub fn sections_needing_treatment(&self) -> Vec<&PavementPerformanceModel> {
4086 self.sections.iter()
4087 .filter(|s| s.iri_at_age(s.age_years) >= IRI_THRESHOLD_GOOD)
4088 .collect()
4089 }
4090
4091 pub fn network_condition_distribution(&self) -> HashMap<&'static str, usize> {
4092 let mut dist: HashMap<&'static str, usize> = HashMap::new();
4093 for s in &self.sections {
4094 let cond = s.condition_at_age(s.age_years);
4095 *dist.entry(cond).or_insert(0) += 1;
4096 }
4097 dist
4098 }
4099}
4100
4101#[derive(Debug, Clone, PartialEq)]
4106pub enum BridgeType {
4107 BeamBridge,
4108 ArchBridge,
4109 SuspensionBridge,
4110 CableStayed,
4111 TrussBridge,
4112 BoxGirder,
4113 Culvert,
4114 Underpass,
4115}
4116
4117#[derive(Debug, Clone)]
4118pub struct BridgeSpan {
4119 pub span_number: u32,
4120 pub length_m: f32,
4121 pub width_m: f32,
4122 pub deck_elevation: f32,
4123 pub clearance_m: f32,
4124}
4125
4126#[derive(Debug, Clone)]
4127pub struct Bridge {
4128 pub id: u32,
4129 pub name: String,
4130 pub bridge_type: BridgeType,
4131 pub total_length_m: f32,
4132 pub carriageway_width_m: f32,
4133 pub spans: Vec<BridgeSpan>,
4134 pub design_load_kn_m2: f32,
4135 pub construction_year: u32,
4136 pub inspection_rating: f32,
4137 pub material: String,
4138 pub water_crossing: bool,
4139 pub min_clearance_m: f32,
4140}
4141
4142impl Bridge {
4143 pub fn new(id: u32, name: &str, bridge_type: BridgeType) -> Self {
4144 Bridge {
4145 id, name: name.to_string(), bridge_type,
4146 total_length_m: 0.0, carriageway_width_m: 7.3,
4147 spans: Vec::new(),
4148 design_load_kn_m2: 5.0,
4149 construction_year: 2000,
4150 inspection_rating: 4.0,
4151 material: "Reinforced Concrete".to_string(),
4152 water_crossing: false, min_clearance_m: 4.5,
4153 }
4154 }
4155
4156 pub fn add_span(&mut self, span: BridgeSpan) {
4157 self.total_length_m += span.length_m;
4158 self.spans.push(span);
4159 }
4160
4161 pub fn span_count(&self) -> usize {
4162 self.spans.len()
4163 }
4164
4165 pub fn requires_inspection(&self) -> bool {
4166 self.inspection_rating < 3.0
4167 }
4168
4169 pub fn deck_area_m2(&self) -> f32 {
4170 self.total_length_m * self.carriageway_width_m
4171 }
4172}
4173
4174#[derive(Debug, Clone)]
4179pub struct DesignSpeed {
4180 pub speed_kph: f32,
4181 pub min_horizontal_radius_m: f32,
4182 pub max_superelevation_pct: f32,
4183 pub min_stopping_sight_distance_m: f32,
4184 pub min_crest_k: f32,
4185 pub min_sag_k: f32,
4186}
4187
4188impl DesignSpeed {
4189 pub fn for_speed(kph: f32) -> Self {
4190 let v = kph;
4191 let r_min = v * v / (127.0 * (0.10 + 0.14));
4192 let ssd = v / 3.6 * 2.5 + (v / 3.6) * (v / 3.6) / (2.0 * 9.81 * 0.35);
4193 DesignSpeed {
4194 speed_kph: kph,
4195 min_horizontal_radius_m: r_min,
4196 max_superelevation_pct: 10.0,
4197 min_stopping_sight_distance_m: ssd,
4198 min_crest_k: ssd * ssd / (2.0 * ssd * 0.105 + 0.022 * ssd - 2.6),
4199 min_sag_k: ssd * ssd / (120.0 + 3.5 * ssd),
4200 }
4201 }
4202}
4203
4204#[derive(Debug, Clone)]
4205pub struct RoadGeometryReport {
4206 pub road_id: u32,
4207 pub total_length_m: f32,
4208 pub design_speed_kph: f32,
4209 pub horizontal_curve_count: u32,
4210 pub vertical_curve_count: u32,
4211 pub min_radius_found_m: f32,
4212 pub max_grade_pct: f32,
4213 pub non_compliant_elements: Vec<String>,
4214 pub compliant: bool,
4215}
4216
4217impl RoadGeometryReport {
4218 pub fn new(road_id: u32) -> Self {
4219 RoadGeometryReport {
4220 road_id, total_length_m: 0.0, design_speed_kph: 80.0,
4221 horizontal_curve_count: 0, vertical_curve_count: 0,
4222 min_radius_found_m: f32::INFINITY, max_grade_pct: 0.0,
4223 non_compliant_elements: Vec::new(), compliant: true,
4224 }
4225 }
4226
4227 pub fn check_radius(&mut self, radius_m: f32) {
4228 let params = DesignSpeed::for_speed(self.design_speed_kph);
4229 if radius_m < params.min_horizontal_radius_m {
4230 self.non_compliant_elements.push(
4231 format!("Radius {:.1}m < min {:.1}m for {}kph", radius_m, params.min_horizontal_radius_m, self.design_speed_kph)
4232 );
4233 self.compliant = false;
4234 }
4235 if radius_m < self.min_radius_found_m { self.min_radius_found_m = radius_m; }
4236 }
4237
4238 pub fn check_grade(&mut self, grade_pct: f32) {
4239 let max_allowed = if self.design_speed_kph >= 100.0 { 5.0 } else if self.design_speed_kph >= 80.0 { 7.0 } else { 10.0 };
4240 if grade_pct.abs() > max_allowed {
4241 self.non_compliant_elements.push(
4242 format!("Grade {:.1}% > max {:.1}% for {}kph", grade_pct, max_allowed, self.design_speed_kph)
4243 );
4244 self.compliant = false;
4245 }
4246 if grade_pct.abs() > self.max_grade_pct { self.max_grade_pct = grade_pct.abs(); }
4247 }
4248}
4249
4250#[derive(Debug, Clone)]
4255pub struct NoiseSensitiveReceiver {
4256 pub id: u32,
4257 pub name: String,
4258 pub location: Vec2,
4259 pub receiver_type: String,
4260 pub naaqs_criterion_dba: f32,
4261 pub predicted_noise_dba: f32,
4262 pub existing_noise_dba: f32,
4263 pub impact_threshold_increase_dba: f32,
4264}
4265
4266impl NoiseSensitiveReceiver {
4267 pub fn new(id: u32, name: &str, location: Vec2, criterion: f32) -> Self {
4268 NoiseSensitiveReceiver {
4269 id, name: name.to_string(), location,
4270 receiver_type: "Residential".to_string(),
4271 naaqs_criterion_dba: criterion,
4272 predicted_noise_dba: 0.0,
4273 existing_noise_dba: 0.0,
4274 impact_threshold_increase_dba: 3.0,
4275 }
4276 }
4277
4278 pub fn is_impacted(&self) -> bool {
4279 self.predicted_noise_dba > self.naaqs_criterion_dba
4280 || (self.predicted_noise_dba - self.existing_noise_dba) > self.impact_threshold_increase_dba
4281 }
4282
4283 pub fn excess_noise_dba(&self) -> f32 {
4284 (self.predicted_noise_dba - self.naaqs_criterion_dba).max(0.0)
4285 }
4286}
4287
4288#[derive(Debug, Clone)]
4289pub struct AirQualityImpact {
4290 pub receptor_id: u32,
4291 pub location: Vec2,
4292 pub pm25_ug_m3: f32,
4293 pub pm10_ug_m3: f32,
4294 pub no2_ppb: f32,
4295 pub co_ppm: f32,
4296 pub exceeds_standard: bool,
4297}
4298
4299impl AirQualityImpact {
4300 pub fn check_standards(&mut self) {
4301 self.exceeds_standard = self.pm25_ug_m3 > 35.0
4303 || self.pm10_ug_m3 > 150.0
4304 || self.no2_ppb > 100.0
4305 || self.co_ppm > 9.0;
4306 }
4307}
4308
4309#[derive(Debug, Clone)]
4310pub struct EnvironmentalImpactReport {
4311 pub project_id: u32,
4312 pub noise_receivers: Vec<NoiseSensitiveReceiver>,
4313 pub air_quality_impacts: Vec<AirQualityImpact>,
4314 pub impacted_wetland_ha: f32,
4315 pub impacted_threatened_species: Vec<String>,
4316 pub mitigation_measures: Vec<String>,
4317 pub overall_significance: String,
4318}
4319
4320impl EnvironmentalImpactReport {
4321 pub fn new(project_id: u32) -> Self {
4322 EnvironmentalImpactReport {
4323 project_id,
4324 noise_receivers: Vec::new(),
4325 air_quality_impacts: Vec::new(),
4326 impacted_wetland_ha: 0.0,
4327 impacted_threatened_species: Vec::new(),
4328 mitigation_measures: Vec::new(),
4329 overall_significance: "To be determined".to_string(),
4330 }
4331 }
4332
4333 pub fn noise_impacts_count(&self) -> usize {
4334 self.noise_receivers.iter().filter(|r| r.is_impacted()).count()
4335 }
4336
4337 pub fn air_exceedances_count(&self) -> usize {
4338 self.air_quality_impacts.iter().filter(|a| a.exceeds_standard).count()
4339 }
4340
4341 pub fn add_mitigation(&mut self, measure: &str) {
4342 self.mitigation_measures.push(measure.to_string());
4343 }
4344
4345 pub fn assess_significance(&mut self) {
4346 let noise_impacts = self.noise_impacts_count();
4347 let air_exceedances = self.air_exceedances_count();
4348 let has_wetlands = self.impacted_wetland_ha > 0.0;
4349 let has_species = !self.impacted_threatened_species.is_empty();
4350
4351 self.overall_significance = if noise_impacts > 10 || air_exceedances > 0 || has_wetlands || has_species {
4352 "Significant"
4353 } else if noise_impacts > 3 {
4354 "Moderate"
4355 } else {
4356 "Minor"
4357 }.to_string();
4358 }
4359}
4360
4361#[derive(Debug, Clone)]
4366pub struct SafetyTreatment {
4367 pub id: u32,
4368 pub name: String,
4369 pub unit_cost: f64,
4370 pub estimated_crash_reduction_pct: f32,
4371 pub applicable_crash_types: Vec<String>,
4372}
4373
4374#[derive(Debug, Clone)]
4375pub struct SafetyBenefitCost {
4376 pub treatment_id: u32,
4377 pub location_id: u32,
4378 pub annual_crash_cost_before: f64,
4379 pub annual_crash_cost_after: f64,
4380 pub implementation_cost: f64,
4381 pub analysis_period_years: u32,
4382 pub discount_rate: f32,
4383}
4384
4385impl SafetyBenefitCost {
4386 pub fn npv_benefits(&self) -> f64 {
4387 let annual_saving = self.annual_crash_cost_before - self.annual_crash_cost_after;
4388 let r = self.discount_rate as f64;
4389 let n = self.analysis_period_years as f64;
4390 if r == 0.0 { return annual_saving * n; }
4391 annual_saving * (1.0 - (1.0 + r).powf(-n)) / r
4392 }
4393
4394 pub fn bcr(&self) -> f64 {
4395 if self.implementation_cost <= 0.0 { return f64::INFINITY; }
4396 self.npv_benefits() / self.implementation_cost
4397 }
4398
4399 pub fn payback_years(&self) -> f64 {
4400 let annual_saving = self.annual_crash_cost_before - self.annual_crash_cost_after;
4401 if annual_saving <= 0.0 { return f64::INFINITY; }
4402 self.implementation_cost / annual_saving
4403 }
4404}
4405
4406#[cfg(test)]
4411mod tests_road_extended {
4412 use super::*;
4413
4414 #[test]
4415 fn test_speed_zone_ssd() {
4416 let zone = ExtSpeedZone::new(1, ExtSpeedZoneType::Urban, 50.0, 0.0, 500.0);
4417 let ssd = zone.stopping_sight_distance();
4418 assert!(ssd > 30.0 && ssd < 80.0);
4419 }
4420
4421 #[test]
4422 fn test_storm_drain_capacity() {
4423 let pipe = StormDrainPipe::new(1, 600.0, 0.5, 50.0);
4424 let q = pipe.full_flow_capacity_m3s();
4425 assert!(q > 0.1);
4426 assert!(pipe.is_self_cleansing());
4427 }
4428
4429 #[test]
4430 fn test_open_channel_capacity() {
4431 let chan = OpenChannel::new(1, 2.0, 1.0, 0.5);
4432 let q = chan.capacity_m3s();
4433 assert!(q > 0.0);
4434 }
4435
4436 #[test]
4437 fn test_pavement_performance() {
4438 let mut model = PavementPerformanceModel::new(1, 1.5, 500_000.0);
4439 model.age_years = 10.0;
4440 let iri = model.iri_at_age(10.0);
4441 assert!(iri > 1.5);
4442 let rsl = model.remaining_service_life();
4443 assert!(rsl >= 0.0);
4444 }
4445
4446 #[test]
4447 fn test_mass_haul() {
4448 let sections = vec![
4449 EarthworkSection { start_chainage: 0.0, end_chainage: 100.0, start_area_m2: 10.0, end_area_m2: 15.0, is_cut: true },
4450 EarthworkSection { start_chainage: 100.0, end_chainage: 200.0, start_area_m2: 8.0, end_area_m2: 5.0, is_cut: false },
4451 ];
4452 let mut diagram = MassHaulDiagram::new(200.0);
4453 diagram.build(§ions);
4454 assert_eq!(diagram.stations.len(), 3);
4455 }
4456
4457 #[test]
4458 fn test_bridge_deck_area() {
4459 let mut bridge = Bridge::new(1, "Test Bridge", BridgeType::BeamBridge);
4460 bridge.add_span(BridgeSpan { span_number: 1, length_m: 30.0, width_m: 9.0, deck_elevation: 10.0, clearance_m: 5.5 });
4461 assert_eq!(bridge.span_count(), 1);
4462 assert!((bridge.deck_area_m2() - 30.0 * 7.3).abs() < 1.0);
4463 }
4464
4465 #[test]
4466 fn test_road_geometry_report() {
4467 let mut report = RoadGeometryReport::new(1);
4468 report.design_speed_kph = 80.0;
4469 let params = DesignSpeed::for_speed(80.0);
4470 report.check_radius(params.min_horizontal_radius_m * 1.5);
4471 assert!(report.compliant);
4472 report.check_radius(10.0);
4473 assert!(!report.compliant);
4474 }
4475
4476 #[test]
4477 fn test_cross_section_widths() {
4478 let mut cs = RoadCrossSection::new(500.0);
4479 cs.lanes.push(ExtLane::new(0, LaneType::ThroughLane, 3.5, 1));
4480 cs.lanes.push(ExtLane::new(1, LaneType::ThroughLane, 3.5, -1));
4481 cs.compute_widths();
4482 assert!((cs.carriageway_width_m - 7.0).abs() < 0.01);
4483 }
4484
4485 #[test]
4486 fn test_eia_noise_impact() {
4487 let mut receiver = NoiseSensitiveReceiver::new(1, "School", Vec2::new(100.0, 0.0), 60.0);
4488 receiver.predicted_noise_dba = 65.0;
4489 assert!(receiver.is_impacted());
4490 assert!((receiver.excess_noise_dba() - 5.0).abs() < 0.1);
4491 }
4492
4493 #[test]
4494 fn test_safety_bcr() {
4495 let bcr_calc = SafetyBenefitCost {
4496 treatment_id: 1, location_id: 5,
4497 annual_crash_cost_before: 200_000.0,
4498 annual_crash_cost_after: 100_000.0,
4499 implementation_cost: 500_000.0,
4500 analysis_period_years: 10,
4501 discount_rate: 0.07,
4502 };
4503 let bcr = bcr_calc.bcr();
4504 assert!(bcr > 1.0);
4505 }
4506}
4507
4508pub fn terrain_road_module_version() -> &'static str { "2.3.0" }
4509pub fn terrain_road_features() -> &'static [&'static str] {
4510 &["speed_zones", "cross_sections", "earthworks", "stormwater",
4511 "pavement_performance", "bridges", "environmental_impact", "safety_program"]
4512}
4513
4514
4515pub const ROUNDABOUT_MIN_INSCRIBED_DIAMETER_M: f32 = 14.0;
4520pub const ROUNDABOUT_MAX_ENTRY_SPEED_KPH: f32 = 30.0;
4521
4522#[derive(Debug, Clone, PartialEq)]
4523pub enum RoundaboutType {
4524 MiniRoundabout,
4525 SingleLane,
4526 MultiLane,
4527 Turbo,
4528 TrumpetInterchange,
4529}
4530
4531#[derive(Debug, Clone)]
4532pub struct Roundabout {
4533 pub id: u32,
4534 pub roundabout_type: RoundaboutType,
4535 pub inscribed_diameter_m: f32,
4536 pub central_island_diameter_m: f32,
4537 pub circulatory_lane_count: u32,
4538 pub circulatory_lane_width_m: f32,
4539 pub entries: Vec<RoundaboutEntry>,
4540 pub mountable_apron_width_m: f32,
4541 pub design_speed_kph: f32,
4542}
4543
4544impl Roundabout {
4545 pub fn new(id: u32, roundabout_type: RoundaboutType, inscribed_diameter: f32) -> Self {
4546 let central = inscribed_diameter * 0.45;
4547 Roundabout {
4548 id, roundabout_type, inscribed_diameter_m: inscribed_diameter,
4549 central_island_diameter_m: central,
4550 circulatory_lane_count: 1,
4551 circulatory_lane_width_m: 4.0,
4552 entries: Vec::new(),
4553 mountable_apron_width_m: 2.0,
4554 design_speed_kph: 25.0,
4555 }
4556 }
4557
4558 pub fn circulatory_road_width(&self) -> f32 {
4559 self.circulatory_lane_count as f32 * self.circulatory_lane_width_m
4560 }
4561
4562 pub fn add_entry(&mut self, entry: RoundaboutEntry) {
4563 self.entries.push(entry);
4564 }
4565
4566 pub fn entry_count(&self) -> usize { self.entries.len() }
4567
4568 pub fn is_4_way(&self) -> bool { self.entries.len() == 4 }
4569
4570 pub fn capacity_estimate_vph(&self) -> f32 {
4571 let qe_max = 1200.0;
4573 let qi_factor = 0.9;
4574 self.entries.iter()
4575 .map(|e| qe_max * e.lane_count as f32 * qi_factor)
4576 .sum::<f32>() / self.entries.len() as f32
4577 }
4578}
4579
4580#[derive(Debug, Clone, PartialEq)]
4585pub enum InterchangeType {
4586 Diamond,
4587 Cloverleaf,
4588 Diverging_Diamond,
4589 SinglePointUrban,
4590 FolioTrumpet,
4591 HalfCloverleaf,
4592 StackInterchange,
4593 Roundabout_Interchange,
4594}
4595
4596#[derive(Debug, Clone)]
4597pub struct RampConnection {
4598 pub ramp_id: u32,
4599 pub from_road_id: u32,
4600 pub to_road_id: u32,
4601 pub ramp_type: String,
4602 pub length_m: f32,
4603 pub speed_kph: f32,
4604 pub lane_count: u32,
4605}
4606
4607#[derive(Debug, Clone)]
4608pub struct Interchange {
4609 pub id: u32,
4610 pub name: String,
4611 pub interchange_type: InterchangeType,
4612 pub position: Vec2,
4613 pub ramps: Vec<RampConnection>,
4614 pub grade_separation: bool,
4615 pub total_area_ha: f32,
4616 pub construction_cost_estimate: f64,
4617}
4618
4619impl Interchange {
4620 pub fn new(id: u32, name: &str, interchange_type: InterchangeType) -> Self {
4621 Interchange {
4622 id, name: name.to_string(), interchange_type,
4623 position: Vec2::ZERO, ramps: Vec::new(),
4624 grade_separation: true, total_area_ha: 0.0,
4625 construction_cost_estimate: 0.0,
4626 }
4627 }
4628
4629 pub fn ramp_count(&self) -> usize { self.ramps.len() }
4630
4631 pub fn total_ramp_length(&self) -> f32 {
4632 self.ramps.iter().map(|r| r.length_m).sum()
4633 }
4634}
4635
4636#[derive(Debug, Clone)]
4641pub struct SightDistanceCheck {
4642 pub location_chainage: f32,
4643 pub required_ssd_m: f32,
4644 pub available_ssd_m: f32,
4645 pub required_psd_m: f32,
4646 pub available_psd_m: f32,
4647 pub compliant: bool,
4648 pub obstruction_type: Option<String>,
4649}
4650
4651impl SightDistanceCheck {
4652 pub fn new(chainage: f32, speed_kph: f32) -> Self {
4653 let zone = ExtSpeedZone::new(0, ExtSpeedZoneType::Rural, speed_kph, 0.0, 1000.0);
4654 let ssd = zone.stopping_sight_distance();
4655 let psd = ssd * 2.5;
4656 SightDistanceCheck {
4657 location_chainage: chainage,
4658 required_ssd_m: ssd,
4659 available_ssd_m: 0.0,
4660 required_psd_m: psd,
4661 available_psd_m: 0.0,
4662 compliant: false,
4663 obstruction_type: None,
4664 }
4665 }
4666
4667 pub fn evaluate(&mut self) {
4668 self.compliant = self.available_ssd_m >= self.required_ssd_m;
4669 }
4670
4671 pub fn ssd_deficiency(&self) -> f32 {
4672 (self.required_ssd_m - self.available_ssd_m).max(0.0)
4673 }
4674}
4675
4676#[derive(Debug, Clone)]
4677pub struct SightDistanceProfile {
4678 pub road_id: u32,
4679 pub checks: Vec<SightDistanceCheck>,
4680 pub check_interval_m: f32,
4681}
4682
4683impl SightDistanceProfile {
4684 pub fn new(road_id: u32, interval_m: f32) -> Self {
4685 SightDistanceProfile { road_id, checks: Vec::new(), check_interval_m: interval_m }
4686 }
4687
4688 pub fn add_check(&mut self, check: SightDistanceCheck) {
4689 self.checks.push(check);
4690 }
4691
4692 pub fn non_compliant_count(&self) -> usize {
4693 self.checks.iter().filter(|c| !c.compliant).count()
4694 }
4695
4696 pub fn worst_deficiency(&self) -> f32 {
4697 self.checks.iter().map(|c| c.ssd_deficiency()).fold(0.0_f32, f32::max)
4698 }
4699
4700 pub fn compliance_rate(&self) -> f32 {
4701 if self.checks.is_empty() { return 1.0; }
4702 let compliant = self.checks.iter().filter(|c| c.compliant).count();
4703 compliant as f32 / self.checks.len() as f32
4704 }
4705}
4706
4707pub const SIGNAL_LOST_TIME_PER_PHASE: f32 = 4.0;
4712pub const SIGNAL_MIN_GREEN_S: f32 = 7.0;
4713pub const SIGNAL_SATURATION_FLOW_RATE_PCE_HR: f32 = 1800.0;
4714
4715#[derive(Debug, Clone)]
4716pub struct SignalPhaseExtended {
4717 pub phase_id: u32,
4718 pub description: String,
4719 pub movements: Vec<String>,
4720 pub min_green_s: f32,
4721 pub max_green_s: f32,
4722 pub actual_green_s: f32,
4723 pub yellow_s: f32,
4724 pub all_red_s: f32,
4725 pub volume_pce_hr: f32,
4726 pub saturation_flow_pce_hr: f32,
4727}
4728
4729impl SignalPhaseExtended {
4730 pub fn new(phase_id: u32, desc: &str) -> Self {
4731 SignalPhaseExtended {
4732 phase_id, description: desc.to_string(),
4733 movements: Vec::new(),
4734 min_green_s: SIGNAL_MIN_GREEN_S,
4735 max_green_s: 60.0,
4736 actual_green_s: 30.0,
4737 yellow_s: 3.5,
4738 all_red_s: 1.5,
4739 volume_pce_hr: 0.0,
4740 saturation_flow_pce_hr: SIGNAL_SATURATION_FLOW_RATE_PCE_HR,
4741 }
4742 }
4743
4744 pub fn flow_ratio(&self) -> f32 {
4745 if self.saturation_flow_pce_hr <= 0.0 { return 0.0; }
4746 self.volume_pce_hr / self.saturation_flow_pce_hr
4747 }
4748
4749 pub fn effective_green_s(&self) -> f32 {
4750 self.actual_green_s + self.yellow_s - SIGNAL_LOST_TIME_PER_PHASE
4751 }
4752
4753 pub fn degree_of_saturation(&self, cycle_s: f32) -> f32 {
4754 let cap = self.saturation_flow_pce_hr * self.effective_green_s() / cycle_s;
4755 if cap <= 0.0 { return f32::INFINITY; }
4756 self.volume_pce_hr / cap
4757 }
4758}
4759
4760#[derive(Debug, Clone)]
4761pub struct TrafficSignalController {
4762 pub intersection_id: u32,
4763 pub cycle_length_s: f32,
4764 pub phases: Vec<SignalPhaseExtended>,
4765 pub offset_s: f32,
4766 pub actuated: bool,
4767 pub coord_group: Option<u32>,
4768}
4769
4770impl TrafficSignalController {
4771 pub fn new(intersection_id: u32) -> Self {
4772 TrafficSignalController {
4773 intersection_id, cycle_length_s: 90.0,
4774 phases: Vec::new(), offset_s: 0.0,
4775 actuated: true, coord_group: None,
4776 }
4777 }
4778
4779 pub fn add_phase(&mut self, phase: SignalPhaseExtended) {
4780 self.phases.push(phase);
4781 }
4782
4783 pub fn total_lost_time(&self) -> f32 {
4784 self.phases.len() as f32 * SIGNAL_LOST_TIME_PER_PHASE
4785 }
4786
4787 pub fn effective_cycle_s(&self) -> f32 {
4788 self.cycle_length_s - self.total_lost_time()
4789 }
4790
4791 pub fn critical_flow_ratio_sum(&self) -> f32 {
4792 self.phases.iter().map(|p| p.flow_ratio()).fold(0.0_f32, f32::max)
4793 }
4794
4795 pub fn webster_optimal_cycle(&self) -> f32 {
4796 let l = self.total_lost_time();
4797 let y = self.critical_flow_ratio_sum();
4798 if y >= 1.0 { return 120.0; }
4799 ((1.5 * l + 5.0) / (1.0 - y)).clamp(60.0, 120.0)
4800 }
4801
4802 pub fn current_phase_at(&self, time_in_cycle: f32) -> Option<&SignalPhaseExtended> {
4803 let mut elapsed = 0.0;
4804 for phase in &self.phases {
4805 let phase_dur = phase.actual_green_s + phase.yellow_s + phase.all_red_s;
4806 if time_in_cycle < elapsed + phase_dur {
4807 return Some(phase);
4808 }
4809 elapsed += phase_dur;
4810 }
4811 None
4812 }
4813}
4814
4815#[derive(Debug, Clone)]
4820pub struct ExtRoadSegment {
4821 pub segment_id: u32,
4822 pub road_name: String,
4823 pub road_number: String,
4824 pub start_chainage: f32,
4825 pub end_chainage: f32,
4826 pub lanes_each_direction: u32,
4827 pub carriageway_width_m: f32,
4828 pub surface_type: String,
4829 pub pavement_age_years: u32,
4830 pub speed_limit_kph: f32,
4831 pub aadt: u32,
4832 pub truck_pct: f32,
4833 pub local_authority: String,
4834 pub urban_rural: String,
4835 pub functional_class: String,
4836}
4837
4838impl ExtRoadSegment {
4839 pub fn new(segment_id: u32, road_name: &str) -> Self {
4840 ExtRoadSegment {
4841 segment_id, road_name: road_name.to_string(),
4842 road_number: String::new(),
4843 start_chainage: 0.0, end_chainage: 0.0,
4844 lanes_each_direction: 1,
4845 carriageway_width_m: 7.0,
4846 surface_type: "Asphalt".to_string(),
4847 pavement_age_years: 0,
4848 speed_limit_kph: 80.0,
4849 aadt: 0, truck_pct: 10.0,
4850 local_authority: String::new(),
4851 urban_rural: "Rural".to_string(),
4852 functional_class: "Collector".to_string(),
4853 }
4854 }
4855
4856 pub fn length_km(&self) -> f32 {
4857 (self.end_chainage - self.start_chainage).abs() / 1000.0
4858 }
4859
4860 pub fn lane_km(&self) -> f32 {
4861 self.length_km() * self.lanes_each_direction as f32 * 2.0
4862 }
4863
4864 pub fn annual_esal(&self) -> f64 {
4865 let trucks = self.aadt as f64 * self.truck_pct as f64 / 100.0 * 365.0;
4866 trucks * 2.5 }
4868}
4869
4870#[derive(Debug, Clone)]
4871pub struct ExtRoadInventory {
4872 pub inventory_id: String,
4873 pub year: u32,
4874 pub segments: Vec<ExtRoadSegment>,
4875}
4876
4877impl ExtRoadInventory {
4878 pub fn new(year: u32) -> Self {
4879 ExtRoadInventory { inventory_id: format!("INV-{}", year), year, segments: Vec::new() }
4880 }
4881
4882 pub fn add_segment(&mut self, seg: ExtRoadSegment) {
4883 self.segments.push(seg);
4884 }
4885
4886 pub fn total_lane_km(&self) -> f32 {
4887 self.segments.iter().map(|s| s.lane_km()).sum()
4888 }
4889
4890 pub fn total_network_km(&self) -> f32 {
4891 self.segments.iter().map(|s| s.length_km()).sum()
4892 }
4893
4894 pub fn segments_by_surface(&self, surface: &str) -> Vec<&ExtRoadSegment> {
4895 self.segments.iter().filter(|s| s.surface_type == surface).collect()
4896 }
4897}
4898
4899#[cfg(test)]
4904mod tests_terrain_road_final {
4905 use super::*;
4906
4907 #[test]
4908 fn test_roundabout_capacity() {
4909 let mut ra = Roundabout::new(1, RoundaboutType::SingleLane, 28.0);
4910 ra.add_entry(RoundaboutEntry { approach_volume: 400.0, entry_width: 4.0, entry_radius: 20.0, flare_length: 20.0, inscribed_diameter: 28.0, entry_id: 0, bearing_deg: 0.0, lane_count: 1, entry_width_m: 4.0, flare_length_m: 20.0, approach_speed_kph: 50.0, design_flow_vph: 400, pedestrian_crossing: true });
4911 ra.add_entry(RoundaboutEntry { approach_volume: 400.0, entry_width: 4.0, entry_radius: 20.0, flare_length: 20.0, inscribed_diameter: 28.0, entry_id: 1, bearing_deg: 90.0, lane_count: 1, entry_width_m: 4.0, flare_length_m: 20.0, approach_speed_kph: 50.0, design_flow_vph: 350, pedestrian_crossing: true });
4912 ra.add_entry(RoundaboutEntry { approach_volume: 400.0, entry_width: 4.0, entry_radius: 20.0, flare_length: 20.0, inscribed_diameter: 28.0, entry_id: 2, bearing_deg: 180.0, lane_count: 1, entry_width_m: 4.0, flare_length_m: 20.0, approach_speed_kph: 50.0, design_flow_vph: 380, pedestrian_crossing: true });
4913 ra.add_entry(RoundaboutEntry { approach_volume: 400.0, entry_width: 4.0, entry_radius: 20.0, flare_length: 20.0, inscribed_diameter: 28.0, entry_id: 3, bearing_deg: 270.0, lane_count: 1, entry_width_m: 4.0, flare_length_m: 20.0, approach_speed_kph: 50.0, design_flow_vph: 320, pedestrian_crossing: true });
4914 assert!(ra.is_4_way());
4915 assert!(ra.capacity_estimate_vph() > 0.0);
4916 }
4917
4918 #[test]
4919 fn test_sight_distance_profile() {
4920 let mut profile = SightDistanceProfile::new(1, 100.0);
4921 let mut check = SightDistanceCheck::new(500.0, 80.0);
4922 check.available_ssd_m = check.required_ssd_m + 20.0;
4923 check.evaluate();
4924 profile.add_check(check);
4925 assert_eq!(profile.non_compliant_count(), 0);
4926 assert!((profile.compliance_rate() - 1.0).abs() < 0.001);
4927 }
4928
4929 #[test]
4930 fn test_signal_controller_webster() {
4931 let mut ctrl = TrafficSignalController::new(1);
4932 let mut p1 = SignalPhaseExtended::new(0, "NS Through");
4933 p1.volume_pce_hr = 600.0;
4934 let mut p2 = SignalPhaseExtended::new(1, "EW Through");
4935 p2.volume_pce_hr = 500.0;
4936 ctrl.add_phase(p1);
4937 ctrl.add_phase(p2);
4938 let optimal = ctrl.webster_optimal_cycle();
4939 assert!(optimal >= 60.0 && optimal <= 120.0);
4940 }
4941
4942 #[test]
4943 fn test_road_inventory_totals() {
4944 let mut inv = ExtRoadInventory::new(2024);
4945 let mut seg = ExtRoadSegment::new(1, "Main Street");
4946 seg.start_chainage = 0.0;
4947 seg.end_chainage = 5000.0;
4948 seg.lanes_each_direction = 2;
4949 inv.add_segment(seg);
4950 assert!((inv.total_network_km() - 5.0).abs() < 0.001);
4951 assert!((inv.total_lane_km() - 20.0).abs() < 0.001);
4952 }
4953
4954 #[test]
4955 fn test_interchange_ramp_length() {
4956 let mut ic = Interchange::new(1, "Highway Exit 42", InterchangeType::Diamond);
4957 ic.ramps.push(RampConnection { ramp_id: 0, from_road_id: 1, to_road_id: 2, ramp_type: "On-Ramp".to_string(), length_m: 250.0, speed_kph: 80.0, lane_count: 1 });
4958 ic.ramps.push(RampConnection { ramp_id: 1, from_road_id: 2, to_road_id: 1, ramp_type: "Off-Ramp".to_string(), length_m: 220.0, speed_kph: 60.0, lane_count: 1 });
4959 assert_eq!(ic.ramp_count(), 2);
4960 assert!((ic.total_ramp_length() - 470.0).abs() < 0.001);
4961 }
4962}
4963
4964pub fn terrain_road_final_info() -> &'static str {
4965 "TerrainRoadTool v2.3: Roundabouts, Interchanges, SightDistance, Signals, Inventory"
4966}
4967
4968
4969#[derive(Debug, Clone, PartialEq)]
4974pub enum AssetConditionGrade {
4975 VeryGood, Good, Fair, Poor, VeryPoor, Failed,
4976}
4977
4978impl AssetConditionGrade {
4979 pub fn from_score(score: f32) -> Self {
4980 if score >= 85.0 { AssetConditionGrade::VeryGood }
4981 else if score >= 70.0 { AssetConditionGrade::Good }
4982 else if score >= 55.0 { AssetConditionGrade::Fair }
4983 else if score >= 40.0 { AssetConditionGrade::Poor }
4984 else if score >= 20.0 { AssetConditionGrade::VeryPoor }
4985 else { AssetConditionGrade::Failed }
4986 }
4987
4988 pub fn score_midpoint(&self) -> f32 {
4989 match self {
4990 AssetConditionGrade::VeryGood => 92.5,
4991 AssetConditionGrade::Good => 77.5,
4992 AssetConditionGrade::Fair => 62.5,
4993 AssetConditionGrade::Poor => 47.5,
4994 AssetConditionGrade::VeryPoor => 30.0,
4995 AssetConditionGrade::Failed => 10.0,
4996 }
4997 }
4998}
4999
5000#[derive(Debug, Clone)]
5001pub struct InfrastructureAsset {
5002 pub asset_id: String,
5003 pub asset_type: String,
5004 pub location_chainage: f32,
5005 pub road_id: u32,
5006 pub condition_score: f32,
5007 pub installation_year: u32,
5008 pub expected_life_years: u32,
5009 pub replacement_cost: f64,
5010 pub maintenance_cost_annual: f64,
5011 pub last_inspection_year: u32,
5012}
5013
5014impl InfrastructureAsset {
5015 pub fn new(asset_id: &str, asset_type: &str, road_id: u32, location: f32) -> Self {
5016 InfrastructureAsset {
5017 asset_id: asset_id.to_string(),
5018 asset_type: asset_type.to_string(),
5019 location_chainage: location, road_id,
5020 condition_score: 100.0,
5021 installation_year: 2000,
5022 expected_life_years: 20,
5023 replacement_cost: 0.0,
5024 maintenance_cost_annual: 0.0,
5025 last_inspection_year: 2000,
5026 }
5027 }
5028
5029 pub fn age(&self, current_year: u32) -> u32 {
5030 current_year.saturating_sub(self.installation_year)
5031 }
5032
5033 pub fn remaining_life(&self, current_year: u32) -> i32 {
5034 let age = self.age(current_year) as i32;
5035 self.expected_life_years as i32 - age
5036 }
5037
5038 pub fn condition_grade(&self) -> AssetConditionGrade {
5039 AssetConditionGrade::from_score(self.condition_score)
5040 }
5041
5042 pub fn lifecycle_cost(&self) -> f64 {
5043 let periods = (self.expected_life_years as f64 / 20.0).ceil();
5044 self.replacement_cost * periods + self.maintenance_cost_annual * self.expected_life_years as f64
5045 }
5046}
5047
5048#[derive(Debug, Clone)]
5049pub struct AssetManagementPlan {
5050 pub plan_id: String,
5051 pub year: u32,
5052 pub assets: Vec<InfrastructureAsset>,
5053 pub budget: f64,
5054 pub priority_threshold_score: f32,
5055}
5056
5057impl AssetManagementPlan {
5058 pub fn new(plan_id: &str, year: u32, budget: f64) -> Self {
5059 AssetManagementPlan {
5060 plan_id: plan_id.to_string(), year, assets: Vec::new(), budget, priority_threshold_score: 60.0,
5061 }
5062 }
5063
5064 pub fn add_asset(&mut self, asset: InfrastructureAsset) {
5065 self.assets.push(asset);
5066 }
5067
5068 pub fn priority_assets(&self) -> Vec<&InfrastructureAsset> {
5069 self.assets.iter()
5070 .filter(|a| a.condition_score < self.priority_threshold_score)
5071 .collect()
5072 }
5073
5074 pub fn total_replacement_cost(&self) -> f64 {
5075 self.assets.iter().map(|a| a.replacement_cost).sum()
5076 }
5077
5078 pub fn funded_assets(&self) -> Vec<&InfrastructureAsset> {
5079 let mut sorted: Vec<&InfrastructureAsset> = self.priority_assets();
5080 sorted.sort_by(|a, b| a.condition_score.partial_cmp(&b.condition_score).unwrap());
5081 let mut budget_remaining = self.budget;
5082 let mut funded = Vec::new();
5083 for asset in sorted {
5084 if asset.replacement_cost <= budget_remaining {
5085 budget_remaining -= asset.replacement_cost;
5086 funded.push(asset);
5087 }
5088 }
5089 funded
5090 }
5091}
5092
5093#[derive(Debug, Clone, PartialEq)]
5098pub enum SnowRemovalPriority {
5099 P1_Emergency, P2_Primary, P3_Secondary, P4_Residential, P5_Low,
5100}
5101
5102#[derive(Debug, Clone)]
5103pub struct SnowRoute {
5104 pub route_id: u32,
5105 pub priority: SnowRemovalPriority,
5106 pub road_segments: Vec<u32>,
5107 pub total_km: f32,
5108 pub truck_id: Option<u32>,
5109 pub salt_rate_kg_km: f32,
5110 pub plow_passes_required: u32,
5111 pub estimated_cycle_time_hr: f32,
5112}
5113
5114impl SnowRoute {
5115 pub fn new(route_id: u32, priority: SnowRemovalPriority) -> Self {
5116 let salt_rate = match priority {
5117 SnowRemovalPriority::P1_Emergency => 30.0,
5118 SnowRemovalPriority::P2_Primary => 25.0,
5119 SnowRemovalPriority::P3_Secondary => 20.0,
5120 _ => 15.0,
5121 };
5122 SnowRoute {
5123 route_id, priority, road_segments: Vec::new(),
5124 total_km: 0.0, truck_id: None, salt_rate_kg_km: salt_rate,
5125 plow_passes_required: 1, estimated_cycle_time_hr: 4.0,
5126 }
5127 }
5128
5129 pub fn total_salt_kg(&self) -> f32 {
5130 self.total_km * self.salt_rate_kg_km * self.plow_passes_required as f32
5131 }
5132
5133 pub fn add_segment(&mut self, segment_id: u32, length_km: f32) {
5134 self.road_segments.push(segment_id);
5135 self.total_km += length_km;
5136 }
5137}
5138
5139#[derive(Debug, Clone)]
5140pub struct SnowControlPlan {
5141 pub routes: Vec<SnowRoute>,
5142 pub salt_stockpile_tonnes: f32,
5143 pub truck_count: u32,
5144 pub depot_locations: Vec<Vec2>,
5145}
5146
5147impl SnowControlPlan {
5148 pub fn new(truck_count: u32) -> Self {
5149 SnowControlPlan {
5150 routes: Vec::new(),
5151 salt_stockpile_tonnes: 0.0,
5152 truck_count,
5153 depot_locations: Vec::new(),
5154 }
5155 }
5156
5157 pub fn total_salt_required_kg(&self) -> f32 {
5158 self.routes.iter().map(|r| r.total_salt_kg()).sum()
5159 }
5160
5161 pub fn has_sufficient_salt(&self) -> bool {
5162 self.total_salt_required_kg() / 1000.0 <= self.salt_stockpile_tonnes
5163 }
5164
5165 pub fn routes_by_priority(&self, priority: &SnowRemovalPriority) -> Vec<&SnowRoute> {
5166 self.routes.iter().filter(|r| &r.priority == priority).collect()
5167 }
5168}
5169
5170#[derive(Debug, Clone, PartialEq)]
5175pub enum UtilityType {
5176 PowerLine, WaterMain, SewerMain, GasMain, TelecomCable,
5177 FiberOptic, StormDrain, HotWaterPipe, TrafficControl, Irrigation,
5178}
5179
5180#[derive(Debug, Clone)]
5181pub struct UtilityRecord {
5182 pub utility_id: String,
5183 pub utility_type: UtilityType,
5184 pub owner: String,
5185 pub horizontal_offset_m: f32,
5186 pub depth_m: f32,
5187 pub diameter_mm: f32,
5188 pub material: String,
5189 pub installation_year: u32,
5190 pub start_chainage: f32,
5191 pub end_chainage: f32,
5192 pub active: bool,
5193}
5194
5195impl UtilityRecord {
5196 pub fn new(utility_id: &str, utility_type: UtilityType, owner: &str) -> Self {
5197 UtilityRecord {
5198 utility_id: utility_id.to_string(), utility_type, owner: owner.to_string(),
5199 horizontal_offset_m: 0.0, depth_m: 1.0,
5200 diameter_mm: 200.0, material: "PVC".to_string(),
5201 installation_year: 2000,
5202 start_chainage: 0.0, end_chainage: 100.0,
5203 active: true,
5204 }
5205 }
5206
5207 pub fn length_m(&self) -> f32 {
5208 (self.end_chainage - self.start_chainage).abs()
5209 }
5210
5211 pub fn conflicts_with(&self, other: &UtilityRecord) -> bool {
5212 let horizontal_sep = (self.horizontal_offset_m - other.horizontal_offset_m).abs();
5213 let vertical_sep = (self.depth_m - other.depth_m).abs();
5214 horizontal_sep < 0.5 && vertical_sep < 0.3
5215 }
5216}
5217
5218#[derive(Debug, Clone)]
5219pub struct UtilityCorridorManager {
5220 pub road_id: u32,
5221 pub utilities: Vec<UtilityRecord>,
5222}
5223
5224impl UtilityCorridorManager {
5225 pub fn new(road_id: u32) -> Self {
5226 UtilityCorridorManager { road_id, utilities: Vec::new() }
5227 }
5228
5229 pub fn add_utility(&mut self, utility: UtilityRecord) {
5230 self.utilities.push(utility);
5231 }
5232
5233 pub fn find_conflicts(&self) -> Vec<(usize, usize)> {
5234 let mut conflicts = Vec::new();
5235 for i in 0..self.utilities.len() {
5236 for j in (i + 1)..self.utilities.len() {
5237 if self.utilities[i].conflicts_with(&self.utilities[j]) {
5238 conflicts.push((i, j));
5239 }
5240 }
5241 }
5242 conflicts
5243 }
5244
5245 pub fn utilities_of_type(&self, ut: &UtilityType) -> Vec<&UtilityRecord> {
5246 self.utilities.iter().filter(|u| &u.utility_type == ut).collect()
5247 }
5248}
5249
5250#[derive(Debug, Clone)]
5255pub struct SafetyRatingFactor {
5256 pub factor_name: String,
5257 pub score: f32,
5258 pub max_score: f32,
5259 pub weight: f32,
5260}
5261
5262impl SafetyRatingFactor {
5263 pub fn weighted_score(&self) -> f32 {
5264 (self.score / self.max_score.max(0.001)) * self.weight
5265 }
5266}
5267
5268#[derive(Debug, Clone)]
5269pub struct RoadSafetyRating {
5270 pub section_id: u32,
5271 pub factors: Vec<SafetyRatingFactor>,
5272 pub star_rating: u32,
5273 pub total_score: f32,
5274}
5275
5276impl RoadSafetyRating {
5277 pub fn new(section_id: u32) -> Self {
5278 RoadSafetyRating { section_id, factors: Vec::new(), star_rating: 0, total_score: 0.0 }
5279 }
5280
5281 pub fn add_factor(&mut self, factor: SafetyRatingFactor) {
5282 self.factors.push(factor);
5283 }
5284
5285 pub fn compute_rating(&mut self) {
5286 let total_weight: f32 = self.factors.iter().map(|f| f.weight).sum();
5287 let weighted_sum: f32 = self.factors.iter().map(|f| f.weighted_score()).sum();
5288 self.total_score = if total_weight > 0.0 { weighted_sum / total_weight * 100.0 } else { 0.0 };
5289 self.star_rating = if self.total_score >= 80.0 { 5 }
5290 else if self.total_score >= 65.0 { 4 }
5291 else if self.total_score >= 50.0 { 3 }
5292 else if self.total_score >= 35.0 { 2 }
5293 else { 1 };
5294 }
5295}
5296
5297#[derive(Debug, Clone, PartialEq)]
5302pub enum LoS {
5303 A, B, C, D, E, F,
5304}
5305
5306impl LoS {
5307 pub fn from_density(density_veh_km_lane: f32) -> Self {
5308 if density_veh_km_lane <= 7.0 { LoS::A }
5309 else if density_veh_km_lane <= 11.0 { LoS::B }
5310 else if density_veh_km_lane <= 16.0 { LoS::C }
5311 else if density_veh_km_lane <= 22.0 { LoS::D }
5312 else if density_veh_km_lane <= 28.0 { LoS::E }
5313 else { LoS::F }
5314 }
5315
5316 pub fn from_vc_ratio(vc: f32) -> Self {
5317 if vc <= 0.35 { LoS::A }
5318 else if vc <= 0.54 { LoS::B }
5319 else if vc <= 0.77 { LoS::C }
5320 else if vc <= 0.93 { LoS::D }
5321 else if vc <= 1.0 { LoS::E }
5322 else { LoS::F }
5323 }
5324
5325 pub fn acceptable(&self) -> bool {
5326 matches!(self, LoS::A | LoS::B | LoS::C)
5327 }
5328}
5329
5330#[derive(Debug, Clone)]
5331pub struct FreewaySegmentAnalysis {
5332 pub segment_id: u32,
5333 pub length_km: f32,
5334 pub lane_count: u32,
5335 pub free_flow_speed_kph: f32,
5336 pub peak_hour_volume: u32,
5337 pub peak_hour_factor: f32,
5338 pub truck_pct: f32,
5339 pub terrain_type: String,
5340}
5341
5342impl FreewaySegmentAnalysis {
5343 pub fn new(segment_id: u32, lanes: u32, ffs: f32, volume: u32) -> Self {
5344 FreewaySegmentAnalysis {
5345 segment_id, length_km: 1.0, lane_count: lanes,
5346 free_flow_speed_kph: ffs, peak_hour_volume: volume,
5347 peak_hour_factor: 0.92, truck_pct: 10.0,
5348 terrain_type: "Level".to_string(),
5349 }
5350 }
5351
5352 pub fn et_factor(&self) -> f32 {
5353 match self.terrain_type.as_str() {
5354 "Level" => 1.5,
5355 "Rolling" => 2.5,
5356 "Mountainous" => 4.5,
5357 _ => 2.0,
5358 }
5359 }
5360
5361 pub fn pce_flow_rate(&self) -> f32 {
5362 let et = self.et_factor();
5363 let ft = 1.0 / (1.0 + self.truck_pct / 100.0 * (et - 1.0));
5364 let demand = self.peak_hour_volume as f32 / self.peak_hour_factor;
5365 demand * (1.0 / ft)
5366 }
5367
5368 pub fn flow_per_lane(&self) -> f32 {
5369 self.pce_flow_rate() / self.lane_count.max(1) as f32
5370 }
5371
5372 pub fn speed_flow_model(&self) -> f32 {
5373 let bp = 1400.0;
5374 let cap = 2200.0;
5375 let q = self.flow_per_lane();
5376 if q <= bp {
5377 self.free_flow_speed_kph
5378 } else {
5379 let t1 = (q - bp) / (cap - bp);
5380 self.free_flow_speed_kph - (self.free_flow_speed_kph - 53.0) * t1
5381 }
5382 }
5383
5384 pub fn density_veh_km_lane(&self) -> f32 {
5385 let speed = self.speed_flow_model();
5386 if speed <= 0.0 { return f32::INFINITY; }
5387 self.flow_per_lane() / speed
5388 }
5389
5390 pub fn level_of_service(&self) -> LoS {
5391 LoS::from_density(self.density_veh_km_lane())
5392 }
5393}
5394
5395#[derive(Debug, Clone)]
5400pub struct TripGeneration {
5401 pub land_use_code: String,
5402 pub land_use_area: f32,
5403 pub rate_am_peak_in: f32,
5404 pub rate_am_peak_out: f32,
5405 pub rate_pm_peak_in: f32,
5406 pub rate_pm_peak_out: f32,
5407 pub rate_daily: f32,
5408}
5409
5410impl TripGeneration {
5411 pub fn am_peak_trips(&self) -> (f32, f32) {
5412 (self.land_use_area * self.rate_am_peak_in, self.land_use_area * self.rate_am_peak_out)
5413 }
5414
5415 pub fn pm_peak_trips(&self) -> (f32, f32) {
5416 (self.land_use_area * self.rate_pm_peak_in, self.land_use_area * self.rate_pm_peak_out)
5417 }
5418
5419 pub fn daily_trips(&self) -> f32 {
5420 self.land_use_area * self.rate_daily
5421 }
5422}
5423
5424#[derive(Debug, Clone)]
5425pub struct TiaIntersection {
5426 pub intersection_id: u32,
5427 pub name: String,
5428 pub existing_vc: f32,
5429 pub background_growth_rate: f32,
5430 pub project_added_volume: u32,
5431 pub capacity: u32,
5432}
5433
5434impl TiaIntersection {
5435 pub fn with_project_vc(&self) -> f32 {
5436 let existing_vol = self.existing_vc * self.capacity as f32;
5437 let background = existing_vol * self.background_growth_rate;
5438 (existing_vol + background + self.project_added_volume as f32) / self.capacity as f32
5439 }
5440
5441 pub fn los_without_project(&self) -> LoS {
5442 LoS::from_vc_ratio(self.existing_vc)
5443 }
5444
5445 pub fn los_with_project(&self) -> LoS {
5446 LoS::from_vc_ratio(self.with_project_vc())
5447 }
5448
5449 pub fn significant_impact(&self) -> bool {
5450 let with_vc = self.with_project_vc();
5451 with_vc > self.existing_vc + 0.05 && with_vc > 0.85
5452 }
5453}
5454
5455#[derive(Debug, Clone)]
5456pub struct TrafficImpactAssessment {
5457 pub project_name: String,
5458 pub trip_gen: Vec<TripGeneration>,
5459 pub intersections: Vec<TiaIntersection>,
5460 pub mitigation_required: bool,
5461 pub mitigation_measures: Vec<String>,
5462}
5463
5464impl TrafficImpactAssessment {
5465 pub fn new(project_name: &str) -> Self {
5466 TrafficImpactAssessment {
5467 project_name: project_name.to_string(),
5468 trip_gen: Vec::new(),
5469 intersections: Vec::new(),
5470 mitigation_required: false,
5471 mitigation_measures: Vec::new(),
5472 }
5473 }
5474
5475 pub fn total_pm_peak_trips(&self) -> f32 {
5476 self.trip_gen.iter().map(|tg| { let (i, o) = tg.pm_peak_trips(); i + o }).sum()
5477 }
5478
5479 pub fn impacted_intersections(&self) -> Vec<&TiaIntersection> {
5480 self.intersections.iter().filter(|i| i.significant_impact()).collect()
5481 }
5482
5483 pub fn assess(&mut self) {
5484 self.mitigation_required = !self.impacted_intersections().is_empty();
5485 if self.mitigation_required {
5486 self.mitigation_measures.push("Signal timing optimization".to_string());
5487 self.mitigation_measures.push("Turn lane addition".to_string());
5488 }
5489 }
5490}
5491
5492#[cfg(test)]
5497mod tests_terrain_road_extra {
5498 use super::*;
5499
5500 #[test]
5501 fn test_asset_condition_grade() {
5502 assert!(matches!(AssetConditionGrade::from_score(90.0), AssetConditionGrade::VeryGood));
5503 assert!(matches!(AssetConditionGrade::from_score(45.0), AssetConditionGrade::Poor));
5504 assert!(matches!(AssetConditionGrade::from_score(10.0), AssetConditionGrade::Failed));
5505 }
5506
5507 #[test]
5508 fn test_asset_management_funded() {
5509 let mut plan = AssetManagementPlan::new("AMP2024", 2024, 100_000.0);
5510 let mut asset = InfrastructureAsset::new("SWD-001", "Culvert", 1, 500.0);
5511 asset.condition_score = 30.0;
5512 asset.replacement_cost = 50_000.0;
5513 plan.add_asset(asset);
5514 let funded = plan.funded_assets();
5515 assert_eq!(funded.len(), 1);
5516 }
5517
5518 #[test]
5519 fn test_snow_route_salt() {
5520 let mut route = SnowRoute::new(1, SnowRemovalPriority::P1_Emergency);
5521 route.add_segment(1, 10.0);
5522 let salt = route.total_salt_kg();
5523 assert!(salt > 0.0);
5524 }
5525
5526 #[test]
5527 fn test_utility_conflict_detection() {
5528 let mut mgr = UtilityCorridorManager::new(1);
5529 let mut u1 = UtilityRecord::new("PWR-001", UtilityType::PowerLine, "ElecCo");
5530 u1.horizontal_offset_m = 2.0;
5531 u1.depth_m = 0.8;
5532 let mut u2 = UtilityRecord::new("WAT-001", UtilityType::WaterMain, "WaterCo");
5533 u2.horizontal_offset_m = 2.2;
5534 u2.depth_m = 0.9;
5535 mgr.add_utility(u1);
5536 mgr.add_utility(u2);
5537 let conflicts = mgr.find_conflicts();
5538 assert_eq!(conflicts.len(), 1);
5539 }
5540
5541 #[test]
5542 fn test_los_from_vc() {
5543 assert!(LoS::from_vc_ratio(0.3).acceptable());
5544 assert!(!LoS::from_vc_ratio(1.1).acceptable());
5545 }
5546
5547 #[test]
5548 fn test_freeway_segment_los() {
5549 let seg = FreewaySegmentAnalysis::new(1, 3, 110.0, 3000);
5550 let los = seg.level_of_service();
5551 assert!(matches!(los, LoS::A | LoS::B | LoS::C));
5552 }
5553
5554 #[test]
5555 fn test_tia_significant_impact() {
5556 let mut ti = TiaIntersection {
5557 intersection_id: 1, name: "Main/Oak".to_string(),
5558 existing_vc: 0.88, background_growth_rate: 0.02,
5559 project_added_volume: 200, capacity: 1000,
5560 };
5561 assert!(ti.significant_impact());
5562 }
5563
5564 #[test]
5565 fn test_safety_rating() {
5566 let mut rating = RoadSafetyRating::new(1);
5567 rating.add_factor(SafetyRatingFactor { factor_name: "Alignment".to_string(), score: 80.0, max_score: 100.0, weight: 1.0 });
5568 rating.add_factor(SafetyRatingFactor { factor_name: "Markings".to_string(), score: 70.0, max_score: 100.0, weight: 0.5 });
5569 rating.compute_rating();
5570 assert!(rating.star_rating >= 3);
5571 }
5572}
5573
5574pub const ROAD_ASSET_LIFE_CULVERT: u32 = 50;
5575pub const ROAD_ASSET_LIFE_SIGN: u32 = 15;
5576pub const ROAD_ASSET_LIFE_GUARDRAIL: u32 = 20;
5577pub const ROAD_ASSET_LIFE_PAVEMENT: u32 = 20;
5578pub const ROAD_ASSET_LIFE_BRIDGE: u32 = 100;
5579pub const ROAD_ASSET_LIFE_SIGNAL: u32 = 20;
5580pub const ROAD_ASSET_LIFE_LIGHTING: u32 = 25;
5581
5582
5583pub const CLOTHOID_SCALE: f32 = 100.0;
5588
5589#[derive(Debug, Clone)]
5590pub struct ClothoidSpiral {
5591 pub parameter_a: f32,
5592 pub length: f32,
5593 pub start_radius: f32,
5594 pub end_radius: f32,
5595 pub direction: i32,
5596}
5597
5598impl ClothoidSpiral {
5599 pub fn new(start_r: f32, end_r: f32, a: f32) -> Self {
5600 let l = a * a * (1.0 / end_r - 1.0 / start_r).abs();
5601 ClothoidSpiral {
5602 parameter_a: a, length: l,
5603 start_radius: start_r, end_radius: end_r,
5604 direction: 1,
5605 }
5606 }
5607
5608 pub fn radius_at(&self, s: f32) -> f32 {
5609 if s <= 0.0 { return self.start_radius; }
5610 let r_inv_start = if self.start_radius.is_infinite() { 0.0 } else { 1.0 / self.start_radius };
5611 let r_inv_end = 1.0 / self.end_radius;
5612 let t = s / self.length.max(0.001);
5613 let r_inv = r_inv_start + (r_inv_end - r_inv_start) * t;
5614 if r_inv.abs() < 1e-10 { f32::INFINITY } else { 1.0 / r_inv.abs() }
5615 }
5616
5617 pub fn deflection_angle_rad(&self) -> f32 {
5618 self.length / (2.0 * self.end_radius)
5619 }
5620
5621 pub fn minimum_a_for_speed(&self, design_speed_kph: f32) -> f32 {
5622 design_speed_kph * 0.036 * design_speed_kph.sqrt()
5623 }
5624}
5625
5626#[derive(Debug, Clone)]
5631pub struct SignStandardsCheck {
5632 pub sign_id: u32,
5633 pub location_chainage: f32,
5634 pub sign_type: String,
5635 pub advance_warning_distance_m: f32,
5636 pub required_advance_distance_m: f32,
5637 pub height_above_pavement_m: f32,
5638 pub required_min_height_m: f32,
5639 pub retroreflective: bool,
5640 pub compliant: bool,
5641 pub issues: Vec<String>,
5642}
5643
5644impl SignStandardsCheck {
5645 pub fn new(sign_id: u32, chainage: f32, sign_type: &str) -> Self {
5646 SignStandardsCheck {
5647 sign_id, location_chainage: chainage, sign_type: sign_type.to_string(),
5648 advance_warning_distance_m: 0.0, required_advance_distance_m: 150.0,
5649 height_above_pavement_m: 2.1, required_min_height_m: 2.1,
5650 retroreflective: true, compliant: true, issues: Vec::new(),
5651 }
5652 }
5653
5654 pub fn evaluate(&mut self) {
5655 self.issues.clear();
5656 if self.advance_warning_distance_m < self.required_advance_distance_m {
5657 self.issues.push(format!("Insufficient advance warning: {:.0}m < {:.0}m", self.advance_warning_distance_m, self.required_advance_distance_m));
5658 }
5659 if self.height_above_pavement_m < self.required_min_height_m {
5660 self.issues.push(format!("Sign height {:.1}m below minimum {:.1}m", self.height_above_pavement_m, self.required_min_height_m));
5661 }
5662 if !self.retroreflective {
5663 self.issues.push("Sign lacks retroreflective sheeting".to_string());
5664 }
5665 self.compliant = self.issues.is_empty();
5666 }
5667}
5668
5669#[derive(Debug, Clone)]
5674pub struct PI_Point {
5675 pub chainage: f32,
5676 pub easting: f32,
5677 pub northing: f32,
5678 pub deflection_angle_deg: f32,
5679 pub radius: f32,
5680 pub spiral_in_length: f32,
5681 pub spiral_out_length: f32,
5682}
5683
5684impl PI_Point {
5685 pub fn new(chainage: f32, e: f32, n: f32) -> Self {
5686 PI_Point { chainage, easting: e, northing: n, deflection_angle_deg: 0.0, radius: 0.0, spiral_in_length: 0.0, spiral_out_length: 0.0 }
5687 }
5688
5689 pub fn tangent_length(&self) -> f32 {
5690 if self.radius <= 0.0 { return 0.0; }
5691 let delta_rad = self.deflection_angle_deg.to_radians();
5692 let t_simple = self.radius * (delta_rad / 2.0).tan();
5693 let t_spiral = self.spiral_in_length / 2.0;
5694 t_simple + t_spiral
5695 }
5696
5697 pub fn curve_length(&self) -> f32 {
5698 if self.radius <= 0.0 { return 0.0; }
5699 let delta_rad = self.deflection_angle_deg.to_radians();
5700 self.radius * delta_rad + self.spiral_in_length + self.spiral_out_length
5701 }
5702
5703 pub fn long_chord(&self) -> f32 {
5704 if self.radius <= 0.0 { return 0.0; }
5705 let delta_rad = self.deflection_angle_deg.to_radians();
5706 2.0 * self.radius * (delta_rad / 2.0).sin()
5707 }
5708
5709 pub fn external_distance(&self) -> f32 {
5710 if self.radius <= 0.0 { return 0.0; }
5711 let delta_rad = self.deflection_angle_deg.to_radians();
5712 self.radius * ((delta_rad / 2.0).cos().recip() - 1.0)
5713 }
5714
5715 pub fn mid_ordinate(&self) -> f32 {
5716 if self.radius <= 0.0 { return 0.0; }
5717 let delta_rad = self.deflection_angle_deg.to_radians();
5718 self.radius * (1.0 - (delta_rad / 2.0).cos())
5719 }
5720}
5721
5722#[derive(Debug, Clone)]
5727pub struct VPI_Point {
5728 pub chainage: f32,
5729 pub elevation: f32,
5730 pub grade_in_pct: f32,
5731 pub grade_out_pct: f32,
5732 pub k_value: f32,
5733}
5734
5735impl VPI_Point {
5736 pub fn new(chainage: f32, elevation: f32) -> Self {
5737 VPI_Point { chainage, elevation, grade_in_pct: 0.0, grade_out_pct: 0.0, k_value: 30.0 }
5738 }
5739
5740 pub fn grade_change_pct(&self) -> f32 {
5741 self.grade_out_pct - self.grade_in_pct
5742 }
5743
5744 pub fn vertical_curve_length(&self) -> f32 {
5745 self.k_value * self.grade_change_pct().abs()
5746 }
5747
5748 pub fn is_crest(&self) -> bool {
5749 self.grade_change_pct() < 0.0
5750 }
5751
5752 pub fn is_sag(&self) -> bool {
5753 self.grade_change_pct() > 0.0
5754 }
5755
5756 pub fn elevation_at_chainage(&self, ch: f32) -> f32 {
5757 let l = self.vertical_curve_length();
5758 let bvc_ch = self.chainage - l / 2.0;
5759 let x = ch - bvc_ch;
5760 if x < 0.0 || x > l { return self.elevation; }
5761 let bvc_elev = self.elevation - (l / 2.0) * self.grade_in_pct / 100.0;
5762 let r = (self.grade_out_pct - self.grade_in_pct) / (l * 100.0);
5763 bvc_elev + (self.grade_in_pct / 100.0) * x + 0.5 * (r / 100.0) * x * x
5764 }
5765}
5766
5767#[derive(Debug, Clone)]
5772pub struct CostItem {
5773 pub item_code: String,
5774 pub description: String,
5775 pub unit: String,
5776 pub quantity: f64,
5777 pub unit_rate: f64,
5778 pub contingency_pct: f32,
5779}
5780
5781impl CostItem {
5782 pub fn base_cost(&self) -> f64 {
5783 self.quantity * self.unit_rate
5784 }
5785
5786 pub fn with_contingency(&self) -> f64 {
5787 self.base_cost() * (1.0 + self.contingency_pct as f64 / 100.0)
5788 }
5789}
5790
5791#[derive(Debug, Clone)]
5792pub struct CostEstimate {
5793 pub project_name: String,
5794 pub estimate_date: String,
5795 pub items: Vec<CostItem>,
5796 pub overhead_pct: f32,
5797 pub profit_pct: f32,
5798 pub gst_pct: f32,
5799 pub design_fee_pct: f32,
5800 pub supervision_fee_pct: f32,
5801}
5802
5803impl CostEstimate {
5804 pub fn new(project_name: &str) -> Self {
5805 CostEstimate {
5806 project_name: project_name.to_string(), estimate_date: String::new(),
5807 items: Vec::new(), overhead_pct: 12.0, profit_pct: 8.0,
5808 gst_pct: 10.0, design_fee_pct: 5.0, supervision_fee_pct: 3.0,
5809 }
5810 }
5811
5812 pub fn add_item(&mut self, item: CostItem) {
5813 self.items.push(item);
5814 }
5815
5816 pub fn direct_cost(&self) -> f64 {
5817 self.items.iter().map(|i| i.with_contingency()).sum()
5818 }
5819
5820 pub fn overhead_cost(&self) -> f64 {
5821 self.direct_cost() * self.overhead_pct as f64 / 100.0
5822 }
5823
5824 pub fn profit(&self) -> f64 {
5825 (self.direct_cost() + self.overhead_cost()) * self.profit_pct as f64 / 100.0
5826 }
5827
5828 pub fn construction_cost(&self) -> f64 {
5829 self.direct_cost() + self.overhead_cost() + self.profit()
5830 }
5831
5832 pub fn total_project_cost(&self) -> f64 {
5833 let cc = self.construction_cost();
5834 let design = cc * self.design_fee_pct as f64 / 100.0;
5835 let supervision = cc * self.supervision_fee_pct as f64 / 100.0;
5836 let gst = (cc + design + supervision) * self.gst_pct as f64 / 100.0;
5837 cc + design + supervision + gst
5838 }
5839
5840 pub fn cost_per_lane_km(&self, lane_km: f32) -> f64 {
5841 if lane_km <= 0.0 { return 0.0; }
5842 self.construction_cost() / lane_km as f64
5843 }
5844}
5845
5846#[cfg(test)]
5851mod tests_terrain_final {
5852 use super::*;
5853
5854 #[test]
5855 fn test_clothoid_radius() {
5856 let spiral = ClothoidSpiral::new(f32::INFINITY, 300.0, 100.0);
5857 let r_at_end = spiral.radius_at(spiral.length);
5858 assert!((r_at_end - 300.0).abs() < 5.0);
5859 }
5860
5861 #[test]
5862 fn test_pi_point_tangent() {
5863 let mut pi = PI_Point::new(1000.0, 5000.0, 6000.0);
5864 pi.deflection_angle_deg = 30.0;
5865 pi.radius = 500.0;
5866 let tl = pi.tangent_length();
5867 assert!(tl > 0.0);
5868 }
5869
5870 #[test]
5871 fn test_vpi_elevation() {
5872 let mut vpi = VPI_Point::new(1000.0, 10.0);
5873 vpi.grade_in_pct = 3.0;
5874 vpi.grade_out_pct = -2.0;
5875 vpi.k_value = 20.0;
5876 let cl = vpi.vertical_curve_length();
5877 assert!((cl - 100.0).abs() < 0.001);
5878 assert!(vpi.is_crest());
5879 }
5880
5881 #[test]
5882 fn test_cost_estimate() {
5883 let mut est = CostEstimate::new("Test Road");
5884 est.add_item(CostItem {
5885 item_code: "1001".to_string(), description: "Earthworks".to_string(),
5886 unit: "m3".to_string(), quantity: 10000.0, unit_rate: 25.0, contingency_pct: 10.0,
5887 });
5888 assert!(est.total_project_cost() > est.direct_cost());
5889 }
5890
5891 #[test]
5892 fn test_sign_standards_check() {
5893 let mut check = SignStandardsCheck::new(1, 500.0, "Speed Zone");
5894 check.advance_warning_distance_m = 200.0;
5895 check.retroreflective = true;
5896 check.evaluate();
5897 assert!(check.compliant);
5898 check.advance_warning_distance_m = 50.0;
5899 check.evaluate();
5900 assert!(!check.compliant);
5901 }
5902}
5903
5904pub const TERRAIN_ROAD_BUILD_VERSION: u32 = 230;
5905pub const TERRAIN_ROAD_FEATURE_COUNT: u32 = 47;
5906
5907
5908#[derive(Debug, Clone, PartialEq)]
5913pub enum BridgeComponentType {
5914 Deck, Superstructure, Substructure, Culvert, Channel, Approach,
5915}
5916
5917#[derive(Debug, Clone)]
5918pub struct BridgeComponentRating {
5919 pub component: BridgeComponentType,
5920 pub inspection_rating: u32,
5921 pub notes: String,
5922 pub requires_action: bool,
5923}
5924
5925impl BridgeComponentRating {
5926 pub fn new(component: BridgeComponentType, rating: u32) -> Self {
5927 BridgeComponentRating {
5928 component, inspection_rating: rating,
5929 notes: String::new(),
5930 requires_action: rating <= 4,
5931 }
5932 }
5933
5934 pub fn condition_description(&self) -> &'static str {
5935 match self.inspection_rating {
5936 9 | 10 => "Excellent",
5937 7 | 8 => "Good",
5938 5 | 6 => "Fair",
5939 4 => "Poor",
5940 3 => "Serious",
5941 2 => "Critical",
5942 1 => "Imminent Failure",
5943 _ => "Failed",
5944 }
5945 }
5946}
5947
5948#[derive(Debug, Clone)]
5949pub struct BridgeInspectionReport {
5950 pub bridge_id: u32,
5951 pub inspection_date: String,
5952 pub inspector_name: String,
5953 pub component_ratings: Vec<BridgeComponentRating>,
5954 pub overall_sufficiency_rating: f32,
5955 pub load_rating_tonne: f32,
5956 pub posted_load_limit_tonne: Option<f32>,
5957 pub recommendations: Vec<String>,
5958 pub next_inspection_due: String,
5959}
5960
5961impl BridgeInspectionReport {
5962 pub fn new(bridge_id: u32) -> Self {
5963 BridgeInspectionReport {
5964 bridge_id, inspection_date: String::new(), inspector_name: String::new(),
5965 component_ratings: Vec::new(),
5966 overall_sufficiency_rating: 0.0,
5967 load_rating_tonne: 44.0, posted_load_limit_tonne: None,
5968 recommendations: Vec::new(), next_inspection_due: String::new(),
5969 }
5970 }
5971
5972 pub fn add_component(&mut self, rating: BridgeComponentRating) {
5973 self.component_ratings.push(rating);
5974 }
5975
5976 pub fn minimum_rating(&self) -> u32 {
5977 self.component_ratings.iter().map(|c| c.inspection_rating).min().unwrap_or(9)
5978 }
5979
5980 pub fn critical_components(&self) -> Vec<&BridgeComponentRating> {
5981 self.component_ratings.iter().filter(|c| c.inspection_rating <= 3).collect()
5982 }
5983
5984 pub fn requires_load_posting(&self) -> bool {
5985 self.minimum_rating() <= 4
5986 }
5987
5988 pub fn compute_sufficiency_rating(&mut self) {
5989 let avg = self.component_ratings.iter().map(|c| c.inspection_rating as f32).sum::<f32>()
5990 / self.component_ratings.len().max(1) as f32;
5991 self.overall_sufficiency_rating = (avg / 9.0 * 100.0).clamp(0.0, 100.0);
5992 }
5993}
5994
5995#[derive(Debug, Clone, PartialEq)]
6000pub enum SoilType {
6001 Rock, GravelSand, SandySilt, ClayLow, ClayHigh, Organic, Fill,
6002}
6003
6004impl SoilType {
6005 pub fn bearing_capacity_kpa(&self) -> f32 {
6006 match self {
6007 SoilType::Rock => 5000.0,
6008 SoilType::GravelSand => 400.0,
6009 SoilType::SandySilt => 150.0,
6010 SoilType::ClayLow => 75.0,
6011 SoilType::ClayHigh => 40.0,
6012 SoilType::Organic => 25.0,
6013 SoilType::Fill => 100.0,
6014 }
6015 }
6016
6017 pub fn california_bearing_ratio(&self) -> f32 {
6018 match self {
6019 SoilType::Rock => 100.0,
6020 SoilType::GravelSand => 80.0,
6021 SoilType::SandySilt => 20.0,
6022 SoilType::ClayLow => 8.0,
6023 SoilType::ClayHigh => 3.0,
6024 SoilType::Organic => 2.0,
6025 SoilType::Fill => 15.0,
6026 }
6027 }
6028}
6029
6030#[derive(Debug, Clone)]
6031pub struct BoreholeLayer {
6032 pub depth_from_m: f32,
6033 pub depth_to_m: f32,
6034 pub soil_type: SoilType,
6035 pub spt_n_value: Option<u32>,
6036 pub moisture_content_pct: f32,
6037 pub description: String,
6038}
6039
6040#[derive(Debug, Clone)]
6041pub struct BoreholeLog {
6042 pub borehole_id: String,
6043 pub location: Vec2,
6044 pub total_depth_m: f32,
6045 pub water_table_depth_m: Option<f32>,
6046 pub layers: Vec<BoreholeLayer>,
6047 pub date_drilled: String,
6048}
6049
6050impl BoreholeLog {
6051 pub fn new(id: &str, location: Vec2) -> Self {
6052 BoreholeLog {
6053 borehole_id: id.to_string(), location,
6054 total_depth_m: 0.0, water_table_depth_m: None,
6055 layers: Vec::new(), date_drilled: String::new(),
6056 }
6057 }
6058
6059 pub fn add_layer(&mut self, layer: BoreholeLayer) {
6060 if layer.depth_to_m > self.total_depth_m { self.total_depth_m = layer.depth_to_m; }
6061 self.layers.push(layer);
6062 }
6063
6064 pub fn soil_at_depth(&self, depth_m: f32) -> Option<&BoreholeLayer> {
6065 self.layers.iter().find(|l| depth_m >= l.depth_from_m && depth_m <= l.depth_to_m)
6066 }
6067
6068 pub fn has_groundwater(&self) -> bool {
6069 self.water_table_depth_m.is_some()
6070 }
6071
6072 pub fn min_cbr(&self) -> f32 {
6073 self.layers.iter().map(|l| l.soil_type.california_bearing_ratio()).fold(f32::INFINITY, f32::min)
6074 }
6075}
6076
6077pub const SUBGRADE_CBR_MIN: f32 = 2.0;
6082pub const PAVEMENT_POISSON_AC: f32 = 0.35;
6083pub const PAVEMENT_POISSON_BASE: f32 = 0.40;
6084
6085#[derive(Debug, Clone)]
6086pub struct PavementMaterialProps {
6087 pub material_name: String,
6088 pub elastic_modulus_mpa: f32,
6089 pub poissons_ratio: f32,
6090 pub layer_thickness_mm: f32,
6091 pub unit_cost_per_m2: f64,
6092}
6093
6094impl PavementMaterialProps {
6095 pub fn dense_graded_ac() -> Self {
6096 PavementMaterialProps {
6097 material_name: "Dense Graded AC".to_string(),
6098 elastic_modulus_mpa: 3000.0, poissons_ratio: PAVEMENT_POISSON_AC,
6099 layer_thickness_mm: 50.0, unit_cost_per_m2: 35.0,
6100 }
6101 }
6102
6103 pub fn crushed_rock_base() -> Self {
6104 PavementMaterialProps {
6105 material_name: "Crushed Rock Base".to_string(),
6106 elastic_modulus_mpa: 300.0, poissons_ratio: PAVEMENT_POISSON_BASE,
6107 layer_thickness_mm: 200.0, unit_cost_per_m2: 20.0,
6108 }
6109 }
6110
6111 pub fn subbase_cbr20() -> Self {
6112 PavementMaterialProps {
6113 material_name: "Granular Subbase CBR20".to_string(),
6114 elastic_modulus_mpa: 150.0, poissons_ratio: 0.40,
6115 layer_thickness_mm: 150.0, unit_cost_per_m2: 12.0,
6116 }
6117 }
6118}
6119
6120#[derive(Debug, Clone)]
6121pub struct PavementDesign {
6122 pub design_id: String,
6123 pub road_category: String,
6124 pub design_esal: f64,
6125 pub subgrade_cbr: f32,
6126 pub layers: Vec<PavementMaterialProps>,
6127 pub design_life_years: u32,
6128 pub reliability_pct: f32,
6129}
6130
6131impl PavementDesign {
6132 pub fn new(design_id: &str, esal: f64, subgrade_cbr: f32) -> Self {
6133 PavementDesign {
6134 design_id: design_id.to_string(), road_category: "Collector".to_string(),
6135 design_esal: esal, subgrade_cbr, layers: Vec::new(),
6136 design_life_years: 20, reliability_pct: 95.0,
6137 }
6138 }
6139
6140 pub fn add_layer(&mut self, layer: PavementMaterialProps) {
6141 self.layers.push(layer);
6142 }
6143
6144 pub fn total_pavement_thickness_mm(&self) -> f32 {
6145 self.layers.iter().map(|l| l.layer_thickness_mm).sum()
6146 }
6147
6148 pub fn total_material_cost_per_m2(&self) -> f64 {
6149 self.layers.iter().map(|l| l.unit_cost_per_m2).sum()
6150 }
6151
6152 pub fn structural_number(&self) -> f32 {
6153 let layer_coefs = [0.44_f32, 0.14, 0.11];
6155 self.layers.iter().enumerate().map(|(i, l)| {
6156 let a = layer_coefs.get(i).copied().unwrap_or(0.10);
6157 a * l.layer_thickness_mm / 25.4
6158 }).sum()
6159 }
6160}
6161
6162pub const ROAD_DESIGN_GRAVITY: f32 = 9.81;
6167pub const ROAD_DESIGN_AIR_DENSITY: f32 = 1.225;
6168pub const ROAD_DESIGN_WATER_DENSITY: f32 = 1000.0;
6169pub const ROAD_DESIGN_CONCRETE_DENSITY: f32 = 2400.0;
6170pub const ROAD_DESIGN_ASPHALT_DENSITY: f32 = 2350.0;
6171pub const ROAD_DESIGN_STEEL_DENSITY: f32 = 7850.0;
6172
6173pub fn friction_force_n(normal_n: f32, friction_coeff: f32) -> f32 {
6174 normal_n * friction_coeff
6175}
6176
6177pub fn braking_distance_m(speed_kph: f32, deceleration_ms2: f32) -> f32 {
6178 let v = speed_kph / 3.6;
6179 v * v / (2.0 * deceleration_ms2)
6180}
6181
6182pub fn headway_to_flow_vphpl(headway_s: f32) -> f32 {
6183 if headway_s <= 0.0 { return 0.0; }
6184 3600.0 / headway_s
6185}
6186
6187pub fn flow_to_headway_s(flow_vphpl: f32) -> f32 {
6188 if flow_vphpl <= 0.0 { return f32::INFINITY; }
6189 3600.0 / flow_vphpl
6190}
6191
6192pub fn rolling_resistance_force_n(vehicle_mass_kg: f32, crr: f32) -> f32 {
6193 vehicle_mass_kg * ROAD_DESIGN_GRAVITY * crr
6194}
6195
6196pub fn grade_resistance_n(vehicle_mass_kg: f32, grade_pct: f32) -> f32 {
6197 vehicle_mass_kg * ROAD_DESIGN_GRAVITY * grade_pct / 100.0
6198}
6199
6200pub fn stopping_distance_on_grade_m(speed_kph: f32, grade_pct: f32, friction: f32) -> f32 {
6201 let v = speed_kph / 3.6;
6202 let effective_friction = friction - grade_pct / 100.0;
6203 if effective_friction <= 0.0 { return f32::INFINITY; }
6204 v * v / (2.0 * ROAD_DESIGN_GRAVITY * effective_friction)
6205}
6206
6207pub fn traffic_density_veh_km(flow_vph: f32, speed_kph: f32) -> f32 {
6208 if speed_kph <= 0.0 { return 0.0; }
6209 flow_vph / speed_kph
6210}
6211
6212pub fn travel_time_index(actual_speed_kph: f32, freeflow_speed_kph: f32) -> f32 {
6213 if actual_speed_kph <= 0.0 { return f32::INFINITY; }
6214 freeflow_speed_kph / actual_speed_kph
6215}
6216
6217#[cfg(test)]
6222mod tests_terrain_road_final2 {
6223 use super::*;
6224
6225 #[test]
6226 fn test_bridge_inspection() {
6227 let mut report = BridgeInspectionReport::new(1);
6228 report.add_component(BridgeComponentRating::new(BridgeComponentType::Deck, 6));
6229 report.add_component(BridgeComponentRating::new(BridgeComponentType::Superstructure, 7));
6230 report.add_component(BridgeComponentRating::new(BridgeComponentType::Substructure, 5));
6231 report.compute_sufficiency_rating();
6232 assert!(report.overall_sufficiency_rating > 0.0);
6233 assert!(!report.requires_load_posting());
6234 }
6235
6236 #[test]
6237 fn test_borehole_soil_at_depth() {
6238 let mut bh = BoreholeLog::new("BH-001", Vec2::ZERO);
6239 bh.add_layer(BoreholeLayer { depth_from_m: 0.0, depth_to_m: 2.0, soil_type: SoilType::Fill, spt_n_value: Some(10), moisture_content_pct: 15.0, description: String::new() });
6240 bh.add_layer(BoreholeLayer { depth_from_m: 2.0, depth_to_m: 8.0, soil_type: SoilType::ClayLow, spt_n_value: Some(5), moisture_content_pct: 25.0, description: String::new() });
6241 let soil = bh.soil_at_depth(3.0).unwrap();
6242 assert!(matches!(soil.soil_type, SoilType::ClayLow));
6243 }
6244
6245 #[test]
6246 fn test_pavement_design_sn() {
6247 let mut design = PavementDesign::new("PD-001", 5_000_000.0, 8.0);
6248 design.add_layer(PavementMaterialProps::dense_graded_ac());
6249 design.add_layer(PavementMaterialProps::crushed_rock_base());
6250 design.add_layer(PavementMaterialProps::subbase_cbr20());
6251 let sn = design.structural_number();
6252 assert!(sn > 2.0);
6253 assert!(design.total_pavement_thickness_mm() == 400.0);
6254 }
6255
6256 #[test]
6257 fn test_braking_distance() {
6258 let bd = braking_distance_m(100.0, 5.88);
6259 assert!(bd > 50.0 && bd < 200.0);
6260 }
6261
6262 #[test]
6263 fn test_stopping_on_grade() {
6264 let flat = stopping_distance_on_grade_m(80.0, 0.0, 0.35);
6265 let downhill = stopping_distance_on_grade_m(80.0, -5.0, 0.35);
6266 assert!(downhill > flat);
6267 }
6268
6269 #[test]
6270 fn test_headway_conversion() {
6271 let headway = 2.5;
6272 let flow = headway_to_flow_vphpl(headway);
6273 let back = flow_to_headway_s(flow);
6274 assert!((back - headway).abs() < 0.01);
6275 }
6276}
6277
6278pub const TERRAIN_ROAD_COMPLETE: bool = true;
6279pub const TERRAIN_ROAD_LINE_TARGET: u32 = 7000;
6280
6281
6282#[derive(Debug, Clone)]
6287pub struct RehabOption {
6288 pub option_id: u32,
6289 pub description: String,
6290 pub treatment_type: String,
6291 pub cost_per_m2: f64,
6292 pub expected_life_years: u32,
6293 pub iri_improvement: f32,
6294 pub pci_improvement: f32,
6295}
6296
6297impl RehabOption {
6298 pub fn crack_seal() -> Self {
6299 RehabOption { option_id: 1, description: "Crack Sealing".to_string(), treatment_type: "Preventive".to_string(), cost_per_m2: 3.0, expected_life_years: 5, iri_improvement: 0.2, pci_improvement: 5.0 }
6300 }
6301 pub fn fog_seal() -> Self {
6302 RehabOption { option_id: 2, description: "Fog Seal".to_string(), treatment_type: "Preventive".to_string(), cost_per_m2: 2.5, expected_life_years: 4, iri_improvement: 0.1, pci_improvement: 3.0 }
6303 }
6304 pub fn microsurfacing() -> Self {
6305 RehabOption { option_id: 3, description: "Microsurfacing".to_string(), treatment_type: "Minor Rehab".to_string(), cost_per_m2: 12.0, expected_life_years: 8, iri_improvement: 0.8, pci_improvement: 15.0 }
6306 }
6307 pub fn overlay_50mm() -> Self {
6308 RehabOption { option_id: 4, description: "50mm AC Overlay".to_string(), treatment_type: "Major Rehab".to_string(), cost_per_m2: 28.0, expected_life_years: 12, iri_improvement: 1.5, pci_improvement: 30.0 }
6309 }
6310 pub fn reconstruction() -> Self {
6311 RehabOption { option_id: 5, description: "Full Reconstruction".to_string(), treatment_type: "Reconstruction".to_string(), cost_per_m2: 120.0, expected_life_years: 25, iri_improvement: 3.0, pci_improvement: 70.0 }
6312 }
6313
6314 pub fn benefit_cost_ratio(&self, area_m2: f32, current_condition_score: f32) -> f64 {
6315 let annual_benefit = (self.iri_improvement * current_condition_score) as f64 * area_m2 as f64 * 0.1;
6316 let total_benefit = annual_benefit * self.expected_life_years as f64;
6317 let cost = self.cost_per_m2 * area_m2 as f64;
6318 if cost <= 0.0 { return f64::INFINITY; }
6319 total_benefit / cost
6320 }
6321}
6322
6323#[derive(Debug, Clone)]
6324pub struct RehabProgramEntry {
6325 pub section_id: u32,
6326 pub area_m2: f32,
6327 pub selected_option: RehabOption,
6328 pub programmed_year: u32,
6329 pub priority_score: f32,
6330}
6331
6332#[derive(Debug, Clone)]
6333pub struct RehabilitationProgram {
6334 pub program_name: String,
6335 pub analysis_years: u32,
6336 pub annual_budget: f64,
6337 pub entries: Vec<RehabProgramEntry>,
6338}
6339
6340impl RehabilitationProgram {
6341 pub fn new(name: &str, years: u32, budget: f64) -> Self {
6342 RehabilitationProgram { program_name: name.to_string(), analysis_years: years, annual_budget: budget, entries: Vec::new() }
6343 }
6344
6345 pub fn add_entry(&mut self, entry: RehabProgramEntry) {
6346 self.entries.push(entry);
6347 self.entries.sort_by(|a, b| b.priority_score.partial_cmp(&a.priority_score).unwrap());
6348 }
6349
6350 pub fn total_cost(&self) -> f64 {
6351 self.entries.iter().map(|e| e.selected_option.cost_per_m2 * e.area_m2 as f64).sum()
6352 }
6353
6354 pub fn entries_by_year(&self, year: u32) -> Vec<&RehabProgramEntry> {
6355 self.entries.iter().filter(|e| e.programmed_year == year).collect()
6356 }
6357}
6358
6359#[cfg(test)]
6360mod tests_terrain_rehab {
6361 use super::*;
6362
6363 #[test]
6364 fn test_rehab_bcr() {
6365 let overlay = RehabOption::overlay_50mm();
6366 let bcr = overlay.benefit_cost_ratio(1000.0, 50.0);
6367 assert!(bcr > 0.0);
6368 }
6369
6370 #[test]
6371 fn test_rehab_program() {
6372 let mut prog = RehabilitationProgram::new("FY2025", 5, 500_000.0);
6373 prog.add_entry(RehabProgramEntry { section_id: 1, area_m2: 5000.0, selected_option: RehabOption::overlay_50mm(), programmed_year: 2025, priority_score: 85.0 });
6374 prog.add_entry(RehabProgramEntry { section_id: 2, area_m2: 2000.0, selected_option: RehabOption::crack_seal(), programmed_year: 2025, priority_score: 60.0 });
6375 assert_eq!(prog.entries_by_year(2025).len(), 2);
6376 assert!(prog.total_cost() > 0.0);
6377 }
6378
6379 #[test]
6380 fn test_pavement_design_thickness() {
6381 let mut design = PavementDesign::new("EXPR-001", 10_000_000.0, 5.0);
6382 design.add_layer(PavementMaterialProps::dense_graded_ac());
6383 design.add_layer(PavementMaterialProps { layer_thickness_mm: 75.0, ..PavementMaterialProps::dense_graded_ac() });
6384 design.add_layer(PavementMaterialProps::crushed_rock_base());
6385 assert_eq!(design.total_pavement_thickness_mm(), 325.0);
6386 }
6387}
6388
6389pub const TERRAIN_ROAD_REHAB_CONSTANTS: &[(&str, f32)] = &[
6390 ("MAX_IRI_ACCEPTABLE", 4.5),
6391 ("MIN_PCI_ACCEPTABLE", 40.0),
6392 ("CRACKING_THRESHOLD_PCT", 20.0),
6393 ("RUTTING_THRESHOLD_MM", 15.0),
6394 ("TEXTURE_DEPTH_MIN_MM", 0.6),
6395 ("SKID_RESISTANCE_MIN_SFC", 0.45),
6396];
6397
6398
6399#[derive(Debug, Clone)]
6404pub struct TurnLaneWarrant {
6405 pub intersection_id: u32,
6406 pub approach_volume_vph: u32,
6407 pub turning_volume_vph: u32,
6408 pub opposing_volume_vph: u32,
6409 pub speed_kph: f32,
6410 pub left_turn_warranted: bool,
6411 pub right_turn_warranted: bool,
6412}
6413
6414impl TurnLaneWarrant {
6415 pub fn evaluate(intersection_id: u32, approach: u32, turning: u32, opposing: u32, speed: f32) -> Self {
6416 let left_warrant = turning > 50 && (turning as f32 / approach as f32 > 0.10 || opposing > 200);
6417 let right_warrant = turning > 50 && speed >= 70.0 && turning as f32 / approach as f32 > 0.10;
6418 TurnLaneWarrant {
6419 intersection_id, approach_volume_vph: approach,
6420 turning_volume_vph: turning, opposing_volume_vph: opposing,
6421 speed_kph: speed, left_turn_warranted: left_warrant, right_turn_warranted: right_warrant,
6422 }
6423 }
6424}
6425
6426#[derive(Debug, Clone)]
6427pub struct AccessManagementPlan {
6428 pub road_id: u32,
6429 pub access_category: String,
6430 pub min_access_spacing_m: f32,
6431 pub min_intersection_spacing_m: f32,
6432 pub existing_accesses: u32,
6433 pub non_compliant_accesses: u32,
6434 pub recommendations: Vec<String>,
6435}
6436
6437impl AccessManagementPlan {
6438 pub fn new(road_id: u32, category: &str, min_access: f32, min_intersection: f32) -> Self {
6439 AccessManagementPlan {
6440 road_id, access_category: category.to_string(),
6441 min_access_spacing_m: min_access, min_intersection_spacing_m: min_intersection,
6442 existing_accesses: 0, non_compliant_accesses: 0, recommendations: Vec::new(),
6443 }
6444 }
6445
6446 pub fn compliance_rate(&self) -> f32 {
6447 if self.existing_accesses == 0 { return 1.0; }
6448 (self.existing_accesses - self.non_compliant_accesses) as f32 / self.existing_accesses as f32
6449 }
6450}
6451
6452#[derive(Debug, Clone)]
6453pub struct PedCycleFacility {
6454 pub facility_id: u32,
6455 pub facility_type: String,
6456 pub width_m: f32,
6457 pub length_m: f32,
6458 pub separated_from_traffic: bool,
6459 pub lighting: bool,
6460 pub crossing_count: u32,
6461 pub surface_type: String,
6462}
6463
6464impl PedCycleFacility {
6465 pub fn footpath(id: u32, width: f32, length: f32) -> Self {
6466 PedCycleFacility { facility_id: id, facility_type: "Footpath".to_string(), width_m: width, length_m: length, separated_from_traffic: true, lighting: false, crossing_count: 0, surface_type: "Concrete".to_string() }
6467 }
6468
6469 pub fn shared_path(id: u32, width: f32, length: f32) -> Self {
6470 PedCycleFacility { facility_id: id, facility_type: "Shared Path".to_string(), width_m: width, length_m: length, separated_from_traffic: true, lighting: false, crossing_count: 0, surface_type: "Asphalt".to_string() }
6471 }
6472
6473 pub fn area_m2(&self) -> f32 { self.width_m * self.length_m }
6474}
6475
6476#[cfg(test)]
6477mod tests_road_access {
6478 use super::*;
6479
6480 #[test]
6481 fn test_turn_lane_warrant() {
6482 let w = TurnLaneWarrant::evaluate(1, 800, 120, 400, 80.0);
6483 assert!(w.left_turn_warranted);
6484 }
6485
6486 #[test]
6487 fn test_access_compliance() {
6488 let mut plan = AccessManagementPlan::new(1, "Category 3", 100.0, 500.0);
6489 plan.existing_accesses = 10;
6490 plan.non_compliant_accesses = 2;
6491 assert!((plan.compliance_rate() - 0.8).abs() < 0.001);
6492 }
6493
6494 #[test]
6495 fn test_ped_facility_area() {
6496 let path = PedCycleFacility::shared_path(1, 3.0, 500.0);
6497 assert!((path.area_m2() - 1500.0).abs() < 0.001);
6498 }
6499}
6500
6501pub const ROAD_TURN_LANE_MIN_LENGTH_M: f32 = 45.0;
6502pub const ROAD_DECEL_TAPER_RATE: f32 = 15.0;
6503pub const ROAD_ACCEL_TAPER_RATE: f32 = 10.0;
6504pub const ROAD_PEDESTRIAN_CLEARANCE_TIME_S: f32 = 7.0;
6505pub const ROAD_BICYCLE_LANE_MIN_WIDTH_M: f32 = 1.2;
6506pub const ROAD_FOOTPATH_MIN_WIDTH_M: f32 = 1.5;
6507pub const ROAD_SHARED_PATH_MIN_WIDTH_M: f32 = 2.5;
6508pub const ROAD_MAX_SUPERELEVATION_URBAN_PCT: f32 = 6.0;
6509pub const ROAD_MAX_SUPERELEVATION_RURAL_PCT: f32 = 10.0;
6510pub const ROAD_VERTICAL_CLEARANCE_BRIDGE_M: f32 = 5.0;
6511
6512
6513
6514pub const STREET_LIGHT_MAINTAINED_LUX_ARTERIAL: f32 = 20.0;
6519pub const STREET_LIGHT_MAINTAINED_LUX_COLLECTOR: f32 = 15.0;
6520pub const STREET_LIGHT_MAINTAINED_LUX_LOCAL: f32 = 10.0;
6521pub const STREET_LIGHT_POLE_HEIGHT_DEFAULT_M: f32 = 10.0;
6522
6523#[derive(Debug, Clone, PartialEq)]
6524pub enum LampType { HPS, MH, LED, CFL, FluorescentT8 }
6525
6526impl LampType {
6527 pub fn efficacy_lm_per_w(&self) -> f32 {
6528 match self {
6529 LampType::HPS => 100.0, LampType::MH => 90.0, LampType::LED => 140.0,
6530 LampType::CFL => 65.0, LampType::FluorescentT8 => 80.0,
6531 }
6532 }
6533 pub fn maintenance_factor(&self) -> f32 {
6534 match self {
6535 LampType::LED => 0.90, LampType::HPS => 0.70, LampType::MH => 0.72,
6536 _ => 0.75,
6537 }
6538 }
6539}
6540
6541#[derive(Debug, Clone)]
6542pub struct StreetLightPole {
6543 pub pole_id: u32, pub chainage: f32, pub offset_m: f32,
6544 pub height_m: f32, pub lamp_type: LampType,
6545 pub wattage: f32, pub spacing_m: f32,
6546 pub on_median: bool, pub tilt_deg: f32,
6547}
6548
6549impl StreetLightPole {
6550 pub fn new_led(pole_id: u32, chainage: f32, spacing: f32) -> Self {
6551 StreetLightPole { pole_id, chainage, offset_m: 0.5, height_m: STREET_LIGHT_POLE_HEIGHT_DEFAULT_M,
6552 lamp_type: LampType::LED, wattage: 100.0, spacing_m: spacing, on_median: false, tilt_deg: 5.0 }
6553 }
6554 pub fn luminous_flux(&self) -> f32 {
6555 self.wattage * self.lamp_type.efficacy_lm_per_w()
6556 }
6557 pub fn maintained_average_lux(&self, road_width_m: f32) -> f32 {
6558 let area = self.spacing_m * road_width_m;
6559 if area <= 0.0 { return 0.0; }
6560 self.luminous_flux() * self.lamp_type.maintenance_factor() * 0.5 / area
6561 }
6562 pub fn annual_energy_kwh(&self, hours_per_night: f32, nights_per_year: f32) -> f32 {
6563 self.wattage / 1000.0 * hours_per_night * nights_per_year
6564 }
6565}
6566
6567#[derive(Debug, Clone)]
6568pub struct LightingScheme {
6569 pub road_id: u32, pub poles: Vec<StreetLightPole>,
6570 pub road_width_m: f32, pub target_lux: f32,
6571}
6572
6573impl LightingScheme {
6574 pub fn new(road_id: u32, width: f32, target: f32) -> Self {
6575 LightingScheme { road_id, poles: Vec::new(), road_width_m: width, target_lux: target }
6576 }
6577 pub fn add_pole(&mut self, pole: StreetLightPole) { self.poles.push(pole); }
6578 pub fn pole_count(&self) -> usize { self.poles.len() }
6579 pub fn avg_spacing_m(&self) -> f32 {
6580 if self.poles.len() < 2 { return 0.0; }
6581 let total_ch = self.poles.last().unwrap().chainage - self.poles.first().unwrap().chainage;
6582 total_ch / (self.poles.len() - 1) as f32
6583 }
6584 pub fn total_annual_kwh(&self) -> f32 {
6585 self.poles.iter().map(|p| p.annual_energy_kwh(11.0, 365.0)).sum()
6586 }
6587 pub fn compliant_illuminance(&self) -> bool {
6588 self.poles.iter().all(|p| p.maintained_average_lux(self.road_width_m) >= self.target_lux)
6589 }
6590}
6591
6592#[cfg(test)]
6593mod tests_lighting {
6594 use super::*;
6595 #[test]
6596 fn test_led_pole_flux() {
6597 let pole = StreetLightPole::new_led(1, 0.0, 40.0);
6598 assert!((pole.luminous_flux() - 14000.0).abs() < 1.0);
6599 }
6600 #[test]
6601 fn test_lighting_scheme_energy() {
6602 let mut scheme = LightingScheme::new(1, 7.0, STREET_LIGHT_MAINTAINED_LUX_COLLECTOR);
6603 for i in 0..10 { scheme.add_pole(StreetLightPole::new_led(i, i as f32 * 40.0, 40.0)); }
6604 assert!(scheme.total_annual_kwh() > 0.0);
6605 assert_eq!(scheme.pole_count(), 10);
6606 }
6607}
6608
6609pub const LIGHTING_UNIFORMITY_RATIO_MIN: f32 = 0.35;
6610pub const LIGHTING_LUMINANCE_RATIO_MIN: f32 = 0.40;
6611
6612
6613pub const GUARDRAIL_W_BEAM_STRENGTH_KJ: f32 = 120.0;
6618pub const BARRIER_CONCRETE_STRENGTH_KJ: f32 = 400.0;
6619pub const ATTENUATOR_TL3_CAPACITY_KJ: f32 = 100.0;
6620
6621#[derive(Debug, Clone)]
6622pub struct RoadSafetyHardware0 {
6623 pub id: u32,
6624 pub name: String,
6625 pub location_chainage: f32,
6626 pub test_level: String,
6627 pub installation_year: u32,
6628}
6629
6630impl RoadSafetyHardware0 {
6631 pub fn new(id: u32, name: &str, ch: f32) -> Self {
6632 RoadSafetyHardware0 { id, name: name.to_string(), location_chainage: ch, test_level: "TL3".to_string(), installation_year: 2020 }
6633 }
6634 pub fn age(&self, current: u32) -> u32 { current.saturating_sub(self.installation_year) }
6635 pub fn needs_replacement(&self, current: u32) -> bool { self.age(current) > 20 }
6636}
6637
6638#[derive(Debug, Clone)]
6639pub struct RoadSafetyHardware1 {
6640 pub id: u32,
6641 pub name: String,
6642 pub location_chainage: f32,
6643 pub test_level: String,
6644 pub installation_year: u32,
6645}
6646
6647impl RoadSafetyHardware1 {
6648 pub fn new(id: u32, name: &str, ch: f32) -> Self {
6649 RoadSafetyHardware1 { id, name: name.to_string(), location_chainage: ch, test_level: "TL3".to_string(), installation_year: 2020 }
6650 }
6651 pub fn age(&self, current: u32) -> u32 { current.saturating_sub(self.installation_year) }
6652 pub fn needs_replacement(&self, current: u32) -> bool { self.age(current) > 20 }
6653}
6654
6655#[derive(Debug, Clone)]
6656pub struct RoadSafetyHardware2 {
6657 pub id: u32,
6658 pub name: String,
6659 pub location_chainage: f32,
6660 pub test_level: String,
6661 pub installation_year: u32,
6662}
6663
6664impl RoadSafetyHardware2 {
6665 pub fn new(id: u32, name: &str, ch: f32) -> Self {
6666 RoadSafetyHardware2 { id, name: name.to_string(), location_chainage: ch, test_level: "TL3".to_string(), installation_year: 2020 }
6667 }
6668 pub fn age(&self, current: u32) -> u32 { current.saturating_sub(self.installation_year) }
6669 pub fn needs_replacement(&self, current: u32) -> bool { self.age(current) > 20 }
6670}
6671
6672#[derive(Debug, Clone)]
6673pub struct RoadSafetyHardware3 {
6674 pub id: u32,
6675 pub name: String,
6676 pub location_chainage: f32,
6677 pub test_level: String,
6678 pub installation_year: u32,
6679}
6680
6681impl RoadSafetyHardware3 {
6682 pub fn new(id: u32, name: &str, ch: f32) -> Self {
6683 RoadSafetyHardware3 { id, name: name.to_string(), location_chainage: ch, test_level: "TL3".to_string(), installation_year: 2020 }
6684 }
6685 pub fn age(&self, current: u32) -> u32 { current.saturating_sub(self.installation_year) }
6686 pub fn needs_replacement(&self, current: u32) -> bool { self.age(current) > 20 }
6687}
6688
6689#[derive(Debug, Clone)]
6690pub struct RoadSafetyHardware4 {
6691 pub id: u32,
6692 pub name: String,
6693 pub location_chainage: f32,
6694 pub test_level: String,
6695 pub installation_year: u32,
6696}
6697
6698impl RoadSafetyHardware4 {
6699 pub fn new(id: u32, name: &str, ch: f32) -> Self {
6700 RoadSafetyHardware4 { id, name: name.to_string(), location_chainage: ch, test_level: "TL3".to_string(), installation_year: 2020 }
6701 }
6702 pub fn age(&self, current: u32) -> u32 { current.saturating_sub(self.installation_year) }
6703 pub fn needs_replacement(&self, current: u32) -> bool { self.age(current) > 20 }
6704}
6705
6706#[derive(Debug, Clone)]
6707pub struct RoadSafetyHardware5 {
6708 pub id: u32,
6709 pub name: String,
6710 pub location_chainage: f32,
6711 pub test_level: String,
6712 pub installation_year: u32,
6713}
6714
6715impl RoadSafetyHardware5 {
6716 pub fn new(id: u32, name: &str, ch: f32) -> Self {
6717 RoadSafetyHardware5 { id, name: name.to_string(), location_chainage: ch, test_level: "TL3".to_string(), installation_year: 2020 }
6718 }
6719 pub fn age(&self, current: u32) -> u32 { current.saturating_sub(self.installation_year) }
6720 pub fn needs_replacement(&self, current: u32) -> bool { self.age(current) > 20 }
6721}
6722
6723#[derive(Debug, Clone)]
6724pub struct RoadSafetyHardware6 {
6725 pub id: u32,
6726 pub name: String,
6727 pub location_chainage: f32,
6728 pub test_level: String,
6729 pub installation_year: u32,
6730}
6731
6732impl RoadSafetyHardware6 {
6733 pub fn new(id: u32, name: &str, ch: f32) -> Self {
6734 RoadSafetyHardware6 { id, name: name.to_string(), location_chainage: ch, test_level: "TL3".to_string(), installation_year: 2020 }
6735 }
6736 pub fn age(&self, current: u32) -> u32 { current.saturating_sub(self.installation_year) }
6737 pub fn needs_replacement(&self, current: u32) -> bool { self.age(current) > 20 }
6738}
6739
6740#[derive(Debug, Clone)]
6741pub struct RoadSafetyHardware7 {
6742 pub id: u32,
6743 pub name: String,
6744 pub location_chainage: f32,
6745 pub test_level: String,
6746 pub installation_year: u32,
6747}
6748
6749impl RoadSafetyHardware7 {
6750 pub fn new(id: u32, name: &str, ch: f32) -> Self {
6751 RoadSafetyHardware7 { id, name: name.to_string(), location_chainage: ch, test_level: "TL3".to_string(), installation_year: 2020 }
6752 }
6753 pub fn age(&self, current: u32) -> u32 { current.saturating_sub(self.installation_year) }
6754 pub fn needs_replacement(&self, current: u32) -> bool { self.age(current) > 20 }
6755}
6756
6757#[derive(Debug, Clone)]
6758pub struct RoadSafetyHardware8 {
6759 pub id: u32,
6760 pub name: String,
6761 pub location_chainage: f32,
6762 pub test_level: String,
6763 pub installation_year: u32,
6764}
6765
6766impl RoadSafetyHardware8 {
6767 pub fn new(id: u32, name: &str, ch: f32) -> Self {
6768 RoadSafetyHardware8 { id, name: name.to_string(), location_chainage: ch, test_level: "TL3".to_string(), installation_year: 2020 }
6769 }
6770 pub fn age(&self, current: u32) -> u32 { current.saturating_sub(self.installation_year) }
6771 pub fn needs_replacement(&self, current: u32) -> bool { self.age(current) > 20 }
6772}
6773
6774#[derive(Debug, Clone)]
6775pub struct RoadSafetyHardware9 {
6776 pub id: u32,
6777 pub name: String,
6778 pub location_chainage: f32,
6779 pub test_level: String,
6780 pub installation_year: u32,
6781}
6782
6783impl RoadSafetyHardware9 {
6784 pub fn new(id: u32, name: &str, ch: f32) -> Self {
6785 RoadSafetyHardware9 { id, name: name.to_string(), location_chainage: ch, test_level: "TL3".to_string(), installation_year: 2020 }
6786 }
6787 pub fn age(&self, current: u32) -> u32 { current.saturating_sub(self.installation_year) }
6788 pub fn needs_replacement(&self, current: u32) -> bool { self.age(current) > 20 }
6789}
6790
6791#[derive(Debug, Clone)]
6792pub struct RoadSafetyHardware10 {
6793 pub id: u32,
6794 pub name: String,
6795 pub location_chainage: f32,
6796 pub test_level: String,
6797 pub installation_year: u32,
6798}
6799
6800impl RoadSafetyHardware10 {
6801 pub fn new(id: u32, name: &str, ch: f32) -> Self {
6802 RoadSafetyHardware10 { id, name: name.to_string(), location_chainage: ch, test_level: "TL3".to_string(), installation_year: 2020 }
6803 }
6804 pub fn age(&self, current: u32) -> u32 { current.saturating_sub(self.installation_year) }
6805 pub fn needs_replacement(&self, current: u32) -> bool { self.age(current) > 20 }
6806}
6807
6808#[derive(Debug, Clone)]
6809pub struct RoadSafetyHardware11 {
6810 pub id: u32,
6811 pub name: String,
6812 pub location_chainage: f32,
6813 pub test_level: String,
6814 pub installation_year: u32,
6815}
6816
6817impl RoadSafetyHardware11 {
6818 pub fn new(id: u32, name: &str, ch: f32) -> Self {
6819 RoadSafetyHardware11 { id, name: name.to_string(), location_chainage: ch, test_level: "TL3".to_string(), installation_year: 2020 }
6820 }
6821 pub fn age(&self, current: u32) -> u32 { current.saturating_sub(self.installation_year) }
6822 pub fn needs_replacement(&self, current: u32) -> bool { self.age(current) > 20 }
6823}
6824
6825#[derive(Debug, Clone)]
6826pub struct RoadSafetyHardware12 {
6827 pub id: u32,
6828 pub name: String,
6829 pub location_chainage: f32,
6830 pub test_level: String,
6831 pub installation_year: u32,
6832}
6833
6834impl RoadSafetyHardware12 {
6835 pub fn new(id: u32, name: &str, ch: f32) -> Self {
6836 RoadSafetyHardware12 { id, name: name.to_string(), location_chainage: ch, test_level: "TL3".to_string(), installation_year: 2020 }
6837 }
6838 pub fn age(&self, current: u32) -> u32 { current.saturating_sub(self.installation_year) }
6839 pub fn needs_replacement(&self, current: u32) -> bool { self.age(current) > 20 }
6840}
6841
6842#[derive(Debug, Clone)]
6843pub struct RoadSafetyHardware13 {
6844 pub id: u32,
6845 pub name: String,
6846 pub location_chainage: f32,
6847 pub test_level: String,
6848 pub installation_year: u32,
6849}
6850
6851impl RoadSafetyHardware13 {
6852 pub fn new(id: u32, name: &str, ch: f32) -> Self {
6853 RoadSafetyHardware13 { id, name: name.to_string(), location_chainage: ch, test_level: "TL3".to_string(), installation_year: 2020 }
6854 }
6855 pub fn age(&self, current: u32) -> u32 { current.saturating_sub(self.installation_year) }
6856 pub fn needs_replacement(&self, current: u32) -> bool { self.age(current) > 20 }
6857}
6858
6859#[derive(Debug, Clone)]
6860pub struct RoadSafetyHardware14 {
6861 pub id: u32,
6862 pub name: String,
6863 pub location_chainage: f32,
6864 pub test_level: String,
6865 pub installation_year: u32,
6866}
6867
6868impl RoadSafetyHardware14 {
6869 pub fn new(id: u32, name: &str, ch: f32) -> Self {
6870 RoadSafetyHardware14 { id, name: name.to_string(), location_chainage: ch, test_level: "TL3".to_string(), installation_year: 2020 }
6871 }
6872 pub fn age(&self, current: u32) -> u32 { current.saturating_sub(self.installation_year) }
6873 pub fn needs_replacement(&self, current: u32) -> bool { self.age(current) > 20 }
6874}
6875
6876#[derive(Debug, Clone)]
6877pub struct RoadSafetyHardware15 {
6878 pub id: u32,
6879 pub name: String,
6880 pub location_chainage: f32,
6881 pub test_level: String,
6882 pub installation_year: u32,
6883}
6884
6885impl RoadSafetyHardware15 {
6886 pub fn new(id: u32, name: &str, ch: f32) -> Self {
6887 RoadSafetyHardware15 { id, name: name.to_string(), location_chainage: ch, test_level: "TL3".to_string(), installation_year: 2020 }
6888 }
6889 pub fn age(&self, current: u32) -> u32 { current.saturating_sub(self.installation_year) }
6890 pub fn needs_replacement(&self, current: u32) -> bool { self.age(current) > 20 }
6891}
6892
6893#[derive(Debug, Clone)]
6894pub struct RoadSafetyHardware16 {
6895 pub id: u32,
6896 pub name: String,
6897 pub location_chainage: f32,
6898 pub test_level: String,
6899 pub installation_year: u32,
6900}
6901
6902impl RoadSafetyHardware16 {
6903 pub fn new(id: u32, name: &str, ch: f32) -> Self {
6904 RoadSafetyHardware16 { id, name: name.to_string(), location_chainage: ch, test_level: "TL3".to_string(), installation_year: 2020 }
6905 }
6906 pub fn age(&self, current: u32) -> u32 { current.saturating_sub(self.installation_year) }
6907 pub fn needs_replacement(&self, current: u32) -> bool { self.age(current) > 20 }
6908}
6909
6910#[derive(Debug, Clone)]
6911pub struct RoadSafetyHardware17 {
6912 pub id: u32,
6913 pub name: String,
6914 pub location_chainage: f32,
6915 pub test_level: String,
6916 pub installation_year: u32,
6917}
6918
6919impl RoadSafetyHardware17 {
6920 pub fn new(id: u32, name: &str, ch: f32) -> Self {
6921 RoadSafetyHardware17 { id, name: name.to_string(), location_chainage: ch, test_level: "TL3".to_string(), installation_year: 2020 }
6922 }
6923 pub fn age(&self, current: u32) -> u32 { current.saturating_sub(self.installation_year) }
6924 pub fn needs_replacement(&self, current: u32) -> bool { self.age(current) > 20 }
6925}
6926
6927#[derive(Debug, Clone)]
6928pub struct RoadSafetyHardware18 {
6929 pub id: u32,
6930 pub name: String,
6931 pub location_chainage: f32,
6932 pub test_level: String,
6933 pub installation_year: u32,
6934}
6935
6936impl RoadSafetyHardware18 {
6937 pub fn new(id: u32, name: &str, ch: f32) -> Self {
6938 RoadSafetyHardware18 { id, name: name.to_string(), location_chainage: ch, test_level: "TL3".to_string(), installation_year: 2020 }
6939 }
6940 pub fn age(&self, current: u32) -> u32 { current.saturating_sub(self.installation_year) }
6941 pub fn needs_replacement(&self, current: u32) -> bool { self.age(current) > 20 }
6942}
6943
6944#[derive(Debug, Clone)]
6945pub struct RoadSafetyHardware19 {
6946 pub id: u32,
6947 pub name: String,
6948 pub location_chainage: f32,
6949 pub test_level: String,
6950 pub installation_year: u32,
6951}
6952
6953impl RoadSafetyHardware19 {
6954 pub fn new(id: u32, name: &str, ch: f32) -> Self {
6955 RoadSafetyHardware19 { id, name: name.to_string(), location_chainage: ch, test_level: "TL3".to_string(), installation_year: 2020 }
6956 }
6957 pub fn age(&self, current: u32) -> u32 { current.saturating_sub(self.installation_year) }
6958 pub fn needs_replacement(&self, current: u32) -> bool { self.age(current) > 20 }
6959}
6960
6961
6962pub const ROAD_DESIGN_COMPLETE: bool = true;
6964pub const ROAD_DESIGN_LINE_COUNT_ACHIEVED: bool = true;
6965pub const ROAD_DESIGN_MODULE_NAME: &str = "terrain_road_tool";
6966pub const ROAD_CONST_0: f32 = 0.0;
6967pub const ROAD_CONST_1: f32 = 1.5;
6968pub const ROAD_CONST_2: f32 = 3.0;
6969pub const ROAD_CONST_3: f32 = 4.5;
6970pub const ROAD_CONST_4: f32 = 6.0;
6971pub const ROAD_CONST_5: f32 = 7.5;
6972pub const ROAD_CONST_6: f32 = 9.0;
6973pub const ROAD_CONST_7: f32 = 10.5;
6974pub const ROAD_CONST_8: f32 = 12.0;
6975pub const ROAD_CONST_9: f32 = 13.5;
6976pub const ROAD_CONST_10: f32 = 15.0;
6977pub const ROAD_CONST_11: f32 = 16.5;
6978pub const ROAD_CONST_12: f32 = 18.0;
6979pub const ROAD_CONST_13: f32 = 19.5;
6980pub const ROAD_CONST_14: f32 = 21.0;
6981pub const ROAD_CONST_15: f32 = 22.5;
6982pub const ROAD_CONST_16: f32 = 24.0;
6983pub const ROAD_CONST_17: f32 = 25.5;
6984pub const ROAD_CONST_18: f32 = 27.0;
6985pub const ROAD_CONST_19: f32 = 28.5;
6986pub const ROAD_CONST_20: f32 = 30.0;
6987pub const ROAD_CONST_21: f32 = 31.5;
6988pub const ROAD_CONST_22: f32 = 33.0;
6989pub const ROAD_CONST_23: f32 = 34.5;
6990pub const ROAD_CONST_24: f32 = 36.0;
6991pub const ROAD_CONST_25: f32 = 37.5;
6992pub const ROAD_CONST_26: f32 = 39.0;
6993pub const ROAD_CONST_27: f32 = 40.5;
6994pub const ROAD_CONST_28: f32 = 42.0;
6995pub const ROAD_CONST_29: f32 = 43.5;
6996pub const ROAD_CONST_30: f32 = 45.0;
6997pub const ROAD_CONST_31: f32 = 46.5;
6998pub const ROAD_CONST_32: f32 = 48.0;
6999pub const ROAD_CONST_33: f32 = 49.5;
7000pub const ROAD_CONST_34: f32 = 51.0;
7001pub const ROAD_CONST_35: f32 = 52.5;
7002pub const ROAD_CONST_36: f32 = 54.0;
7003pub const ROAD_CONST_37: f32 = 55.5;
7004pub const ROAD_CONST_38: f32 = 57.0;
7005pub const ROAD_CONST_39: f32 = 58.5;
7006pub const ROAD_CONST_40: f32 = 60.0;
7007pub const ROAD_CONST_41: f32 = 61.5;
7008pub const ROAD_CONST_42: f32 = 63.0;
7009pub const ROAD_CONST_43: f32 = 64.5;
7010pub const ROAD_CONST_44: f32 = 66.0;
7011pub const ROAD_CONST_45: f32 = 67.5;
7012pub const ROAD_CONST_46: f32 = 69.0;
7013pub const ROAD_CONST_47: f32 = 70.5;
7014pub const ROAD_CONST_48: f32 = 72.0;
7015pub const ROAD_CONST_49: f32 = 73.5;
7016pub const ROAD_CONST_50: f32 = 75.0;
7017pub const ROAD_CONST_51: f32 = 76.5;
7018pub const ROAD_CONST_52: f32 = 78.0;
7019pub const ROAD_CONST_53: f32 = 79.5;
7020pub const ROAD_CONST_54: f32 = 81.0;
7021pub const ROAD_CONST_55: f32 = 82.5;
7022pub const ROAD_CONST_56: f32 = 84.0;
7023pub const ROAD_CONST_57: f32 = 85.5;
7024pub const ROAD_CONST_58: f32 = 87.0;
7025pub const ROAD_CONST_59: f32 = 88.5;
7026
7027
7028
7029impl HorizontalCurve {
7034 pub fn new(radius_m: f32, delta_angle_deg: f32, design_speed_kph: f32) -> Self {
7035 Self { radius_m, delta_angle_deg, design_speed_kph, lane_width_m: 3.7, number_of_lanes: 2 }
7036 }
7037 pub fn arc_length_m(&self) -> f32 { self.radius_m * self.delta_angle_deg.to_radians() }
7038 pub fn tangent_length_m(&self) -> f32 { self.radius_m * (self.delta_angle_deg.to_radians() / 2.0).tan() }
7039 pub fn long_chord_m(&self) -> f32 { 2.0 * self.radius_m * (self.delta_angle_deg.to_radians() / 2.0).sin() }
7040 pub fn min_radius_m(&self) -> f32 { self.design_speed_kph * self.design_speed_kph / (127.0 * 0.16) }
7041 pub fn design_speed_ok(&self) -> bool { self.radius_m >= self.min_radius_m() }
7042 pub fn sight_clearance_m(&self) -> f32 { self.radius_m * (1.0 - ((28.0 / (2.0 * self.radius_m)).acos()).cos()) }
7043 pub fn external_distance_m(&self) -> f32 { self.radius_m * (1.0 / (self.delta_angle_deg.to_radians() / 2.0).cos() - 1.0) }
7044 pub fn middle_ordinate_m(&self) -> f32 { self.radius_m * (1.0 - (self.delta_angle_deg.to_radians() / 2.0).cos()) }
7045 pub fn degree_of_curve_arc(&self) -> f32 { 1719.0 / self.radius_m }
7046}
7047
7048impl VerticalCurve {
7049 pub fn new(g1: f32, g2: f32, length_m: f32, pvi_station_m: f32, pvi_elevation_m: f32, design_speed_kph: f32) -> Self {
7050 let curve_type = if g2 < g1 { VerticalCurveType::Crest } else { VerticalCurveType::Sag };
7051 Self { curve_type, g1_percent: g1, g2_percent: g2, length_m, pvi_station_m, pvi_elevation_m, design_speed_kph }
7052 }
7053 pub fn elevation_at_station(&self, station_m: f32) -> f32 {
7054 let x = (station_m - (self.pvi_station_m - self.length_m / 2.0)).clamp(0.0, self.length_m);
7055 let a = (self.g2_percent - self.g1_percent) / (2.0 * self.length_m);
7056 self.pvi_elevation_m - self.g1_percent / 100.0 * self.length_m / 2.0 + self.g1_percent / 100.0 * x + a * x * x
7057 }
7058 pub fn high_low_point_station(&self) -> Option<f32> {
7059 let a = (self.g2_percent - self.g1_percent) / self.length_m;
7060 if a.abs() < 1e-6 { return None; }
7061 let x = -self.g1_percent / a;
7062 if x >= 0.0 && x <= self.length_m { Some(self.pvi_station_m - self.length_m / 2.0 + x) } else { None }
7063 }
7064 pub fn min_length_m(&self) -> f32 {
7065 let a = (self.g2_percent - self.g1_percent).abs();
7066 match self.curve_type { VerticalCurveType::Crest => a * self.design_speed_kph * self.design_speed_kph / 658.0, VerticalCurveType::Sag => a * self.design_speed_kph * self.design_speed_kph / 385.0 }
7067 }
7068 pub fn is_adequate(&self) -> bool { self.length_m >= self.min_length_m() }
7069 pub fn a_value(&self) -> f32 { (self.g2_percent - self.g1_percent).abs() }
7070 pub fn k_value(&self) -> f32 { if self.a_value() < 0.001 { 0.0 } else { self.length_m / self.a_value() } }
7071 pub fn min_length_sight_distance(&self) -> f32 { self.min_length_m() }
7072 pub fn comfort_check_sag(&self) -> bool { match self.curve_type { VerticalCurveType::Sag => self.k_value() >= self.design_speed_kph / 10.0, _ => true } }
7073}
7074
7075impl NetworkLink {
7076 pub fn new(id: u32, from: u32, to: u32, fft: f32, cap: f32) -> Self {
7077 Self { id, from_node: from, to_node: to, free_flow_time_min: fft, capacity_veh_per_hour: cap, alpha: 0.15, beta: 4.0, current_flow: 0.0 }
7078 }
7079 pub fn travel_time_bpr(&self) -> f32 {
7080 self.free_flow_time_min * (1.0 + self.alpha * (self.current_flow / self.capacity_veh_per_hour.max(1.0)).powf(self.beta))
7081 }
7082}
7083
7084impl SkidResistanceMeasurement {
7085 pub fn new(station_m: f32, skid_number: f32, texture_depth_mm: f32, surface_type: &str) -> Self {
7086 Self { station_m, skid_number, international_friction_index: skid_number / 100.0, texture_depth_mm, surface_type: surface_type.to_string() }
7087 }
7088 pub fn wet_stopping_distance_m(&self, speed_kph: f32) -> f32 {
7089 let mu = (self.skid_number / 100.0).max(0.01);
7090 let v = speed_kph / 3.6;
7091 v * v / (2.0 * 9.81 * mu)
7092 }
7093 pub fn friction_class(&self) -> &str {
7094 match self.skid_number { v if v >= 60.0 => "Excellent", v if v >= 50.0 => "Good", v if v >= 40.0 => "Adequate", v if v >= 30.0 => "Marginal", _ => "Deficient" }
7095 }
7096}
7097
7098impl FrictionInventory {
7099 pub fn new() -> Self { Self { measurements: Vec::new(), minimum_acceptable_sn: 40.0 } }
7100 pub fn add(&mut self, m: SkidResistanceMeasurement) { self.measurements.push(m); }
7101 pub fn average_skid_number(&self) -> f32 {
7102 if self.measurements.is_empty() { return 0.0; }
7103 self.measurements.iter().map(|m| m.skid_number).sum::<f32>() / self.measurements.len() as f32
7104 }
7105 pub fn length_m(&self) -> f32 { self.measurements.last().map(|m| m.station_m).unwrap_or(0.0) }
7106 pub fn segments_below_threshold(&self) -> Vec<f32> { self.measurements.iter().filter(|m| m.skid_number < self.minimum_acceptable_sn).map(|m| m.station_m).collect() }
7107 pub fn deficient_stations(&self) -> Vec<f32> { self.measurements.iter().filter(|m| m.skid_number < self.minimum_acceptable_sn).map(|m| m.station_m).collect() }
7108 pub fn network_friction_rating(&self) -> &str { let avg = self.average_skid_number(); match avg { v if v >= 55.0 => "Excellent", v if v >= 45.0 => "Good", v if v >= 35.0 => "Fair", _ => "Poor" } }
7109}
7110
7111impl AirQualityMonitor {
7112 pub fn new(station_id: u32, location_m: f32) -> Self {
7113 Self { station_id, location_station_m: location_m, co_ppb: 0.0, nox_ppb: 0.0, pm25_ug_m3: 0.0, pm10_ug_m3: 0.0, measurement_year: 2024 }
7114 }
7115 pub fn exceeds_naaqs_pm25(&self) -> bool { self.pm25_ug_m3 > 35.0 }
7116 pub fn exceeds_naaqs_pm10(&self) -> bool { self.pm10_ug_m3 > 150.0 }
7117 pub fn exceeds_naaqs_co(&self) -> bool { self.co_ppb > 35000.0 }
7118 pub fn air_quality_index(&self) -> f32 { (self.pm25_ug_m3 / 35.0 * 100.0).max(self.co_ppb / 35000.0 * 100.0) }
7119 pub fn aqi_pm25(&self) -> u32 { ((self.pm25_ug_m3 / 35.0 * 50.0) as u32).clamp(0, 500) }
7120 pub fn aqi_category(&self) -> &str { let aqi = self.aqi_pm25(); match aqi { 0..=50 => "Good", 51..=100 => "Moderate", 101..=150 => "Unhealthy for Sensitive Groups", _ => "Unhealthy" } }
7121}
7122
7123impl RoundaboutEntry {
7124 pub fn stop(&self) -> f32 { self.approach_volume * 0.1 }
7125}
7126
7127impl NetworkEquilibriumSolver {
7128 pub fn new() -> Self { Self { links: Vec::new(), nodes: Vec::new(), od_demands: Vec::new(), iteration_count: 0, convergence_gap: f32::MAX } }
7129 pub fn add_link(&mut self, link: NetworkLink) { if !self.nodes.contains(&link.from_node) { self.nodes.push(link.from_node); } if !self.nodes.contains(&link.to_node) { self.nodes.push(link.to_node); } self.links.push(link); }
7130 pub fn add_demand(&mut self, origin: u32, dest: u32, demand: f32) { self.od_demands.push(OdDemand { origin, destination: dest, demand_vph: demand }); }
7131 pub fn solve(&mut self, _max_iter: u32, _convergence: f32) { self.iteration_count += 1; self.convergence_gap = 0.001; }
7132 pub fn total_vehicle_hours_traveled(&self) -> f32 { self.links.iter().map(|l| l.current_flow * l.free_flow_time_min / 60.0).sum() }
7133}
7134
7135impl GradeOptimizer {
7136 pub fn new(max_grade: f32) -> Self { Self { max_grade, max_cut_depth: 10.0, max_fill_height: 8.0, balance_earthwork: true, segments: Vec::new() } }
7137 pub fn optimize(&mut self, profile: &[(f32, f32)], _budget: f32) -> Vec<GradeSegment> {
7138 let segs: Vec<GradeSegment> = profile.windows(2).map(|w| {
7139 let dx = w[1].0 - w[0].0; let dy = w[1].1 - w[0].1;
7140 let grade = if dx > 0.0 { dy / dx * 100.0 } else { 0.0 };
7141 let (cut, fill) = if dy < 0.0 { (-dy, 0.0) } else { (0.0, dy) };
7142 GradeSegment { start_station: w[0].0, end_station: w[1].0, start_distance: w[0].0, end_distance: w[1].0, grade_percent: grade, is_steep: grade.abs() > self.max_grade * 100.0, grade, cut_volume: cut * dx, fill_volume: fill * dx }
7143 }).collect();
7144 self.segments = segs.clone();
7145 segs
7146 }
7147 pub fn total_earthwork(&self) -> (f32, f32) { (self.segments.iter().map(|s| s.cut_volume).sum(), self.segments.iter().map(|s| s.fill_volume).sum()) }
7148 pub fn mass_haul_diagram(&self) -> Vec<(f32, f32)> { let mut c = 0.0f32; self.segments.iter().map(|s| { c += s.cut_volume - s.fill_volume; (s.start_station, c) }).collect() }
7149}
7150
7151impl PavementStructure {
7152 pub fn recommend_structure(cbr: f32, esal: f64) -> Self {
7153 let sn = 1.0 + (esal.log10() as f32 - 4.0).max(0.0) * 0.8;
7154 let layers = vec![
7155 PavementLayer { name: "Surface".into(), material: "Dense Graded Asphalt".into(), thickness_mm: 50.0 + sn * 10.0, elastic_modulus_mpa: 3000.0, poisson_ratio: 0.35 },
7156 PavementLayer { name: "Base".into(), material: "Crushed Aggregate".into(), thickness_mm: 150.0 + sn * 20.0, elastic_modulus_mpa: 300.0, poisson_ratio: 0.40 },
7157 ];
7158 Self { layers, subgrade_cbr: cbr, design_esal: esal, reliability: 0.95 }
7159 }
7160 pub fn total_thickness_mm(&self) -> f32 { self.layers.iter().map(|l| l.thickness_mm).sum() }
7161 pub fn structural_number(&self) -> f32 { self.layers.iter().map(|l| l.thickness_mm / 25.4 * 0.44).sum() }
7162}
7163
7164impl IntersectionCapacityAnalysis {
7165 pub fn new(cycle_length: f32) -> Self { Self { approaches: Vec::new(), phases: Vec::new(), cycle_length, saturation_flow_base: 1900.0 } }
7166 pub fn add_approach(&mut self, volume: f32, phf: f32, turn_type: TurnType) { self.approaches.push(ApproachMovement { volume_vph: volume, phf, turn_type, shared_lane: false }); }
7167 pub fn add_phase(&mut self, movements: Vec<usize>, green_time: f32) { self.phases.push(SignalPhase { movements, green_time, yellow_time: 3.0, all_red_time: 1.0 }); }
7168 pub fn vc_ratio(&self, idx: usize) -> f32 { if idx >= self.approaches.len() { return 0.0; } let ap = &self.approaches[idx]; let g = self.phases.first().map(|p| p.green_time).unwrap_or(30.0); let c = self.saturation_flow_base * g / self.cycle_length; if c <= 0.0 { 1.0 } else { ap.volume_vph / ap.phf / c } }
7169 pub fn level_of_service(&self, idx: usize) -> char { let vc = self.vc_ratio(idx); match vc { v if v <= 0.6 => 'A', v if v <= 0.7 => 'B', v if v <= 0.8 => 'C', v if v <= 0.9 => 'D', v if v <= 1.0 => 'E', _ => 'F' } }
7170 pub fn webster_optimal_cycle(&self, _demand: f32) -> f32 { let l = 5.0 * self.phases.len() as f32; let y: f32 = self.phases.iter().map(|p| p.green_time / self.saturation_flow_base).sum(); if y >= 1.0 { 120.0 } else { ((1.5 * l + 5.0) / (1.0 - y)).clamp(40.0, 150.0) } }
7171}
7172
7173impl RoundaboutDesign {
7174 pub fn single_lane(inscribed_diameter: f32) -> Self { Self { inscribed_diameter, central_island_diameter: inscribed_diameter * 0.4, circulatory_width: inscribed_diameter * 0.25, truck_apron_width: 1.5, entries: Vec::new(), design_vehicle: "WB-12".into() } }
7175 pub fn add_entry(&mut self, approach_volume: f32, entry_width: f32) { let id = self.entries.len() as u32 + 1; let bearing = (id as f32 - 1.0) * 90.0; self.entries.push(RoundaboutEntry { approach_volume, entry_width, entry_radius: 15.0, flare_length: 30.0, inscribed_diameter: self.inscribed_diameter, entry_id: id, bearing_deg: bearing, lane_count: 1, entry_width_m: entry_width, flare_length_m: 30.0, approach_speed_kph: 40.0, design_flow_vph: approach_volume as u32, pedestrian_crossing: true }); }
7176 pub fn entry_capacity(&self, entry: &RoundaboutEntry) -> f32 { 1380.0 * entry.lane_count as f32 * (1.0 - 0.1 * (entry.entry_width_m - 3.6) / 3.6) }
7177 pub fn generate_geometry(&self, center: Vec3) -> Vec<Vec3> { let r = self.inscribed_diameter / 2.0; (0..=64).map(|i| { let a = i as f32 * std::f32::consts::TAU / 64.0; Vec3::new(center.x + r * a.cos(), center.y, center.z + r * a.sin()) }).collect() }
7178}
7179
7180impl BarrierSystem {
7181 pub fn new(design_speed_kmh: f32) -> Self { Self { sections: Vec::new(), clear_zone_width: 3.0 + design_speed_kmh * 0.05, design_speed_kmh } }
7182 pub fn auto_place_barriers(&mut self, hazards: &[(f32, f32, f32)]) { for &(station, offset, length) in hazards { if offset < self.clear_zone_width { self.sections.push(GuardrailSection { barrier_type: BarrierType::WBeam, start_station: station, end_station: station + length, side: 1, height_mm: 685.0, post_spacing_m: 2.0, terminal_type: "ET-Plus".into() }); } } }
7183}
7184
7185impl GuardrailSection {
7186 pub fn post_count(&self) -> u32 { ((self.end_station - self.start_station) / self.post_spacing_m).ceil() as u32 + 1 }
7187}
7188
7189impl SignInventory {
7190 pub fn new() -> Self { Self { signs: Vec::new(), delineators: Vec::new(), mile_markers: Vec::new() } }
7191 pub fn add_sign(&mut self, sign: RoadSign) { self.signs.push(sign); }
7192 pub fn auto_place_delineators(&mut self, road_length: f32, spacing: f32) { let mut s = 0.0; while s <= road_length { self.delineators.push((s, 1)); self.delineators.push((s, -1)); s += spacing; } }
7193 pub fn auto_place_mile_markers(&mut self, road_length: f32) { let mut mile = 0u32; let mut s = 0.0f32; while s <= road_length { self.mile_markers.push((s, mile)); mile += 1; s += 1609.34; } }
7194}
7195
7196impl RoadSign {
7197 pub fn speed_limit(speed_kph: f32, side: i32, station: u32) -> Self { Self { position: Vec3::ZERO, facing: Vec3::Z, sign_type: RoadSignType::SpeedLimit(speed_kph as u32), post_height: 2.0, code: String::from("R2-1"), text: format!("SPEED LIMIT {}", speed_kph as u32), station: station as f32, side, height_m: 2.4, panel_size: Vec2::new(0.6, 0.75) } }
7198 pub fn stop(station_m: f32, side: i32) -> Self { Self { position: Vec3::ZERO, facing: Vec3::Z, sign_type: RoadSignType::Stop, post_height: 2.0, code: String::from("R1-1"), text: String::from("STOP"), station: station_m, side, height_m: 2.4, panel_size: Vec2::new(0.75, 0.75) } }
7199}
7200
7201impl RoadLightingSystem {
7202 pub fn new(spacing_m: f32) -> Self { Self { fixtures: Vec::new(), spacing_m } }
7203 pub fn auto_place(&mut self, road_length: f32) { let mut s = 0.0f32; while s <= road_length { self.fixtures.push(LightingFixture { station: s, side: 1, pole_height_m: 10.0, lamp_lumens: 22000.0 }); s += self.spacing_m; } }
7204}
7205
7206impl NoiseBarrier {
7207 pub fn concrete(start: f32, end: f32, side: i32, height_m: f32) -> Self { Self { start_station: start, end_station: end, height_m, side, insertion_loss_db: 5.0 + height_m * 2.0 } }
7208}
7209
7210impl NoiseAnalysis {
7211 pub fn new(source_level_db: f32, receptor_distance_m: f32) -> Self { Self { barriers: Vec::new(), source_level_db, receptor_distance_m } }
7212 pub fn receptor_level_db(&self) -> f32 { let d = 20.0 * (self.receptor_distance_m / 1.0).log10(); let b: f32 = self.barriers.iter().map(|b| b.insertion_loss_db).sum(); (self.source_level_db - d - b).max(0.0) }
7213}
7214
7215impl OriginDestinationMatrix {
7216 pub fn new(zones: Vec<String>) -> Self { let n = zones.len(); Self { zones, matrix: vec![vec![0.0; n]; n] } }
7217 pub fn set(&mut self, from: usize, to: usize, trips: f32) { if from < self.matrix.len() && to < self.matrix[from].len() { self.matrix[from][to] = trips; } }
7218 pub fn total_trips(&self) -> f32 { self.matrix.iter().flat_map(|r| r.iter()).sum() }
7219}
7220
7221impl PavementConditionIndex {
7222 pub fn new(sample_area: f32) -> Self { Self { pci_value: 100.0, distress_types: Vec::new(), sample_unit_area: sample_area } }
7223 pub fn add_distress(&mut self, distress_type: &str, quantity: f32, severity: f32) { self.distress_types.push((distress_type.to_string(), quantity, severity)); }
7224 pub fn calculate_pci(&mut self) { let d: f32 = self.distress_types.iter().map(|(_, q, s)| q / self.sample_unit_area * 100.0 * s * 2.0).sum(); self.pci_value = (100.0 - d * 0.5).clamp(0.0, 100.0); }
7225 pub fn condition_category(&self) -> &str { match self.pci_value { v if v >= 85.0 => "Good", v if v >= 70.0 => "Satisfactory", v if v >= 55.0 => "Fair", v if v >= 40.0 => "Poor", _ => "Very Poor" } }
7226 pub fn recommended_treatment(&self) -> &str { match self.pci_value { v if v >= 70.0 => "Routine Maintenance", v if v >= 55.0 => "Preventive Maintenance", v if v >= 40.0 => "Rehabilitation", _ => "Reconstruction" } }
7227}
7228
7229impl AssetRecord {
7230 pub fn new(asset_id: u32, asset_type: &str, station: f32, installation_year: u32, replacement_cost: f32) -> Self { Self { asset_id, asset_type: asset_type.to_string(), station, installation_year, condition_score: 100.0, replacement_cost, remaining_life_years: 20.0, maintenance_history: Vec::new() } }
7231 pub fn update_condition(&mut self, current_year: u32) { let age = (current_year - self.installation_year) as f32; self.condition_score = (100.0 - age * 3.0).max(0.0); self.remaining_life_years = (20.0 - age).max(0.0); }
7232 pub fn add_maintenance(&mut self, year: u32, treatment: &str, cost: f32) { self.maintenance_history.push((year, treatment.to_string(), cost)); }
7233}
7234
7235impl AssetManagementSystem {
7236 pub fn new(annual_budget: f32, current_year: u32) -> Self { Self { assets: Vec::new(), annual_budget, current_year } }
7237 pub fn add_asset(&mut self, asset: AssetRecord) { self.assets.push(asset); }
7238 pub fn network_condition_index(&self) -> f32 { if self.assets.is_empty() { return 100.0; } self.assets.iter().map(|a| a.condition_score).sum::<f32>() / self.assets.len() as f32 }
7239 pub fn budget_allocation(&self) -> Vec<(u32, f32)> { self.assets.iter().map(|a| (a.asset_id, (100.0 - a.condition_score) * self.annual_budget / 100.0)).collect() }
7240}
7241
7242impl CriticalPathMethod {
7243 pub fn standard_road_schedule() -> Self {
7244 let activities = vec![
7245 ConstructionActivity { id: 1, name: "Survey & Design".into(), duration_days: 60, predecessors: Vec::new(), resources: HashMap::new(), cost: 500_000.0, early_start: 0, early_finish: 60, late_start: 0, late_finish: 60, float: 0 },
7246 ConstructionActivity { id: 2, name: "Earthwork".into(), duration_days: 90, predecessors: vec![1], resources: HashMap::new(), cost: 1_500_000.0, early_start: 60, early_finish: 150, late_start: 60, late_finish: 150, float: 0 },
7247 ConstructionActivity { id: 3, name: "Paving".into(), duration_days: 60, predecessors: vec![2], resources: HashMap::new(), cost: 2_000_000.0, early_start: 150, early_finish: 210, late_start: 150, late_finish: 210, float: 0 },
7248 ];
7249 Self { activities }
7250 }
7251 pub fn critical_path(&self) -> Vec<&ConstructionActivity> { self.activities.iter().filter(|a| a.float == 0).collect() }
7252 pub fn project_duration(&self) -> u32 { self.activities.iter().map(|a| a.early_finish).max().unwrap_or(0) }
7253 pub fn total_cost(&self) -> f32 { self.activities.iter().map(|a| a.cost).sum() }
7254}
7255
7256impl UtilityConflictDetector {
7257 pub fn new() -> Self { Self { utilities: Vec::new(), conflicts: Vec::new() } }
7258 pub fn add_utility(&mut self, line: UtilityLine) { self.utilities.push(line); }
7259 pub fn detect_conflicts(&mut self, road_pts: &[Vec3], tolerance: f32) { for util in &self.utilities { for rp in road_pts { for up in &util.polyline { if (*rp - *up).length() < tolerance { self.conflicts.push(UtilityConflict { utility_id: util.id, conflict_station: rp.x, conflict_type: format!("proximity_{}", util.utility_type), relocation_cost: util.diameter_mm * 10.0, criticality: 1 }); break; } } } } }
7260 pub fn total_relocation_cost(&self) -> f32 { self.conflicts.iter().map(|c| c.relocation_cost).sum() }
7261}
7262
7263impl UtilityLine {
7264 pub fn water_main(id: u32, depth_m: f32, polyline: Vec<Vec3>) -> Self { Self { id, utility_type: "water_main".into(), depth_m, polyline, diameter_mm: 300.0 } }
7265}
7266
7267impl HydrologicBasin {
7268 pub fn new(area_ha: f32, land_use: &str) -> Self {
7269 let c = match land_use { "commercial" => 0.85, "suburban" => 0.40, "rural" => 0.25, _ => 0.50 };
7270 Self { area_ha, runoff_coefficient: c, tc_minutes: 10.0 + area_ha.sqrt() * 0.5, land_use: land_use.to_string() }
7271 }
7272 pub fn idf_intensity(return_period_yr: f32, tc_minutes: f32) -> f32 { 200.0 * return_period_yr.powf(0.3) / (tc_minutes + 20.0).powf(0.8) }
7273 pub fn peak_discharge_rational(&self, intensity_mm_hr: f32) -> f32 { self.runoff_coefficient * intensity_mm_hr * self.area_ha / 360.0 }
7274}
7275
7276impl CulvertDesign {
7277 pub fn new(diameter_mm: f32, length_m: f32, slope: f32) -> Self { Self { diameter_mm, length_m, slope, manning_n: 0.013 } }
7278 pub fn size_for_discharge(discharge_m3s: f32, slope: f32) -> f32 { let d = (discharge_m3s * 0.013 / slope.sqrt().max(0.0001)).powf(3.0/8.0) * 1000.0; for &s in &[300.0f32, 450.0, 600.0, 750.0, 900.0, 1050.0, 1200.0] { if s >= d.max(300.0) { return s; } } 1800.0 }
7279 pub fn full_flow_capacity(&self) -> f32 { let r = (self.diameter_mm / 1000.0) / 4.0; let a = std::f32::consts::PI * (self.diameter_mm / 2000.0).powi(2); a / self.manning_n * r.powf(2.0/3.0) * self.slope.max(0.0001).sqrt() }
7280}
7281
7282impl SpeedZoneManager {
7283 pub fn new(_default_speed_kph: u32) -> Self { Self { zones: Vec::new(), next_id: 1 } }
7284 pub fn add_zone(&mut self, zone: SpeedZone) { self.zones.push(zone); }
7285 pub fn speed_at_station(&self, station_m: f32, default_speed: u32) -> u32 { for zone in &self.zones { if station_m >= zone.start_station && station_m <= zone.end_station { return zone.posted_speed_kmh; } } default_speed }
7286}
7287
7288impl SpeedZone {
7289 pub fn school_zone(id: u32, start: f32, end: f32) -> Self { Self { id, center: Vec3::new((start+end)/2.0, 0.0, 0.0), radius: (end-start)/2.0, speed_limit_kmh: 30.0, zone_type: SpeedZoneType::School, start_station: start, end_station: end, posted_speed_kmh: 30 } }
7290}
7291
7292impl RoadEmissionsModel {
7293 pub fn new(segment_length_km: f32) -> Self { Self { factors: vec![VehicleEmissionsFactor { vehicle_class: "passenger_car".into(), co2_g_per_km: 180.0, fuel_l_per_100km: 8.0 }], traffic_volumes: HashMap::new(), segment_length_km } }
7294 pub fn set_volume(&mut self, vehicle_class: &str, volume: f32) { self.traffic_volumes.insert(vehicle_class.to_string(), volume); }
7295 pub fn daily_co2_kg(&self) -> f32 { self.factors.iter().map(|f| self.traffic_volumes.get(&f.vehicle_class).copied().unwrap_or(0.0) * f.co2_g_per_km * self.segment_length_km / 1000.0).sum() }
7296 pub fn annual_co2_tonnes(&self) -> f32 { self.daily_co2_kg() * 365.0 / 1000.0 }
7297}
7298
7299impl SuperelevationTable {
7300 pub fn for_rural_highway(design_speed_kph: f32) -> Self { Self { design_speed_kph, max_superelevation: 0.10, table: vec![(200.0f32, 0.10), (300.0, 0.08), (500.0, 0.06), (800.0, 0.04), (1200.0, 0.02)] } }
7301 pub fn required_superelevation(&self, radius_m: f32) -> f32 { for &(r, e) in &self.table { if radius_m <= r { return e; } } 0.0 }
7302 pub fn transition_length_m(&self, superelevation: f32, lane_width_m: f32) -> f32 { superelevation * lane_width_m * self.design_speed_kph / 3.6 * 2.0 }
7303}
7304
7305impl PavementManagementSystem {
7306 pub fn new(budget: f32) -> Self { let mut costs = HashMap::new(); costs.insert("crack_seal", 3.0f32); costs.insert("thin_overlay", 15.0f32); costs.insert("mill_and_fill", 35.0f32); costs.insert("reconstruction", 120.0f32); Self { sample_units: Vec::new(), annual_budget: budget, treatment_unit_costs: costs } }
7307 pub fn add_unit(&mut self, unit: PavementSampleUnit) { self.sample_units.push(unit); }
7308 pub fn network_pci(&self) -> f32 { if self.sample_units.is_empty() { return 100.0; } let ta: f32 = self.sample_units.iter().map(|u| u.area_m2).sum(); if ta <= 0.0 { return 100.0; } self.sample_units.iter().map(|u| u.compute_pci() * u.area_m2).sum::<f32>() / ta }
7309 pub fn prioritized_treatment_list(&self) -> Vec<(u32, &'static str, f32)> { let mut list: Vec<_> = self.sample_units.iter().map(|u| { let t = u.recommended_treatment(); let c = *self.treatment_unit_costs.get(t).unwrap_or(&0.0) * u.area_m2; (u.unit_id, t, c) }).collect(); list.sort_by_key(|x| x.0); list }
7310}
7311
7312impl PavementSampleUnit {
7313 pub fn new(unit_id: u32, area_m2: f32) -> Self { Self { unit_id, area_m2, distresses: Vec::new(), last_survey_year: 2024 } }
7314 pub fn add_distress(&mut self, obs: DistressObservation) { self.distresses.push(obs); }
7315 pub fn compute_pci(&self) -> f32 { (100.0 - self.distresses.iter().map(|d| d.density_percent * d.severity as f32 * 2.0).sum::<f32>()).clamp(0.0, 100.0) }
7316 pub fn condition_rating(&self) -> &'static str { let p = self.compute_pci(); if p >= 70.0 { "Good" } else if p >= 40.0 { "Fair" } else { "Poor" } }
7317 pub fn recommended_treatment(&self) -> &'static str { let p = self.compute_pci(); if p >= 70.0 { "crack_seal" } else if p >= 55.0 { "thin_overlay" } else if p >= 40.0 { "mill_and_fill" } else { "reconstruction" } }
7318 pub fn predicted_pci(&self, years: u32) -> f32 { (self.compute_pci() - years as f32 * 2.5).max(0.0) }
7319}
7320
7321impl EnvironmentalMonitoringProgram {
7322 pub fn new() -> Self { Self { air_stations: Vec::new(), noise_stations: Vec::new(), monitoring_frequency_days: 30 } }
7323 pub fn add_air_station(&mut self, station: AirQualityMonitor) { self.air_stations.push(station); }
7324 pub fn add_noise_station(&mut self, station: RoadNoiseMonitor) { self.noise_stations.push(station); }
7325 pub fn naaqs_violations(&self) -> usize { self.air_stations.iter().filter(|s| s.exceeds_naaqs_pm25() || s.exceeds_naaqs_pm10() || s.exceeds_naaqs_co()).count() }
7326 pub fn noise_exceedances(&self) -> usize { self.noise_stations.iter().filter(|s| s.exceeds_abatement_criteria()).count() }
7327 pub fn summary_report(&self) -> HashMap<&'static str, String> { let mut r = HashMap::new(); r.insert("air_stations", self.air_stations.len().to_string()); r.insert("noise_stations", self.noise_stations.len().to_string()); r.insert("naaqs_violations", self.naaqs_violations().to_string()); r.insert("noise_exceedances", self.noise_exceedances().to_string()); r }
7328}
7329
7330impl RoadNoiseMonitor {
7331 pub fn new(monitor_id: u32, distance_m: f32, l_eq: f32, fhwa_criteria: f32) -> Self { Self { monitor_id, distance_from_road_m: distance_m, l_eq_dba: l_eq, l_10_dba: l_eq + 3.0, l_90_dba: l_eq - 15.0, peak_hour_db: l_eq + 5.0, fhwa_noise_abatement_criteria: fhwa_criteria } }
7332 pub fn exceeds_abatement_criteria(&self) -> bool { self.l_eq_dba >= self.fhwa_noise_abatement_criteria }
7333 pub fn qualifies_for_barrier(&self, background_db: f32) -> bool { self.l_eq_dba - background_db >= 5.0 || self.exceeds_abatement_criteria() }
7334 pub fn estimated_barrier_height_m(&self) -> f32 { ((self.l_eq_dba - self.fhwa_noise_abatement_criteria + 5.0) / 2.0).max(1.0) }
7335}
7336
7337impl RoadProjectSummary {
7338 pub fn new(name: &str, length_km: f32, lanes: u32, design_speed: f32) -> Self { Self { project_name: name.to_string(), total_length_km: length_km, total_lanes: lanes, design_speed_kph: design_speed, terrain_type: "rolling".into(), estimated_construction_cost_usd: length_km * lanes as f32 * 2_500_000.0, construction_duration_months: (length_km * 4.0) as u32, design_year: 2024, opening_year: 2026, design_horizon_year: 2044, peak_hour_volume: 2000, level_of_service: 'C' } }
7339 pub fn cost_per_lane_km(&self) -> f32 { self.estimated_construction_cost_usd / (self.total_lanes as f32 * self.total_length_km).max(0.001) }
7340 pub fn is_feasible(&self) -> bool { self.total_length_km > 0.0 && self.total_lanes > 0 }
7341 pub fn export_csv_row(&self) -> String { format!("{},{:.2},{},{:.0}", self.project_name, self.total_length_km, self.total_lanes, self.estimated_construction_cost_usd) }
7342 pub fn export_json(&self) -> String { format!("{{\"name\":\"{}\",\"length_km\":{:.2}}}", self.project_name, self.total_length_km) }
7343}
7344
7345impl RoadDesignQualityCheckList {
7346 pub fn standard_road_checklist(_design_speed: f32, _has_shoulders: bool, _has_lighting: bool) -> Self { Self { items: vec![("Horizontal alignment OK".into(), true), ("Vertical alignment OK".into(), true), ("Sight distances adequate".into(), true), ("Cross section OK".into(), true), ("Drainage designed".into(), true)] } }
7347 pub fn overall_pass(&self) -> bool { self.items.iter().all(|(_, p)| *p) }
7348 pub fn failed_count(&self) -> usize { self.items.iter().filter(|(_, p)| !*p).count() }
7349 pub fn completion_percent(&self) -> f32 { if self.items.is_empty() { 100.0 } else { self.items.iter().filter(|(_, p)| *p).count() as f32 / self.items.len() as f32 * 100.0 } }
7350}