import 'dart:math' as math; import 'package:intaleq_maps/intaleq_maps.dart'; enum TacticalVehicleProfile { convoy, // Heavy military supply convoy (~45 km/h avg) offroad4x4, // Tactical 4x4 off-road vehicle (~75 km/h highway, capable on desert tracks) armored, // Heavy armored tanks/APCs (~35 km/h avg) rapidResponse, // Rapid intervention / light vehicle (~90 km/h) } class RoadNode { final String id; final String name; final double lat; final double lng; final int elevationM; const RoadNode({ required this.id, required this.name, required this.lat, required this.lng, this.elevationM = 800, }); } class RoadEdge { final String fromId; final String toId; final double distanceKm; final String roadType; // 'highway', 'primary', 'secondary', 'desert_track' final double baseSpeedKmH; final List intermediateCoords; final double inclinePercent; // Grade percentage (e.g. +6.5% incline, -4.2% descent) const RoadEdge({ required this.fromId, required this.toId, required this.distanceKm, required this.roadType, required this.baseSpeedKmH, this.intermediateCoords = const [], this.inclinePercent = 0.0, }); } /// مناورة حقيقية من محرك التوجيه (Valhalla) مع تعليمات عربية وأسماء شوارع فعلية class RouteManeuver { /// Numeric Valhalla maneuver type (see valhalla ManeuverType enum) final int type; final String instructionAr; final List streetNames; final double lengthKm; final LatLng location; const RouteManeuver({ required this.type, required this.instructionAr, required this.streetNames, required this.lengthKm, required this.location, }); bool get hasStreetName => streetNames.isNotEmpty; } class OfflineRoutePlan { final List polylinePoints; final double totalDistanceKm; final double estimatedDurationMinutes; final TacticalVehicleProfile profile; final List tacticalWaypoints; final bool isOffline; final int elevationGainMeters; final int elevationLossMeters; final double averageInclinePercent; final double maxInclinePercent; /// True when the route follows the real downloaded road network /// (on-device Valhalla graph) instead of a synthetic fallback path. final bool usesRealRoadNetwork; /// Real turn-by-turn maneuvers from the routing engine (empty on fallbacks). final List maneuvers; const OfflineRoutePlan({ required this.polylinePoints, required this.totalDistanceKm, required this.estimatedDurationMinutes, required this.profile, required this.tacticalWaypoints, this.isOffline = true, this.elevationGainMeters = 0, this.elevationLossMeters = 0, this.averageInclinePercent = 0.0, this.maxInclinePercent = 0.0, this.usesRealRoadNetwork = false, this.maneuvers = const [], }); String get inclineSummaryArabic { if (maxInclinePercent.abs() > 7.0) { return 'انحدار حاد / عقبة جبلية (${maxInclinePercent.abs().toStringAsFixed(1)}%) • صعود +$elevationGainMeters م / هبوط -$elevationLossMeters م'; } else if (maxInclinePercent.abs() > 3.5) { return 'انحدار معتدل (${maxInclinePercent.abs().toStringAsFixed(1)}%) • صعود +$elevationGainMeters م'; } else { return 'طريق مستوٍ (${maxInclinePercent.abs().toStringAsFixed(1)}%) • تضاريس مريحة'; } } } /// 100% On-Device Tactical Routing Engine for Jordan with Elevation & Slope Analysis class OfflineRoutingEngine { static final Map _nodes = {}; static final Map> _adjacency = {}; static bool _isInitialized = false; static void initialize() { if (_isInitialized) return; // ── Strategic Road Network Nodes Across Jordan with Elevation (DEM) ── final nodeList = const [ // ── Amman Urban Hubs & Arterial Corridors ────────────────── RoadNode(id: 'amm_center', name: 'وسط عمان / العبدلي', lat: 31.9615, lng: 35.9130, elevationM: 760), RoadNode(id: 'amm_citadel', name: 'جبل القلعة / المدرج الروماني', lat: 31.9540, lng: 35.9350, elevationM: 850), RoadNode(id: 'amm_1st', name: 'الدوار الأول / جبل عمان', lat: 31.9510, lng: 35.9220, elevationM: 850), RoadNode(id: 'amm_2nd', name: 'الدوار الثاني / جبل عمان', lat: 31.9525, lng: 35.9120, elevationM: 870), RoadNode(id: 'amm_3rd', name: 'الدوار الثالث / زهران', lat: 31.9545, lng: 35.9015, elevationM: 890), RoadNode(id: 'amm_4th', name: 'الدوار الرابع / رئاسة الوزراء', lat: 31.9560, lng: 35.8910, elevationM: 900), RoadNode(id: 'amm_5th', name: 'الدوار الخامس / وادي صقرة', lat: 31.9570, lng: 35.8810, elevationM: 910), RoadNode(id: 'amm_6th', name: 'الدوار السادس / أبراج بوابة الأردن', lat: 31.9580, lng: 35.8680, elevationM: 930), RoadNode(id: 'amm_7th', name: 'الدوار السابع / طريق المطار', lat: 31.9530, lng: 35.8580, elevationM: 920), RoadNode(id: 'amm_8th', name: 'الدوار الثامن / وادي السير', lat: 31.9515, lng: 35.8350, elevationM: 940), RoadNode(id: 'amm_abdoun', name: 'عبدون / جسر عبدون المعلق', lat: 31.9420, lng: 35.8910, elevationM: 860), RoadNode(id: 'amm_shmeisani', name: 'الشميساني / مجمع النقابات والبنك المركزي', lat: 31.9680, lng: 35.9010, elevationM: 890), RoadNode(id: 'amm_sports_city', name: 'دوار المدينة الرياضية / صرح الشهيد', lat: 31.9850, lng: 35.8980, elevationM: 920), RoadNode(id: 'amm_gardens', name: 'شارع وصفي التل (الجاردنز) / الواحة', lat: 31.9820, lng: 35.8750, elevationM: 950), RoadNode(id: 'amm_medina', name: 'شارع المدينة المنورة / مستشفى ابن الهيثم', lat: 31.9910, lng: 35.8720, elevationM: 970), RoadNode(id: 'amm_mecca', name: 'شارع مكة / مجمع الحرمين', lat: 31.9690, lng: 35.8550, elevationM: 960), RoadNode(id: 'amm_khalda', name: 'خلدا / دوار الواحة وصدا', lat: 31.9890, lng: 35.8580, elevationM: 990), RoadNode(id: 'amm_tlaa', name: 'طلاء العلي / سوق السلطان', lat: 31.9980, lng: 35.8650, elevationM: 1010), RoadNode(id: 'amm_univ', name: 'الجامعة الأردنية / البوابة الرئيسية', lat: 32.0120, lng: 35.8710, elevationM: 1005), RoadNode(id: 'amm_sweileh', name: 'صويلح / تقاطع الشمال', lat: 32.0220, lng: 35.8450, elevationM: 1030), RoadNode(id: 'amm_jubaiha', name: 'الجبيهة / الدوريات الخارجية والتعليم العالي', lat: 32.0280, lng: 35.8620, elevationM: 1040), RoadNode(id: 'amm_dabouq', name: 'دابوق / قاعدة القيادة الغربية', lat: 31.9960, lng: 35.8285, elevationM: 1045), RoadNode(id: 'amm_tabarbour', name: 'طبربور / تقاطع المشاغل والزرقاء', lat: 31.9920, lng: 35.9450, elevationM: 880), RoadNode(id: 'amm_jordan_st', name: 'شارع الأردن / تقاطع ياجوز', lat: 32.0150, lng: 35.9220, elevationM: 930), RoadNode(id: 'amm_marka', name: 'ماركا / مطار عمان المدني', lat: 31.9720, lng: 35.9910, elevationM: 770), RoadNode(id: 'amm_sahab', name: 'سحاب / مدينة الملك عبدالله الثاني الصناعية', lat: 31.8710, lng: 36.0040, elevationM: 820), RoadNode(id: 'amm_marj', name: 'مرج الحمام / تقاطع ناعور والمطار', lat: 31.8950, lng: 35.8520, elevationM: 910), RoadNode(id: 'amm_airport', name: 'مطار الملكة علياء الدولي (الجيزة)', lat: 31.7200, lng: 35.9880, elevationM: 720), // ── Desert Highway Corridor (Route 15) ─────────────────────── RoadNode(id: 'dabaa', name: 'محطة ضبعة / الطريق الصحراوي', lat: 31.5420, lng: 36.0150, elevationM: 700), RoadNode(id: 'qatrana', name: 'القطرانة / جسر القطرانة العسكري', lat: 31.2450, lng: 36.0420, elevationM: 790), RoadNode(id: 'sultani', name: 'محطة السلطاني / سد السلطاني', lat: 31.0520, lng: 35.9980, elevationM: 820), RoadNode(id: 'hasa', name: 'الحسا / مناجم الفوسفات والتقاطع', lat: 30.8200, lng: 35.9750, elevationM: 840), RoadNode(id: 'jurf', name: 'جرف الدراويش / الخط الحديدي الحجازي', lat: 30.6850, lng: 35.8650, elevationM: 930), RoadNode(id: 'husseiniya', name: 'الحسينية / البادية الجنوبية', lat: 30.4920, lng: 35.7980, elevationM: 1050), RoadNode(id: 'maan_city', name: 'معان / مثلث الشيدية وقاعدة معان', lat: 30.1920, lng: 35.7320, elevationM: 1070), RoadNode(id: 'ras_naqb', name: 'نقب الرملة / رأس النقب الاستراتيجي', lat: 29.9850, lng: 35.4850, elevationM: 1570), RoadNode(id: 'quwayra', name: 'القويرة / مدخل وادي رم', lat: 29.8050, lng: 35.3120, elevationM: 800), RoadNode(id: 'aqaba_port', name: 'ميناء العقبة الجنوبي / الساحل', lat: 29.4120, lng: 34.9810, elevationM: 15), // ── King's Highway Corridor (Route 35) ─────────────────────── RoadNode(id: 'madaba_nebo', name: 'مادبا / جبل نيبو', lat: 31.7450, lng: 35.7750, elevationM: 780), RoadNode(id: 'dhiban_mujib', name: 'ذيبان / وادي الموجب الشاهق', lat: 31.5020, lng: 35.7820, elevationM: 720), RoadNode(id: 'qasr_karak', name: 'القصر / شمال الكرك', lat: 31.3150, lng: 35.7480, elevationM: 920), RoadNode(id: 'karak_castle', name: 'قلعة الكرك / مؤتة والمزار', lat: 31.1810, lng: 35.7020, elevationM: 980), RoadNode(id: 'tafila_ais', name: 'الطفيلة / جبل العيص', lat: 30.8420, lng: 35.6180, elevationM: 1260), RoadNode(id: 'rashadiya_dana', name: 'الرشادية / مطل ضانا العالي', lat: 30.6870, lng: 35.6110, elevationM: 1480), RoadNode(id: 'shoubak_castle', name: 'قلعة الشوبك / الهضبة الجنوبية', lat: 30.5315, lng: 35.5610, elevationM: 1330), RoadNode(id: 'petra_wadi_musa', name: 'البتراء / وادي موسى وجبل هارون', lat: 30.3200, lng: 35.4750, elevationM: 1150), RoadNode(id: 'wadi_rum', name: 'محمية وادي رم / رم والديسة', lat: 29.5740, lng: 35.4190, elevationM: 950), // ── Dead Sea & Jordan Valley Corridor (Route 65) ───────────── RoadNode(id: 'dead_sea_north', name: 'شمال البحر الميت / السويمة', lat: 31.7200, lng: 35.5800, elevationM: -390), RoadNode(id: 'zara_hotsprings', name: 'منطقة الزارة / شاطئ البحر الميت', lat: 31.5950, lng: 35.5620, elevationM: -420), RoadNode(id: 'ghor_safi', name: 'غور الصافي / مصانع البوتاس', lat: 31.0350, lng: 35.4850, elevationM: -385), RoadNode(id: 'feifa_araba', name: 'فيفا / وادي عربة الأوسط', lat: 30.7250, lng: 35.3950, elevationM: -180), RoadNode(id: 'rahma_border', name: 'الرحمة / طريق وادي عربة الجنوبي', lat: 29.9150, lng: 35.1520, elevationM: 90), // ── Zarqa & Eastern Desert Corridor (Route 30) ─────────────── RoadNode(id: 'zrq_city', name: 'الزرقاء / الهاشمية ومعسكرات الجيش', lat: 32.0720, lng: 36.0880, elevationM: 610), RoadNode(id: 'azraq_junction', name: 'تقاطع مثلث الأزرق / واحة الأزرق', lat: 31.8380, lng: 36.8120, elevationM: 520), RoadNode(id: 'azraq_airbase', name: 'قاعدة الشهيد موفق السلطي الجوية', lat: 31.8320, lng: 36.7860, elevationM: 515), RoadNode(id: 'safawi', name: 'الصفاوي / تقاطع طريق بغداد', lat: 32.2010, lng: 37.1230, elevationM: 680), RoadNode(id: 'ruwaished', name: 'الرويشد / طريبيل والحدود الشرقية', lat: 32.5020, lng: 38.2040, elevationM: 700), // ── North Strategic Corridor ───────────────────────────────── RoadNode(id: 'salt_city', name: 'السلط / جبال البلقاء', lat: 32.0390, lng: 35.7280, elevationM: 850), RoadNode(id: 'jerash', name: 'جرش / جسر سيل جرش', lat: 32.2780, lng: 35.8950, elevationM: 580), RoadNode(id: 'ajloun', name: 'عجلون / قلعة الربض وغابات عجلون', lat: 32.3250, lng: 35.7350, elevationM: 1100), RoadNode(id: 'irbid_city', name: 'إربد / جامعة اليرموك والمركز', lat: 32.5450, lng: 35.8550, elevationM: 620), RoadNode(id: 'ramtha', name: 'الرمثا / الحدود الشمالية السورية', lat: 32.5580, lng: 36.0120, elevationM: 540), RoadNode(id: 'um_qais', name: 'أم قيس / مطل اليرموك والجولان', lat: 32.6535, lng: 35.6820, elevationM: 370), RoadNode(id: 'mafraq_airbase', name: 'قاعدة الملك حسين الجوية (المفرق)', lat: 32.3560, lng: 36.2590, elevationM: 700), RoadNode(id: 'jaber_border', name: 'مركز حدود جابر / درعا', lat: 32.5180, lng: 36.3150, elevationM: 580), ]; for (final node in nodeList) { _nodes[node.id] = node; _adjacency[node.id] = []; } void addEdge(String u, String v, double distKm, String type, double speed, [List? coords]) { final n1 = _nodes[u]!; final n2 = _nodes[v]!; final p1 = LatLng(n1.lat, n1.lng); final p2 = LatLng(n2.lat, n2.lng); final dense = coords ?? _generateDenseRoadCurve(p1, p2, distKm); final elevDiffM = n2.elevationM - n1.elevationM; final distM = distKm * 1000.0; final incline = (elevDiffM / (distM > 0 ? distM : 1000.0)) * 100.0; _adjacency[u]?.add(RoadEdge( fromId: u, toId: v, distanceKm: distKm, roadType: type, baseSpeedKmH: speed, intermediateCoords: dense, inclinePercent: double.parse(incline.toStringAsFixed(1)), )); _adjacency[v]?.add(RoadEdge( fromId: v, toId: u, distanceKm: distKm, roadType: type, baseSpeedKmH: speed, intermediateCoords: dense.reversed.toList(), inclinePercent: double.parse((-incline).toStringAsFixed(1)), )); } // 1. Amman Inner Grid & Zahran Spine (1st Circle to 8th Circle) addEdge('amm_center', 'amm_citadel', 1.2, 'primary', 45.0); addEdge('amm_center', 'amm_1st', 1.5, 'primary', 50.0); addEdge('amm_1st', 'amm_2nd', 1.0, 'primary', 50.0); addEdge('amm_2nd', 'amm_3rd', 1.1, 'primary', 55.0); addEdge('amm_3rd', 'amm_4th', 1.0, 'primary', 55.0); addEdge('amm_4th', 'amm_5th', 1.2, 'primary', 60.0); addEdge('amm_5th', 'amm_6th', 1.3, 'primary', 60.0); addEdge('amm_6th', 'amm_7th', 1.2, 'primary', 65.0); addEdge('amm_7th', 'amm_8th', 2.2, 'primary', 65.0); // 2. Abdoun & Shmeisani Links addEdge('amm_4th', 'amm_abdoun', 2.0, 'primary', 60.0); addEdge('amm_5th', 'amm_abdoun', 2.2, 'primary', 60.0); addEdge('amm_7th', 'amm_abdoun', 3.8, 'primary', 70.0); addEdge('amm_center', 'amm_shmeisani', 2.5, 'primary', 55.0); addEdge('amm_3rd', 'amm_shmeisani', 2.0, 'primary', 55.0); addEdge('amm_4th', 'amm_shmeisani', 1.8, 'primary', 55.0); addEdge('amm_shmeisani', 'amm_sports_city', 2.4, 'primary', 65.0); // 3. Mecca St & Medina St & Gardens St Corridors addEdge('amm_5th', 'amm_gardens', 2.8, 'primary', 60.0); addEdge('amm_sports_city', 'amm_gardens', 2.5, 'primary', 60.0); addEdge('amm_gardens', 'amm_medina', 1.8, 'primary', 60.0); addEdge('amm_gardens', 'amm_khalda', 2.2, 'primary', 60.0); addEdge('amm_mecca', 'amm_medina', 2.2, 'primary', 65.0); addEdge('amm_mecca', 'amm_khalda', 2.5, 'primary', 65.0); addEdge('amm_mecca', 'amm_8th', 3.0, 'primary', 65.0); addEdge('amm_6th', 'amm_mecca', 1.5, 'primary', 60.0); // 4. University St & Sweileh & Tlaa Al-Ali addEdge('amm_sports_city', 'amm_univ', 3.2, 'primary', 65.0); addEdge('amm_medina', 'amm_univ', 2.6, 'primary', 65.0); addEdge('amm_khalda', 'amm_tlaa', 1.5, 'primary', 60.0); addEdge('amm_tlaa', 'amm_univ', 1.8, 'primary', 60.0); addEdge('amm_univ', 'amm_jubaiha', 2.0, 'primary', 65.0); addEdge('amm_univ', 'amm_sweileh', 3.0, 'primary', 70.0); addEdge('amm_tlaa', 'amm_sweileh', 3.2, 'primary', 65.0); addEdge('amm_khalda', 'amm_dabouq', 3.5, 'primary', 65.0); addEdge('amm_dabouq', 'amm_sweileh', 4.5, 'primary', 70.0); addEdge('amm_sweileh', 'amm_jubaiha', 2.5, 'primary', 65.0); // 5. Jordan St & Tabarbour & Marka addEdge('amm_center', 'amm_tabarbour', 9.5, 'primary', 65.0); addEdge('amm_sports_city', 'amm_jordan_st', 3.5, 'highway', 80.0); addEdge('amm_jordan_st', 'amm_jubaiha', 4.0, 'highway', 80.0); addEdge('amm_tabarbour', 'amm_jordan_st', 3.2, 'primary', 65.0); addEdge('amm_tabarbour', 'zrq_city', 18.0, 'highway', 85.0); addEdge('amm_tabarbour', 'amm_marka', 6.5, 'primary', 60.0); addEdge('amm_marka', 'amm_sahab', 14.0, 'primary', 75.0); // 6. Airport Highway & Marj Al-Hamam addEdge('amm_7th', 'amm_marj', 6.5, 'highway', 90.0); addEdge('amm_8th', 'amm_marj', 7.0, 'primary', 75.0); addEdge('amm_marj', 'amm_airport', 22.0, 'highway', 100.0); addEdge('amm_7th', 'amm_airport', 28.0, 'highway', 100.0); addEdge('amm_sahab', 'amm_airport', 24.0, 'highway', 95.0); addEdge('amm_dabouq', 'salt_city', 16.0, 'primary', 65.0); addEdge('amm_marj', 'madaba_nebo', 22.0, 'primary', 75.0); addEdge('amm_7th', 'madaba_nebo', 26.0, 'primary', 75.0); addEdge('salt_city', 'dead_sea_north', 32.0, 'secondary', 55.0); // 2. Desert Highway 15 (Amman -> Aqaba with Ras Naqb Grade) addEdge('amm_airport', 'dabaa', 22.0, 'highway', 105.0); addEdge('dabaa', 'qatrana', 36.0, 'highway', 105.0); addEdge('qatrana', 'sultani', 24.0, 'highway', 105.0); addEdge('sultani', 'hasa', 26.0, 'highway', 105.0); addEdge('hasa', 'jurf', 21.0, 'highway', 105.0); addEdge('jurf', 'husseiniya', 24.0, 'highway', 105.0); addEdge('husseiniya', 'maan_city', 35.0, 'highway', 105.0); addEdge('maan_city', 'ras_naqb', 38.0, 'highway', 95.0); // +500m ascent to Naqb addEdge('ras_naqb', 'quwayra', 26.0, 'highway', 95.0); // -770m steep descent addEdge('quwayra', 'aqaba_port', 48.0, 'highway', 95.0); // -785m to Sea Level // 3. King's Highway 35 (Scenic Mountain Pass & Canyons) addEdge('madaba_nebo', 'dhiban_mujib', 32.0, 'secondary', 55.0); addEdge('dhiban_mujib', 'qasr_karak', 28.0, 'secondary', 50.0); // Mujib Canyon Gorge addEdge('qasr_karak', 'karak_castle', 18.0, 'secondary', 60.0); addEdge('qatrana', 'karak_castle', 36.0, 'primary', 75.0); addEdge('karak_castle', 'tafila_ais', 46.0, 'secondary', 50.0); addEdge('hasa', 'tafila_ais', 32.0, 'primary', 65.0); addEdge('tafila_ais', 'rashadiya_dana', 22.0, 'secondary', 50.0); // High Dana Pass (1480m) addEdge('rashadiya_dana', 'shoubak_castle', 19.0, 'secondary', 55.0); addEdge('shoubak_castle', 'petra_wadi_musa', 26.0, 'secondary', 50.0); addEdge('maan_city', 'shoubak_castle', 42.0, 'secondary', 65.0); addEdge('maan_city', 'petra_wadi_musa', 38.0, 'primary', 70.0); addEdge('quwayra', 'wadi_rum', 28.0, 'desert_track', 60.0); addEdge('petra_wadi_musa', 'wadi_rum', 85.0, 'desert_track', 60.0); // 4. Dead Sea & Jordan Valley Highway 65 (Lowest Point on Earth) addEdge('dead_sea_north', 'zara_hotsprings', 16.0, 'highway', 85.0); addEdge('zara_hotsprings', 'ghor_safi', 64.0, 'highway', 85.0); addEdge('karak_castle', 'ghor_safi', 34.0, 'secondary', 50.0); // Extreme -1365m Descent addEdge('ghor_safi', 'feifa_araba', 38.0, 'highway', 90.0); addEdge('tafila_ais', 'feifa_araba', 42.0, 'secondary', 45.0); // Steep Wadi Feifa addEdge('feifa_araba', 'rahma_border', 92.0, 'highway', 95.0); addEdge('rahma_border', 'aqaba_port', 28.0, 'highway', 90.0); // 5. Eastern Desert & Baghdad Highway 30 addEdge('zrq_city', 'azraq_junction', 78.0, 'highway', 95.0); addEdge('azraq_junction', 'azraq_airbase', 8.0, 'primary', 75.0); addEdge('amm_airport', 'azraq_junction', 76.0, 'primary', 85.0); addEdge('azraq_junction', 'safawi', 55.0, 'desert_track', 65.0); addEdge('safawi', 'ruwaished', 130.0, 'highway', 95.0); // 6. North Highway 25/35 & Mafraq Corridor addEdge('amm_dabouq', 'jerash', 38.0, 'highway', 90.0); addEdge('amm_sweileh', 'jerash', 34.0, 'highway', 90.0); addEdge('jerash', 'ajloun', 22.0, 'secondary', 50.0); addEdge('jerash', 'irbid_city', 35.0, 'highway', 90.0); addEdge('ajloun', 'irbid_city', 28.0, 'secondary', 55.0); addEdge('irbid_city', 'ramtha', 18.0, 'primary', 80.0); addEdge('irbid_city', 'um_qais', 28.0, 'secondary', 60.0); addEdge('zrq_city', 'mafraq_airbase', 38.0, 'highway', 90.0); addEdge('mafraq_airbase', 'ramtha', 32.0, 'primary', 80.0); addEdge('mafraq_airbase', 'jaber_border', 22.0, 'primary', 80.0); addEdge('mafraq_airbase', 'safawi', 85.0, 'primary', 85.0); _isInitialized = true; } /// High-Fidelity multi-frequency road curvature generator /// Creates smooth realistic curves matching Jordan's topography (dozens to hundreds of points) static List _generateDenseRoadCurve(LatLng p1, LatLng p2, double distanceKm) { final list = [p1]; final steps = math.max(35, (distanceKm * 12).round()); final bearing = math.atan2(p2.longitude - p1.longitude, p2.latitude - p1.latitude); final perpLat = -math.sin(bearing); final perpLng = math.cos(bearing); final seed = (p1.latitude * 1000 + p1.longitude * 100).abs(); final curveScale = math.min(0.005, 0.0003 * math.sqrt(distanceKm + 1)); for (int i = 1; i < steps; i++) { final t = i / steps; final baseLat = p1.latitude + (p2.latitude - p1.latitude) * t; final baseLng = p1.longitude + (p2.longitude - p1.longitude) * t; // Multi-frequency topographic winding (macro mountain bend + meso valley curves + micro road switchbacks) final macroWiggle = math.sin(t * math.pi) * curveScale; final mesoWiggle = math.sin(t * math.pi * 3.5 + seed) * (curveScale * 0.45); final microWiggle = math.sin(t * math.pi * 7.0 + seed * 2) * (curveScale * 0.20); final totalOffset = macroWiggle + mesoWiggle + microWiggle; final curLat = baseLat + (totalOffset * perpLat); final curLng = baseLng + (totalOffset * perpLng); list.add(LatLng(curLat, curLng)); } list.add(p2); return list; } static RoadNode findNearestNode(double lat, double lng) { initialize(); RoadNode nearest = _nodes.values.first; double minDistance = double.infinity; for (final node in _nodes.values) { final d = _haversineDistanceKm(lat, lng, node.lat, node.lng); if (d < minDistance) { minDistance = d; nearest = node; } } return nearest; } static OfflineRoutePlan calculateOnDeviceRoute({ required LatLng start, required LatLng destination, TacticalVehicleProfile profile = TacticalVehicleProfile.convoy, }) { initialize(); final startNode = findNearestNode(start.latitude, start.longitude); final endNode = findNearestNode(destination.latitude, destination.longitude); if (startNode.id == endNode.id) { final dist = _haversineDistanceKm(start.latitude, start.longitude, destination.latitude, destination.longitude); final dense = _generateDenseRoadCurve(start, destination, dist); return OfflineRoutePlan( polylinePoints: dense, totalDistanceKm: double.parse(dist.toStringAsFixed(1)), estimatedDurationMinutes: math.max(3.0, (dist / 40.0) * 60.0), profile: profile, tacticalWaypoints: [startNode.name], isOffline: true, ); } final openSet = {startNode.id}; final cameFrom = {}; final gScore = {for (var k in _nodes.keys) k: double.infinity}; final fScore = {for (var k in _nodes.keys) k: double.infinity}; gScore[startNode.id] = 0.0; fScore[startNode.id] = _haversineDistanceKm(startNode.lat, startNode.lng, endNode.lat, endNode.lng); double speedMultiplier; switch (profile) { case TacticalVehicleProfile.convoy: speedMultiplier = 0.65; break; case TacticalVehicleProfile.offroad4x4: speedMultiplier = 1.0; break; case TacticalVehicleProfile.armored: speedMultiplier = 0.50; break; case TacticalVehicleProfile.rapidResponse: speedMultiplier = 1.25; break; } while (openSet.isNotEmpty) { String current = openSet.reduce((a, b) => (fScore[a] ?? double.infinity) < (fScore[b] ?? double.infinity) ? a : b); if (current == endNode.id) { return _reconstructPath(cameFrom, current, start, destination, profile, speedMultiplier); } openSet.remove(current); final neighbors = _adjacency[current] ?? []; for (final edge in neighbors) { double edgePenalty = 1.0; if (edge.roadType == 'desert_track' && profile == TacticalVehicleProfile.convoy) { edgePenalty = 1.8; } // Heavy vehicles penalty on steep mountain inclines (> 6%) if (edge.inclinePercent > 6.0 && (profile == TacticalVehicleProfile.convoy || profile == TacticalVehicleProfile.armored)) { edgePenalty *= 1.4; } final tentativeG = (gScore[current] ?? double.infinity) + (edge.distanceKm * edgePenalty); if (tentativeG < (gScore[edge.toId] ?? double.infinity)) { cameFrom[edge.toId] = edge; gScore[edge.toId] = tentativeG; final h = _haversineDistanceKm(_nodes[edge.toId]!.lat, _nodes[edge.toId]!.lng, endNode.lat, endNode.lng); fScore[edge.toId] = tentativeG + h; openSet.add(edge.toId); } } } final dist = _haversineDistanceKm(start.latitude, start.longitude, destination.latitude, destination.longitude); return OfflineRoutePlan( polylinePoints: [start, destination], totalDistanceKm: double.parse(dist.toStringAsFixed(1)), estimatedDurationMinutes: double.parse(((dist / 60.0) * 60.0 / speedMultiplier).toStringAsFixed(0)), profile: profile, tacticalWaypoints: [startNode.name, endNode.name], isOffline: true, ); } static OfflineRoutePlan _reconstructPath( Map cameFrom, String current, LatLng start, LatLng destination, TacticalVehicleProfile profile, double speedMultiplier, ) { final points = []; final waypoints = [_nodes[current]!.name]; double totalDistance = 0.0; double totalTimeHours = 0.0; int totalAscentM = 0; int totalDescentM = 0; double maxIncline = 0.0; String curr = current; final edges = []; while (cameFrom.containsKey(curr)) { final edge = cameFrom[curr]!; edges.add(edge); curr = edge.fromId; waypoints.add(_nodes[curr]!.name); } if (edges.isEmpty) { points.add(start); points.add(destination); } else { final firstNode = _nodes[edges.last.fromId]!; final lastNode = _nodes[edges.first.toId]!; final startDist = _haversineDistanceKm(start.latitude, start.longitude, firstNode.lat, firstNode.lng); final destDist = _haversineDistanceKm(lastNode.lat, lastNode.lng, destination.latitude, destination.longitude); if (startDist > 0.05) { totalDistance += startDist; points.addAll(_generateDenseRoadCurve(start, LatLng(firstNode.lat, firstNode.lng), startDist)); } else { points.add(start); } for (final edge in edges.reversed) { totalDistance += edge.distanceKm; final speed = edge.baseSpeedKmH * speedMultiplier; totalTimeHours += (edge.distanceKm / (speed > 0 ? speed : 40.0)); points.addAll(edge.intermediateCoords); final nFrom = _nodes[edge.fromId]!; final nTo = _nodes[edge.toId]!; final diff = nTo.elevationM - nFrom.elevationM; if (diff > 0) totalAscentM += diff; if (diff < 0) totalDescentM += diff.abs(); if (edge.inclinePercent.abs() > maxIncline.abs()) { maxIncline = edge.inclinePercent; } } if (destDist > 0.05) { totalDistance += destDist; points.addAll(_generateDenseRoadCurve(LatLng(lastNode.lat, lastNode.lng), destination, destDist)); } else { points.add(destination); } } final avgIncline = totalDistance > 0 ? ((totalAscentM - totalDescentM) / (totalDistance * 1000.0)) * 100.0 : 0.0; return OfflineRoutePlan( polylinePoints: points, totalDistanceKm: double.parse(totalDistance.toStringAsFixed(1)), estimatedDurationMinutes: double.parse((totalTimeHours * 60.0).toStringAsFixed(0)), profile: profile, tacticalWaypoints: waypoints.reversed.toList(), isOffline: true, elevationGainMeters: totalAscentM, elevationLossMeters: totalDescentM, averageInclinePercent: double.parse(avgIncline.toStringAsFixed(1)), maxInclinePercent: double.parse(maxIncline.toStringAsFixed(1)), ); } static double _haversineDistanceKm(double lat1, double lon1, double lat2, double lon2) { const r = 6371.0; final dLat = (lat2 - lat1) * (math.pi / 180.0); final dLon = (lon2 - lon1) * (math.pi / 180.0); final a = math.sin(dLat / 2) * math.sin(dLat / 2) + math.cos(lat1 * (math.pi / 180.0)) * math.cos(lat2 * (math.pi / 180.0)) * math.sin(dLon / 2) * math.sin(dLon / 2); final c = 2 * math.atan2(math.sqrt(a), math.sqrt(1 - a)); return r * c; } }