Files
maps-saas/packages/tactical_app/lib/services/offline_routing_engine.dart
T

597 lines
30 KiB
Dart

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<LatLng> 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<String> 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<LatLng> polylinePoints;
final double totalDistanceKm;
final double estimatedDurationMinutes;
final TacticalVehicleProfile profile;
final List<String> 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<RouteManeuver> 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<String, RoadNode> _nodes = {};
static final Map<String, List<RoadEdge>> _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<LatLng>? 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<LatLng> _generateDenseRoadCurve(LatLng p1, LatLng p2, double distanceKm) {
final list = <LatLng>[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 = <String>{startNode.id};
final cameFrom = <String, RoadEdge>{};
final gScore = <String, double>{for (var k in _nodes.keys) k: double.infinity};
final fScore = <String, double>{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<String, RoadEdge> cameFrom,
String current,
LatLng start,
LatLng destination,
TacticalVehicleProfile profile,
double speedMultiplier,
) {
final points = <LatLng>[];
final waypoints = <String>[_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 = <RoadEdge>[];
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;
}
}