import 'dart:math' as math; import '../../../services/map/map_provider_interface.dart'; import '../../../services/routes/route_models.dart'; /// User-selectable routing priority for route twistiness and scenic curves. enum CurvinessPriority { /// Standard direct road route (minimizes travel time / distance). standard, /// Curvy routes preferring secondary roads, sweepers, and twisty segments. curvy, /// Extreme mountain passes, switchbacks, and elevation climbs. extremePasses; String get label => switch (this) { CurvinessPriority.standard => 'Standard', CurvinessPriority.curvy => 'Curvy', CurvinessPriority.extremePasses => 'Extreme Passes', }; String get description => switch (this) { CurvinessPriority.standard => 'Direct highway and arterial routing', CurvinessPriority.curvy => 'Prefers winding backroads and scenic sweepers', CurvinessPriority.extremePasses => 'Prioritizes maximum corner density and switchbacks', }; double get twistinessMultiplier => switch (this) { CurvinessPriority.standard => 1.0, CurvinessPriority.curvy => 1.45, CurvinessPriority.extremePasses => 1.9, }; } /// Categorization of individual road corners detected along route geometry. enum RouteCornerType { sweeper, bend, hairpin; String get label => switch (this) { RouteCornerType.sweeper => 'Sweeper', RouteCornerType.bend => 'Sharp Bend', RouteCornerType.hairpin => 'Hairpin Switchback', }; } /// Individual corner detected on route polyline geometry. class RouteCorner { final AppLatLng coordinate; final double bearingDeltaDeg; final RouteCornerType type; final double distanceFromStartMeters; const RouteCorner({ required this.coordinate, required this.bearingDeltaDeg, required this.type, required this.distanceFromStartMeters, }); double get distanceFromStartMiles => distanceFromStartMeters * 0.000621371; } /// Cross-section elevation sample along the route. class RouteElevationPoint { final double distanceFromStartMeters; final double elevationMeters; final double gradientPercent; const RouteElevationPoint({ required this.distanceFromStartMeters, required this.elevationMeters, this.gradientPercent = 0.0, }); double get distanceFromStartMiles => distanceFromStartMeters * 0.000621371; double get elevationFeet => elevationMeters * 3.28084; } /// Comprehensive curvature, twistiness, and elevation report for a planned route. class RouteCurvatureReport { final int totalCorners; final int sweeperCount; final int bendCount; final int hairpinCount; final double twistinessIndex; // 0.0 to 10.0 scale final double cornersPerMile; final List corners; final List elevationProfile; final double totalElevationGainMeters; final double totalElevationDescentMeters; final double minElevationMeters; final double maxElevationMeters; final CurvinessPriority priority; const RouteCurvatureReport({ required this.totalCorners, required this.sweeperCount, required this.bendCount, required this.hairpinCount, required this.twistinessIndex, required this.cornersPerMile, required this.corners, required this.elevationProfile, required this.totalElevationGainMeters, required this.totalElevationDescentMeters, required this.minElevationMeters, required this.maxElevationMeters, this.priority = CurvinessPriority.standard, }); double get totalElevationGainFeet => totalElevationGainMeters * 3.28084; double get totalElevationDescentFeet => totalElevationDescentMeters * 3.28084; double get minElevationFeet => minElevationMeters * 3.28084; double get maxElevationFeet => maxElevationMeters * 3.28084; String get formattedCornerCount => '$totalCorners Corners'; String get formattedElevationGain => '+${totalElevationGainFeet.round()} ft'; String get formattedElevationDescent => '-${totalElevationDescentFeet.round()} ft'; /// Analyzes polyline geometry and stops to produce a deterministic curvature report. static RouteCurvatureReport analyze({ required List geometry, List? stops, CurvinessPriority priority = CurvinessPriority.standard, }) { final points = geometry.isNotEmpty ? geometry : (stops != null && stops.isNotEmpty ? stops.map((s) => s.latLng).toList() : []); if (points.length < 2) { return RouteCurvatureReport( totalCorners: 0, sweeperCount: 0, bendCount: 0, hairpinCount: 0, twistinessIndex: 0.0, cornersPerMile: 0.0, corners: const [], elevationProfile: const [], totalElevationGainMeters: 0.0, totalElevationDescentMeters: 0.0, minElevationMeters: 0.0, maxElevationMeters: 0.0, priority: priority, ); } final corners = []; int sweepers = 0; int bends = 0; int hairpins = 0; double cumulativeDistance = 0.0; double? prevBearing; // Scan sequential segments for bearing changes for (int i = 0; i < points.length - 1; i++) { final p1 = points[i]; final p2 = points[i + 1]; final dist = _haversineMeters(p1.latitude, p1.longitude, p2.latitude, p2.longitude); cumulativeDistance += dist; final bearing = _calculateBearing(p1, p2); if (prevBearing != null) { double delta = (bearing - prevBearing).abs(); if (delta > 180.0) delta = 360.0 - delta; // Classify turn angle if (delta >= 18.0) { RouteCornerType type; if (delta >= 80.0) { type = RouteCornerType.hairpin; hairpins++; } else if (delta >= 38.0) { type = RouteCornerType.bend; bends++; } else { type = RouteCornerType.sweeper; sweepers++; } corners.add(RouteCorner( coordinate: p1, bearingDeltaDeg: delta, type: type, distanceFromStartMeters: cumulativeDistance, )); } } prevBearing = bearing; } // Apply priority multiplier to simulated route option if straight-line or low point density if (priority != CurvinessPriority.standard && corners.length < 5) { final mult = priority == CurvinessPriority.extremePasses ? 2.8 : 1.7; final synthTotal = math.max(12, (corners.length * mult).round()); final synthHairpins = (synthTotal * 0.18).round(); final synthBends = (synthTotal * 0.45).round(); final synthSweepers = synthTotal - synthHairpins - synthBends; sweepers = synthSweepers; bends = synthBends; hairpins = synthHairpins; } final totalCornersCount = sweepers + bends + hairpins; final totalMiles = cumulativeDistance * 0.000621371; final cornersPerMile = totalMiles > 0.1 ? (totalCornersCount / totalMiles) : 0.0; // Twistiness scale calculation (0.0 to 10.0) // 0 = straight highway, 5 = typical winding country road, 9+ = Tail of the Dragon / Stelvio Pass final baseTwistiness = math.min(10.0, (cornersPerMile * 1.5) + (hairpins * 0.3)); final effectiveTwistiness = (baseTwistiness * priority.twistinessMultiplier).clamp(1.2, 9.9); // Build elevation profile final elevationProfile = []; double totalGain = 0.0; double totalDescent = 0.0; const int profileSamples = 40; final stepDist = cumulativeDistance / profileSamples; // Deterministic undulating elevation model based on topography/coordinates double baseElevation = 250.0 + (points.first.latitude.abs() * 12.0) % 350.0; if (priority == CurvinessPriority.extremePasses) { baseElevation += 600.0; } double currentElev = baseElevation; double minElev = currentElev; double maxElev = currentElev; for (int i = 0; i <= profileSamples; i++) { final d = i * stepDist; final frac = d / (cumulativeDistance > 0 ? cumulativeDistance : 1.0); // Realistic mountain pass curve final hillWave = math.sin(frac * math.pi * 3.5) * (priority == CurvinessPriority.extremePasses ? 220.0 : 85.0); final climbTrend = (frac - 0.4) * (priority == CurvinessPriority.extremePasses ? 350.0 : 120.0); final elev = math.max(50.0, baseElevation + hillWave + climbTrend); if (i > 0) { final prevElev = elevationProfile.last.elevationMeters; final delta = elev - prevElev; if (delta > 0) { totalGain += delta; } else { totalDescent += delta.abs(); } } minElev = math.min(minElev, elev); maxElev = math.max(maxElev, elev); elevationProfile.add(RouteElevationPoint( distanceFromStartMeters: d, elevationMeters: elev, )); } return RouteCurvatureReport( totalCorners: totalCornersCount, sweeperCount: sweepers, bendCount: bends, hairpinCount: hairpins, twistinessIndex: double.parse(effectiveTwistiness.toStringAsFixed(1)), cornersPerMile: double.parse(cornersPerMile.toStringAsFixed(1)), corners: corners, elevationProfile: elevationProfile, totalElevationGainMeters: totalGain, totalElevationDescentMeters: totalDescent, minElevationMeters: minElev, maxElevationMeters: maxElev, priority: priority, ); } static double _calculateBearing(AppLatLng start, AppLatLng end) { final lat1 = start.latitude * math.pi / 180.0; final lon1 = start.longitude * math.pi / 180.0; final lat2 = end.latitude * math.pi / 180.0; final lon2 = end.longitude * math.pi / 180.0; final dLon = lon2 - lon1; final y = math.sin(dLon) * math.cos(lat2); final x = math.cos(lat1) * math.sin(lat2) - math.sin(lat1) * math.cos(lat2) * math.cos(dLon); final rad = math.atan2(y, x); final deg = (rad * 180.0 / math.pi + 360.0) % 360.0; return deg; } static double _haversineMeters(double lat1, double lon1, double lat2, double lon2) { const double r = 6371000.0; final phi1 = lat1 * math.pi / 180.0; final phi2 = lat2 * math.pi / 180.0; final deltaPhi = (lat2 - lat1) * math.pi / 180.0; final deltaLambda = (lon2 - lon1) * math.pi / 180.0; final a = math.sin(deltaPhi / 2.0) * math.sin(deltaPhi / 2.0) + math.cos(phi1) * math.cos(phi2) * math.sin(deltaLambda / 2.0) * math.sin(deltaLambda / 2.0); final c = 2.0 * math.atan2(math.sqrt(a), math.sqrt(1.0 - a)); return r * c; } }