597 lines
30 KiB
Dart
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;
|
|
}
|
|
}
|