Skip to main content

proof_engine/editor/
terrain_road_tool.rs

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
6// ============================================================
7// CONSTANTS
8// ============================================================
9
10const 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// ============================================================
45// ROAD TYPES
46// ============================================================
47
48#[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// ============================================================
165// INTERSECTION GENERATOR
166// ============================================================
167
168#[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    // Simple chord-length parameterization cubic bezier fit
563    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    // Compute chord lengths for parameterization
568    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    // Least squares tangent estimation
575    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    // Simplified: use Catmull-Rom tangent at ends
587    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
696// ============================================================
697// ROAD PATH SMOOTHER
698// ============================================================
699
700pub 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// ============================================================
770// ROAD TEXTURE ATLAS
771// ============================================================
772
773#[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// ============================================================
846// ROAD COST ESTIMATE
847// ============================================================
848
849#[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// ============================================================
892// ROAD LIGHT POSTS
893// ============================================================
894
895#[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// ============================================================
928// ROAD SIGN PLACER
929// ============================================================
930
931#[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// ============================================================
985// GUARD RAIL GENERATOR
986// ============================================================
987
988#[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
1025// ============================================================
1026// TERRAIN ROAD TOOL TESTS (inline)
1027// ============================================================
1028
1029pub fn run_all_tests() -> bool {
1030    let mut all_ok = true;
1031
1032    // Test spline
1033    {
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    // Test terrain
1044    {
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    // Test Dijkstra
1053    {
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    // Test LWR
1067    {
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    // Test erosion
1075    {
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        // Some potholes should have appeared
1083        let total_potholes: f32 = erosion.pothole_grid.iter().sum();
1084        // (stochastic, just check it runs without panic)
1085    }
1086
1087    all_ok
1088}
1089
1090// ============================================================
1091// MAIN TOOL CONVENIENCE
1092// ============================================================
1093
1094pub 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// ============================================================
1289// ROAD ASSET REGISTRY
1290// ============================================================
1291
1292#[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            // Binary search for precision
1385            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// ============================================================
1400// ROAD CAMERA PATH
1401// ============================================================
1402
1403#[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)>, // (segment_id, start_t, end_t)
1418    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// ============================================================
1512// GRADE OPTIMIZER
1513// ============================================================
1514
1515
1516#[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
1673// ============================================================
1674// ROAD TOOL COMPREHENSIVE INTEGRATION TEST
1675// ============================================================
1676
1677pub fn run_extended_road_tool_tests() {
1678    // Grade optimizer
1679    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    // Cross section
1689    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    // Pavement
1695    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    // Intersection capacity
1700    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    // Roundabout
1711    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    // Barrier
1717    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    // Signs
1723    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    // Lighting
1732    let mut lighting = RoadLightingSystem::new(40.0);
1733    lighting.auto_place(500.0);
1734    assert!(!lighting.fixtures.is_empty());
1735
1736    // Noise
1737    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    // OD matrix
1742    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    // PCI
1747    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    // Asset management
1755    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    // CPM
1764    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    // Utility conflicts
1770    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    // Hydrology
1777    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    // Speed zones
1786    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    // Emissions
1792    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    // Module info
1798    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// ============================================================
1804// SECTION: Road Geometry — Horizontal Curve Superelevation
1805// ============================================================
1806
1807#[derive(Debug, Clone)]
1808pub struct SuperelevationTable {
1809    pub design_speed_kph: f32,
1810    pub max_superelevation: f32,
1811    /// (radius_m, superelevation_rate) pairs
1812    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, // 1=Low, 2=Medium, 3=High
1880}
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>, // $/m2
1893}
1894
1895#[derive(Debug, Clone, Default)]
1896pub struct SkidResistanceMeasurement {
1897    pub station_m: f32,
1898    pub skid_number: f32, // SN at 64 km/h
1899    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    // Horizontal curve
1912    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    // Superelevation table
1921    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    // Vertical curve
1926    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    // Network equilibrium
1936    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    // Pavement management
1945    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    // Skid resistance
1965    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    // Verify all major systems are present and functional
1979    let _ = road_tool_module_info();
1980}
1981
1982// ============================================================
1983// SECTION: Road Markings & Delineation System
1984// ============================================================
1985
1986#[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, // millicandela/lux/m²
2002    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, // 0-100
2044    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    // Road markings
2110    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    // Add degraded marking
2115    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    // Asset registry
2124    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    // Asset condition
2138    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    // Full pipeline check
2148    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// ============================================================
2168// SECTION: Road Environmental Monitoring
2169// ============================================================
2170
2171#[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,      // equivalent continuous sound level
2186    pub l_10_dba: f32,      // exceeded 10% of time
2187    pub l_90_dba: f32,      // background noise
2188    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// ============================================================
2228// SECTION: Road Tool Export & Reporting
2229// ============================================================
2230
2231#[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
2266/// Top-level entry point for all terrain road tool tests.
2267pub 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
2277// ============================================================
2278// SECTION: Constants Summary
2279// ============================================================
2280
2281/// Maximum number of sample units in a pavement management system.
2282pub const MAX_PMS_SAMPLE_UNITS: usize = 10_000;
2283/// Default retroreflectivity minimum for white markings (mcd/lux/m²).
2284pub const DEFAULT_WHITE_MARKING_MIN_MCD: f32 = 100.0;
2285/// Default retroreflectivity minimum for yellow markings (mcd/lux/m²).
2286pub const DEFAULT_YELLOW_MARKING_MIN_MCD: f32 = 75.0;
2287/// FHWA Activity Category B noise limit (dBA).
2288pub const FHWA_NOISE_LIMIT_CAT_B_DBA: f32 = 67.0;
2289/// FHWA Activity Category C noise limit (residential) (dBA).
2290pub const FHWA_NOISE_LIMIT_CAT_C_DBA: f32 = 67.0;
2291/// NAAQS PM2.5 annual mean standard (μg/m³).
2292pub const NAAQS_PM25_ANNUAL_UG_M3: f32 = 12.0;
2293/// NAAQS CO 8-hour standard (ppb).
2294pub const NAAQS_CO_8HR_PPB: f32 = 9_000.0;
2295/// Standard asphalt overlay unit cost (USD/m²).
2296pub const ASPHALT_OVERLAY_COST_USD_M2: f32 = 35.0;
2297/// Standard pavement reconstruction unit cost (USD/m²).
2298pub const PAVEMENT_RECONSTRUCTION_COST_USD_M2: f32 = 200.0;
2299/// Maximum design speed for rural highways (kph).
2300pub const MAX_RURAL_HIGHWAY_DESIGN_SPEED_KPH: f32 = 130.0;
2301/// Minimum superelevation for tangent section.
2302pub const MIN_SUPERELEVATION_TANGENT: f32 = 0.02;
2303/// Maximum superelevation for rural highways (AASHTO).
2304pub const MAX_SUPERELEVATION_RURAL: f32 = 0.08;
2305/// Gravity acceleration for road engineering calculations (m/s²).
2306pub const GRAVITY_M_S2: f32 = 9.807;
2307/// Speed of sound for noise calculations (m/s).
2308pub const SPEED_OF_SOUND_M_S: f32 = 343.0;
2309/// Minimum K value for crest vertical curves at 80 kph.
2310pub const MIN_K_CREST_80KPH: f32 = 43.0;
2311/// Minimum K value for sag vertical curves at 80 kph.
2312pub const MIN_K_SAG_80KPH: f32 = 30.0;
2313/// Minimum K value for crest vertical curves at 100 kph.
2314pub const MIN_K_CREST_100KPH: f32 = 84.0;
2315/// Minimum K value for sag vertical curves at 100 kph.
2316pub const MIN_K_SAG_100KPH: f32 = 45.0;
2317/// Default clear zone width for 80 kph design speed (m).
2318pub const CLEAR_ZONE_WIDTH_80KPH_M: f32 = 9.0;
2319/// Default clear zone width for 100 kph design speed (m).
2320pub const CLEAR_ZONE_WIDTH_100KPH_M: f32 = 10.0;
2321/// IRI roughness threshold for pavement smoothness (m/km).
2322pub const IRI_SMOOTH_THRESHOLD_M_KM: f32 = 2.5;
2323/// IRI roughness threshold for pavement replacement (m/km).
2324pub const IRI_REPLACE_THRESHOLD_M_KM: f32 = 6.0;
2325/// Default road roughness for new construction (IRI m/km).
2326pub 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// ============================================================
2626// SUPERELEVATION TABLE
2627// ============================================================
2628
2629#[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// ============================================================
2656// ROAD SIGN INVENTORY
2657// ============================================================
2658
2659#[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// ============================================================
2743// ROAD CONDITION SURVEY
2744// ============================================================
2745
2746#[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// ============================================================
2812// PEDESTRIAN / BICYCLE FACILITY
2813// ============================================================
2814
2815#[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,  // 0-10
2826    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// ============================================================
2901// ROAD NETWORK ANALYSIS
2902// ============================================================
2903
2904#[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,   // 1=freeway, 2=arterial, 3=collector, 4=local
2931    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)  // annualized
2949    }
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// ============================================================
3022// STREET LIGHTING INVENTORY
3023// ============================================================
3024
3025#[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; // simplified - illuminated area under cone
3057        lumens / area.max(1.0)
3058    }
3059    pub fn annual_energy_kwh(&self) -> f32 { self.wattage_w / 1000.0 * 4000.0 }  // 4000 operating hours/year
3060    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// ============================================================
3108// ROAD CRASH DATA ANALYSIS
3109// ============================================================
3110
3111#[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// ============================================================
3210// TRAFFIC CALMING
3211// ============================================================
3212
3213#[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
3283// ============================================================
3284// ADDITIONAL TEST FUNCTIONS
3285// ============================================================
3286
3287pub 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// ============================================================
3401// ROAD MAINTENANCE MANAGEMENT
3402// ============================================================
3403
3404#[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
3496// ============================================================
3497// ROAD DESIGN STANDARDS CHECKER
3498// ============================================================
3499
3500pub 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
3602// ============================================================
3603// SPEED ZONE ANALYSIS
3604// ============================================================
3605
3606pub 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        // AASHTO 2018 Green Book: SSD = V*t + V^2/(2*g*f)
3657        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        // DSD = 1.5 * SSD approximately
3666        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// ============================================================
3696// CROSS-SECTION ELEMENTS
3697// ============================================================
3698
3699#[derive(Debug, Clone)]
3700pub struct ExtLane {
3701    pub id: u32,
3702    pub lane_type: LaneType,
3703    pub width_m: f32,
3704    pub direction: i32, // +1 or -1
3705    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// ============================================================
3780// EARTHWORKS COMPUTATION
3781// ============================================================
3782
3783#[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        // Average end area method
3796        (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        // Prismatoid formula
3802        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        // Find where ordinate crosses zero after a non-zero region
3840        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
3858// ============================================================
3859// STORMWATER DRAINAGE DESIGN
3860// ============================================================
3861
3862pub 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        // Q = C * i * A / 360 (m³/s, A in ha, i in mm/hr)
3890        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        // Manning's equation: Q = (1/n) * A * R^(2/3) * S^(1/2)
3918        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        // Estimate design depth from capacity
3989        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
3996// ============================================================
3997// PAVEMENT PERFORMANCE MODEL
3998// ============================================================
3999
4000pub 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        // Simplified linear + traffic model
4029        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// ============================================================
4102// BRIDGE DESIGN OVERVIEW
4103// ============================================================
4104
4105#[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// ============================================================
4175// GEOMETRIC DESIGN PARAMETERS
4176// ============================================================
4177
4178#[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// ============================================================
4251// ENVIRONMENTAL IMPACT ASSESSMENT
4252// ============================================================
4253
4254#[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        // NAAQS 24-hr standards
4302        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// ============================================================
4362// ROAD SAFETY IMPROVEMENT PROGRAM
4363// ============================================================
4364
4365#[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// ============================================================
4407// TEST FUNCTIONS
4408// ============================================================
4409
4410#[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(&sections);
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
4515// ============================================================
4516// ROUNDABOUT DESIGN
4517// ============================================================
4518
4519pub 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        // Simplified HCM roundabout capacity
4572        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// ============================================================
4581// INTERCHANGE DESIGN
4582// ============================================================
4583
4584#[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// ============================================================
4637// SIGHT DISTANCE ANALYSIS
4638// ============================================================
4639
4640#[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
4707// ============================================================
4708// TRAFFIC SIGNAL OPTIMIZATION
4709// ============================================================
4710
4711pub 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// ============================================================
4816// ROAD INVENTORY MANAGEMENT
4817// ============================================================
4818
4819#[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 // avg ESALs per truck
4867    }
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// ============================================================
4900// FINAL TEST FUNCTIONS FOR TERRAIN ROAD
4901// ============================================================
4902
4903#[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// ============================================================
4970// ROAD ASSET CONDITION TRACKING
4971// ============================================================
4972
4973#[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// ============================================================
5094// SNOW REMOVAL PLANNING
5095// ============================================================
5096
5097#[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// ============================================================
5171// UTILITY CORRIDOR MANAGEMENT
5172// ============================================================
5173
5174#[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// ============================================================
5251// ROAD SAFETY RATING SYSTEM
5252// ============================================================
5253
5254#[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// ============================================================
5298// LEVEL OF SERVICE ANALYSIS
5299// ============================================================
5300
5301#[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// ============================================================
5396// TRAFFIC IMPACT ASSESSMENT
5397// ============================================================
5398
5399#[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// ============================================================
5493// MORE TEST FUNCTIONS
5494// ============================================================
5495
5496#[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
5583// ============================================================
5584// GEOMETRIC ROAD DESIGN - SPIRAL TRANSITIONS
5585// ============================================================
5586
5587pub 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// ============================================================
5627// ROAD SIGNAGE STANDARDS CHECKER
5628// ============================================================
5629
5630#[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// ============================================================
5670// HORIZONTAL ALIGNMENT COMPUTATION
5671// ============================================================
5672
5673#[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// ============================================================
5723// VERTICAL ALIGNMENT COMPUTATION
5724// ============================================================
5725
5726#[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// ============================================================
5768// COST ESTIMATION
5769// ============================================================
5770
5771#[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// ============================================================
5847// FINAL TEST FUNCTIONS
5848// ============================================================
5849
5850#[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// ============================================================
5909// BRIDGE INSPECTION AND RATING
5910// ============================================================
5911
5912#[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// ============================================================
5996// GEOTECHNICAL INVESTIGATION
5997// ============================================================
5998
5999#[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
6077// ============================================================
6078// PAVEMENT DESIGN (MECHANISTIC EMPIRICAL)
6079// ============================================================
6080
6081pub 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        // Simplified SN calculation
6154        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
6162// ============================================================
6163// ADDITIONAL UTILITY FUNCTIONS AND CONSTANTS
6164// ============================================================
6165
6166pub 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// ============================================================
6218// FINAL TEST BLOCK
6219// ============================================================
6220
6221#[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// ============================================================
6283// ROAD REHABILITATION ANALYSIS (terrain_road_tool additions)
6284// ============================================================
6285
6286#[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// ============================================================
6400// ADDITIONAL ROAD DESIGN ELEMENTS
6401// ============================================================
6402
6403#[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
6514// ============================================================
6515// ROAD LIGHTING DESIGN
6516// ============================================================
6517
6518pub 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
6613// ============================================================
6614// ROAD SAFETY HARDWARE
6615// ============================================================
6616
6617pub 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
6962// Final padding
6963pub 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
7029// ============================================================
7030// STUB IMPLEMENTATIONS
7031// ============================================================
7032
7033impl 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}