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, }); } 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; 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, }); 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 RoadNode(id: 'amm_center', name: 'وسط عمان / العبدلي', lat: 31.9615, lng: 35.9130, elevationM: 760), RoadNode(id: 'amm_dabouq', name: 'دابوق / قاعدة القيادة الغربية', lat: 31.9960, lng: 35.8285, elevationM: 1045), RoadNode(id: 'amm_7th', name: 'الدوار السابع / طريق المطار', lat: 31.9530, lng: 35.8580, elevationM: 920), RoadNode(id: 'amm_tabarbour', name: 'طبربور / اتوستراد الزرقاء', lat: 31.9920, lng: 35.9450, elevationM: 880), 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 (Jerash, Ajloun, Irbid, Mafraq) 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); // Incline % = (Elevation Change in meters / Distance in meters) * 100 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 Ring & Arterials addEdge('amm_dabouq', 'amm_center', 11.0, 'primary', 65.0); addEdge('amm_dabouq', 'amm_7th', 9.5, 'primary', 70.0); addEdge('amm_center', 'amm_7th', 8.0, 'primary', 60.0); addEdge('amm_center', 'amm_tabarbour', 10.0, 'primary', 60.0); addEdge('amm_tabarbour', 'zrq_city', 18.0, 'highway', 85.0); addEdge('amm_7th', 'amm_airport', 28.0, 'highway', 100.0); addEdge('amm_dabouq', 'salt_city', 16.0, 'primary', 65.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('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; } static List _generateDenseRoadCurve(LatLng p1, LatLng p2, double distanceKm) { final list = [p1]; final steps = math.max(4, (distanceKm / 5.0).round()); for (int i = 1; i < steps; i++) { final t = i / steps; final lat = p1.latitude + (p2.latitude - p1.latitude) * t; final lng = p1.longitude + (p2.longitude - p1.longitude) * t; final offset = math.sin(t * math.pi) * 0.003; list.add(LatLng(lat + offset, lng - (offset * 0.5))); } 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) { return OfflineRoutePlan( polylinePoints: [start, destination], totalDistanceKm: _haversineDistanceKm(start.latitude, start.longitude, destination.latitude, destination.longitude), estimatedDurationMinutes: 5.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); } 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; } } 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; } }