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

568 lines
23 KiB
Dart

import 'dart:math' as math;
import 'package:flutter/foundation.dart';
import 'package:intaleq_maps/intaleq_maps.dart';
import 'dem_tile_elevation_service.dart';
/// Ultra-High Precision Sovereign Digital Elevation Model (DEM) for Jordan
/// Incorporating 90+ strategic ground-control points across all cities, ridges, peaks & valleys.
class JordanDemSurface {
JordanDemSurface._();
static const double earthRadiusM = 6371000.0;
static const double kRefraction = 0.13;
static const double effectiveRadiusM = earthRadiusM / (1.0 - kRefraction);
/// Sagitta drop of the sight ray due to Earth curvature & atmospheric refraction:
/// h_sagitta = d1 * d2 / (2 * R_eff)
static double curvatureDrop(double d1Meters, double d2Meters) {
return (d1Meters * d2Meters) / (2.0 * effectiveRadiusM);
}
static double sagitta(double d1Meters, double d2Meters) =>
curvatureDrop(d1Meters, d2Meters);
static const List<List<double>> _control = [
// ── Greater Amman & Balqa (عمان والبلقاء) ──
[32.0220, 35.8450, 1060.0], // صويلح (Sweileh Peak)
[31.9960, 35.8285, 1045.0], // دابوق (Dabouq)
[32.0120, 35.8680, 1020.0], // الجبيهة / الجامعة الأردنية (Jubaiha)
[32.0350, 35.8750, 980.0], // أبو نصير (Abu Nseir)
[31.9890, 35.8580, 1010.0], // خلدا (Khalda)
[31.9810, 35.8720, 990.0], // تلاع العلي (Tla' Al-Ali)
[31.9680, 35.8950, 900.0], // الشميساني (Shmeisani)
[31.9570, 35.8810, 930.0], // الدوار الخامس / عبدون (5th Circle)
[31.9530, 35.8580, 920.0], // الدوار السابع (7th Circle)
[31.9515, 35.8350, 940.0], // الدوار الثامن (8th Circle)
[31.9420, 35.8210, 800.0], // وادي السير (Wadi Seer)
[31.9750, 35.7950, 1020.0], // بدر الجديدة (Badr Al-Jadeeda)
[31.9520, 35.9220, 850.0], // الدوار الأول / جبل عمان (1st Circle)
[31.9615, 35.9130, 740.0], // وسط البلد / العبدلي القديم (Downtown Valley)
[31.9580, 35.9350, 850.0], // جبل القلعة (Amman Citadel)
[31.9650, 35.9250, 830.0], // جبل اللويبده (Jabal Al-Lweibdeh)
[31.9380, 35.9250, 720.0], // رأس العين (Ras Al-Ain)
[31.9920, 35.9450, 880.0], // طبربور / المشاغل (Tabarbour)
[31.9720, 35.9910, 770.0], // ماركا / المطار (Marka)
[31.9250, 35.8750, 950.0], // مرج الحمام (Marj Al-Hamam)
[31.8950, 35.8250, 920.0], // ناعور (Naour)
[31.8850, 35.9350, 850.0], // اليادودة (Al-Yadudah)
[31.8710, 36.0040, 820.0], // سحاب (Sahab)
[31.8350, 36.0750, 760.0], // الموقر (Muwaqqar)
[31.7200, 35.9880, 720.0], // مطار الملكة علياء / الجيزة (QAIA)
[32.0390, 35.7280, 850.0], // السلط (Salt)
[32.0150, 35.7720, 920.0], // الفحيص (Fuheis)
[31.9950, 35.7650, 900.0], // ماحص (Mahis)
[32.0950, 35.7150, 1050.0], // زي / مرتفعات البلقاء (Zai)
[32.1250, 35.7350, 700.0], // علان (Allan)
// ── Zarqa & Eastern Desert (الزرقاء والبادية الشرقية) ──
[32.0720, 36.0880, 610.0], // الزرقاء (Zarqa City)
[32.0250, 36.0350, 670.0], // الرصيفة (Rusaifa)
[32.1250, 36.1150, 580.0], // الهاشمية (Hashimiyya)
[32.1150, 35.9550, 750.0], // بيرين (Birayn)
[31.8380, 36.8120, 520.0], // الأزرق (Azraq Oasis)
[32.2010, 37.1230, 680.0], // الصفاوي (Safawi)
[32.5020, 38.2040, 700.0], // الرويشد (Ruwaished)
// ── Northern Jordan (الشمال: إربد، عجلون، جرش، المفرق) ──
[32.2780, 35.8950, 600.0], // جرش (Jerash)
[32.2550, 35.8150, 950.0], // دبين (Dibbeen)
[32.3250, 35.7350, 1150.0], // عجلون / قلعة الربض (Ajloun)
[32.3150, 35.7650, 980.0], // عنجرة (Anjara)
[32.3350, 35.6850, 450.0], // كفرنجة (Kufranjeh)
[32.5450, 35.8550, 620.0], // إربد (Irbid)
[32.4850, 35.8750, 680.0], // الحصن (Husn)
[32.4450, 35.8450, 850.0], // المزار الشمالي (Mazar Shimali)
[32.4250, 35.9250, 700.0], // النعيمة (Nu'ayyimah)
[32.5580, 36.0120, 540.0], // الرمثا (Ramtha)
[32.6535, 35.6820, 370.0], // أم قيس (Um Qais)
[32.3560, 36.2590, 700.0], // المفرق (Mafraq)
[32.2750, 36.1550, 650.0], // بلعما (Bal'ama)
[32.5180, 36.3150, 580.0], // جابر (Jaber)
// ── Dead Sea, Jordan Valley & Madaba (الأغوار، البحر الميت ومادبا) ──
[31.8550, 35.5450, -300.0], // جسر الملك حسين (King Hussein Bridge)
[31.7200, 35.5800, -390.0], // شمال البحر الميت / السويمة (Dead Sea North)
[31.5950, 35.5620, -420.0], // الزارة (Zara Hot Springs)
[31.0350, 35.4850, -385.0], // غور الصافي (Ghor Safi)
[30.7250, 35.3950, -180.0], // فيفا (Wadi Araba / Feifa)
[29.9150, 35.1520, 90.0], // الرحمة (Rahma)
[29.7450, 35.1150, 120.0], // الريشة (Rishah)
[31.7450, 35.7750, 780.0], // مادبا (Madaba)
[31.7680, 35.7250, 710.0], // جبل نيبو (Mount Nebo)
[31.5750, 35.7450, 720.0], // مكاور (Mukawir)
[31.5020, 35.7820, 720.0], // ذيبان (Dhiban)
[31.4500, 35.6800, 150.0], // وادي الموجب (Mujib Gorge)
// ── Karak, Tafilah & Southern Highlands (الكرك، الطفيلة والشراة) ──
[31.3150, 35.7480, 920.0], // القصر (Qasr)
[31.2650, 35.7350, 940.0], // الربة (Rabbah)
[31.1810, 35.7020, 1020.0], // قلعة الكرك (Karak Castle)
[31.1250, 35.7150, 1100.0], // مؤتة (Muta)
[31.0850, 35.7050, 1220.0], // المزار الجنوبي (Mazar Janoubi)
[31.2450, 36.0420, 790.0], // القطرانة (Qatrana)
[31.0520, 35.9980, 820.0], // السلطاني (Sultani)
[30.8420, 35.6180, 1260.0], // الطفيلة (Tafilah)
[30.8650, 35.6350, 1300.0], // العيص (Ais)
[30.7450, 35.6250, 1200.0], // بصيرا (Buseira)
[30.6870, 35.6110, 1520.0], // الرشادية / ضانا (Dana Peak)
[30.8200, 35.9750, 840.0], // الحسا (Hasa)
[30.5315, 35.5610, 1360.0], // الشوبك (Shobak)
[30.5150, 35.5850, 1400.0], // نجل (Nijil)
[30.4920, 35.7980, 1050.0], // الحسينية (Husseiniya)
[30.3200, 35.4750, 1200.0], // البتراء / وادي موسى (Petra)
[30.3150, 35.4150, 1350.0], // جبل هارون (Jabal Haroun)
[30.2550, 35.4450, 1400.0], // الطيبة الجنوبية (Tayyibah)
[30.1250, 35.4850, 1550.0], // الراجف (Rajif)
[30.1920, 35.7320, 1070.0], // معان (Maan)
[29.9850, 35.4850, 1600.0], // رأس النقب (Ras Naqb)
[29.8050, 35.3120, 820.0], // القويرة (Quweira)
[29.6850, 35.5250, 900.0], // الديسة (Disi)
[29.5740, 35.4190, 1050.0], // قرية وادي رم (Wadi Rum)
[29.5650, 35.4050, 1734.0], // جبل رم (Jabal Ram Peak)
[
29.3150,
35.4350,
1854.0
], // جبل أم الدامي (Jabal Umm ad Dami - Highest Point in Jordan)
[29.4120, 34.9810, 20.0], // ميناء العقبة (Aqaba)
];
/// High-accuracy 2D Spatial IDW Elevation Interpolation across Jordan (1:1 with Server)
static double elevationAt(double lat, double lng) {
// 1. Deep Jordan Rift Valley / Dead Sea trench
if (lng < 35.65 && lat < 32.5 && lat > 30.8) {
final riftCenter = 35.50;
final distFromRift = (lng - riftCenter).abs();
final riftElev = -420.0 + distFromRift * 3200.0;
if (distFromRift < 0.15) {
return math.max(-430.0, math.min(1000.0, riftElev)).roundToDouble();
}
}
// 2. Inverse Distance Weighted (IDW) 2D Spatial Spline with power p=2.0
double num = 0.0;
double den = 0.0;
final latRad = lat * (math.pi / 180.0);
for (final c in _control) {
final dLat = (lat - c[0]) * 111.0;
final dLng = (lng - c[1]) * 111.0 * math.cos(latRad);
final distKm = math.max(math.sqrt(dLat * dLat + dLng * dLng), 0.15);
final w = 1.0 / math.pow(distKm, 2.0);
num += w * c[2];
den += w;
}
final baseElev = den > 0 ? num / den : 750.0;
// Authentic IDW elevation based on surveyed benchmarks across Jordan
return baseElev.roundToDouble();
}
}
/// One sampled station along the observer→target geodesic.
class OfflineLosSample {
final double distanceM;
final double lat;
final double lng;
final double groundElevationM;
final double rayHeightM;
final double clearanceM;
final bool isVisible;
final bool isBlocked;
final bool isDeadGround;
final bool isHighestObstacle;
const OfflineLosSample({
required this.distanceM,
required this.lat,
required this.lng,
required this.groundElevationM,
required this.rayHeightM,
required this.clearanceM,
required this.isVisible,
required this.isBlocked,
this.isDeadGround = false,
this.isHighestObstacle = false,
});
}
class OfflineLosObstacle {
final double distanceM;
final double elevationM;
final double sightRayElevationM;
final double lat;
final double lng;
final double excessM; // Penetration / Deficit in meters
const OfflineLosObstacle({
required this.distanceM,
required this.elevationM,
required this.sightRayElevationM,
required this.lat,
required this.lng,
required this.excessM,
});
}
/// Comprehensive tactical line-of-sight report matching server structure
class OfflineLosReport {
final double totalDistanceM;
final double azimuthDeg;
final double azimuthMilsNato;
final double azimuthMilsSoviet;
final double observerHeightM;
final double targetHeightM;
final double observerGroundElevM;
final double targetGroundElevM;
final double observerTotalElevM;
final double targetTotalElevM;
final double minElevationM;
final double maxElevationM;
final bool isDirectlyVisible;
final int deadGroundPercent;
final double verticalAngleDeg;
final double verticalAngleMils;
final double verticalAngleMilsSoviet;
final OfflineLosObstacle? highestObstacle;
final List<OfflineLosSample> profile;
const OfflineLosReport({
required this.totalDistanceM,
required this.azimuthDeg,
required this.azimuthMilsNato,
required this.azimuthMilsSoviet,
required this.observerHeightM,
required this.targetHeightM,
required this.observerGroundElevM,
required this.targetGroundElevM,
required this.observerTotalElevM,
required this.targetTotalElevM,
required this.minElevationM,
required this.maxElevationM,
required this.isDirectlyVisible,
required this.deadGroundPercent,
required this.verticalAngleDeg,
required this.verticalAngleMils,
required this.verticalAngleMilsSoviet,
required this.highestObstacle,
required this.profile,
});
/// Extract contiguous segments of visible line-of-sight paths (gap-free)
List<List<LatLng>> get visibleSegments {
final segments = <List<LatLng>>[];
List<LatLng> current = [];
for (int i = 0; i < profile.length - 1; i++) {
final s1 = profile[i];
final s2 = profile[i + 1];
if (s1.isVisible && s2.isVisible) {
if (current.isEmpty) {
current.add(LatLng(s1.lat, s1.lng));
}
current.add(LatLng(s2.lat, s2.lng));
} else {
if (current.length > 1) {
segments.add(List.from(current));
}
current = [];
}
}
if (current.length > 1) {
segments.add(current);
}
return segments;
}
/// Extract contiguous segments of obstructed line-of-sight paths (gap-free)
List<List<LatLng>> get blockedSegments {
final segments = <List<LatLng>>[];
List<LatLng> current = [];
for (int i = 0; i < profile.length - 1; i++) {
final s1 = profile[i];
final s2 = profile[i + 1];
if (!s1.isVisible || !s2.isVisible) {
if (current.isEmpty) {
current.add(LatLng(s1.lat, s1.lng));
}
current.add(LatLng(s2.lat, s2.lng));
} else {
if (current.length > 1) {
segments.add(List.from(current));
}
current = [];
}
}
if (current.length > 1) {
segments.add(current);
}
return segments;
}
/// Print comprehensive formatted tactical telemetry to terminal console
void printTacticalTelemetry({String tag = 'SOVEREIGN OFFLINE DEM LOS'}) {
final statusStr = isDirectlyVisible
? '🟢 مكشوف وسالك (CLEAR LOS)'
: '🔴 محجوب بتضاريس عازلة (OBSTRUCTED)';
final obsStr =
'${observerGroundElevM.round()}م (سطح الأرض) + ${observerHeightM.round()}م (بصريات) = ${observerTotalElevM.round()}م إجمالي';
final tgtStr =
'${targetGroundElevM.round()}م (سطح الأرض) + ${targetHeightM.round()}م (بصريات) = ${targetTotalElevM.round()}م إجمالي';
debugPrint(
'════════════════════════════════════════════════════════════════════');
debugPrint('🎯 [$tag]');
debugPrint('• حالة خط الرؤية: $statusStr');
debugPrint(
'• المسافة الجيوديسية: ${(totalDistanceM / 1000.0).toStringAsFixed(2)} كم (${totalDistanceM.round()} متر)');
debugPrint(
'• السمت التكتيكي (Azimuth): ${azimuthDeg.toStringAsFixed(1)}° | ${azimuthMilsNato.round()}₥ NATO | ${azimuthMilsSoviet.round()}₥ Soviet');
debugPrint(
'• زاوية الارتفاع الرأسية: ${verticalAngleDeg.toStringAsFixed(2)}° (${verticalAngleMils.toStringAsFixed(1)}₥)');
debugPrint('• موقع الراصد (Observer): $obsStr');
debugPrint('• موقع الهدف (Target): $tgtStr');
debugPrint(
'• نطاق الارتفاعات: أدنى ${minElevationM.round()}م • أعلى ${maxElevationM.round()}م');
debugPrint('• نسبة المناطق الميتة (Dead Ground): $deadGroundPercent%');
debugPrint(
'• عدد محطات الاستقراء (Sample Stations): ${profile.length} محطة');
if (highestObstacle != null && !isDirectlyVisible) {
debugPrint('⚠️ نقطة الحجب التضاريسي القصوى (Critical Obstacle):');
debugPrint(' - الارتفاع: ${highestObstacle!.elevationM.round()}م AMSL');
debugPrint(
' - المسافة من الراصد: ${highestObstacle!.distanceM.round()}م');
debugPrint(
' - ارتفاع شعاع الرؤية عندها: ${highestObstacle!.sightRayElevationM.round()}م');
debugPrint(
' - مقدار اختراق الحجب: +${highestObstacle!.excessM.toStringAsFixed(1)}م');
debugPrint(
' - الإحداثيات: ${highestObstacle!.lat}, ${highestObstacle!.lng}');
}
debugPrint(
'════════════════════════════════════════════════════════════════════');
}
}
/// Sovereign Offline Line of Sight Engine with Full Geodesic Station Profile Sampling
class OfflineLosEngine {
static double _haversineMeters(
double lat1, double lon1, double lat2, double lon2) {
const r = 6371000.0;
final dLat = (lat2 - lat1) * (math.pi / 180.0);
final dLon = (lon2 - lon1) * (math.pi / 180.0);
final a = math.sin(dLat / 2.0) * math.sin(dLat / 2.0) +
math.cos(lat1 * (math.pi / 180.0)) *
math.cos(lat2 * (math.pi / 180.0)) *
math.sin(dLon / 2.0) *
math.sin(dLon / 2.0);
final c = 2.0 * math.atan2(math.sqrt(a), math.sqrt(1.0 - a));
return r * c;
}
static double _calculateBearing(
double lat1, double lon1, double lat2, double lon2) {
final phi1 = lat1 * (math.pi / 180.0);
final phi2 = lat2 * (math.pi / 180.0);
final deltaLambda = (lon2 - lon1) * (math.pi / 180.0);
final y = math.sin(deltaLambda) * math.cos(phi2);
final x = math.cos(phi1) * math.sin(phi2) -
math.sin(phi1) * math.cos(phi2) * math.cos(deltaLambda);
return ((math.atan2(y, x) * 180.0) / math.pi + 360.0) % 360.0;
}
/// High-accuracy calculation with pre-fetched satellite DEM raster tiles
static Future<OfflineLosReport> calculateAsync({
required LatLng observer,
required LatLng target,
double observerHeightM = 2.0,
double targetHeightM = 2.0,
int? samplesCount,
double? stepMeters,
}) async {
await DemTileElevationService.prefetchTilesForCoords([observer, target],
zoom: 12);
return calculate(
observer: observer,
target: target,
observerHeightM: observerHeightM,
targetHeightM: targetHeightM,
samplesCount: samplesCount,
stepMeters: stepMeters,
);
}
/// Synchronous calculation using DemTileElevationService (or DEM surface fallback)
static OfflineLosReport calculate({
required LatLng observer,
required LatLng target,
double observerHeightM = 2.0,
double targetHeightM = 2.0,
int? samplesCount,
double? stepMeters,
}) {
final totalDistM = _haversineMeters(
observer.latitude,
observer.longitude,
target.latitude,
target.longitude,
);
int effectiveSamples;
if (stepMeters != null && stepMeters > 0) {
effectiveSamples =
math.max(15, math.min(300, (totalDistM / stepMeters).round() + 1));
} else if (samplesCount != null && samplesCount >= 10) {
effectiveSamples = math.min(300, samplesCount);
} else {
double step;
if (totalDistM <= 500) {
step = 5.0;
} else if (totalDistM <= 2000) {
step = 10.0;
} else if (totalDistM <= 10000) {
step = 25.0;
} else if (totalDistM <= 30000) {
step = 50.0;
} else {
step = 100.0;
}
effectiveSamples =
math.max(25, math.min(200, (totalDistM / step).round() + 1));
}
final azimuthDeg = _calculateBearing(
observer.latitude,
observer.longitude,
target.latitude,
target.longitude,
);
final observerGround = DemTileElevationService.getElevationSync(
observer.latitude, observer.longitude);
final targetGround = DemTileElevationService.getElevationSync(
target.latitude, target.longitude);
final observerTotal = observerGround + observerHeightM;
final targetTotal = targetGround + targetHeightM;
final samples = <OfflineLosSample>[];
bool isDirectlyVisible = true;
OfflineLosObstacle? highestObstacle;
double maxPenetration = 0.0;
double minElev = math.min(observerGround, targetGround);
double maxElev = math.max(observerGround, targetGround);
double maxHorizonAngle = -double.infinity;
int deadGroundCount = 0;
for (int i = 0; i < effectiveSamples; i++) {
final fraction = effectiveSamples == 1 ? 0.0 : i / (effectiveSamples - 1);
final dMeters = totalDistM * fraction;
final pLat =
observer.latitude + (target.latitude - observer.latitude) * fraction;
final pLng = observer.longitude +
(target.longitude - observer.longitude) * fraction;
final gElev = DemTileElevationService.getElevationSync(pLat, pLng);
minElev = math.min(minElev, gElev);
maxElev = math.max(maxElev, gElev);
final d1 = dMeters;
final d2 = totalDistM - dMeters;
final sagitta = JordanDemSurface.sagitta(d1, d2);
final apparentElev = gElev + sagitta;
final rayElev = observerTotal + (targetTotal - observerTotal) * fraction;
final margin = rayElev - apparentElev;
final isPointVisible = i == 0 || i == effectiveSamples - 1 || margin >= 0;
if (!isPointVisible) {
isDirectlyVisible = false;
final penetration = apparentElev - rayElev;
if (penetration > maxPenetration) {
maxPenetration = penetration;
highestObstacle = OfflineLosObstacle(
distanceM: dMeters.roundToDouble(),
lat: double.parse(pLat.toStringAsFixed(6)),
lng: double.parse(pLng.toStringAsFixed(6)),
elevationM: gElev.roundToDouble(),
sightRayElevationM: rayElev.roundToDouble(),
excessM: double.parse(penetration.toStringAsFixed(1)),
);
}
}
final angleFromObserver =
dMeters > 0 ? (apparentElev - observerTotal) / dMeters : 0.0;
bool isDeadGround = false;
if (i > 0) {
if (angleFromObserver < maxHorizonAngle) {
isDeadGround = true;
deadGroundCount++;
} else {
maxHorizonAngle = angleFromObserver;
}
}
samples.add(OfflineLosSample(
distanceM: dMeters.roundToDouble(),
lat: double.parse(pLat.toStringAsFixed(6)),
lng: double.parse(pLng.toStringAsFixed(6)),
groundElevationM: gElev.roundToDouble(),
rayHeightM: rayElev.roundToDouble(),
clearanceM: margin.roundToDouble(),
isVisible: isPointVisible,
isBlocked: !isPointVisible,
isDeadGround: isDeadGround,
));
}
final elevDiff = targetTotal - observerTotal;
final vertAngleRad =
totalDistM > 0 ? math.atan(elevDiff / totalDistM) : 0.0;
final vertAngleDeg = vertAngleRad * (180.0 / math.pi);
final vertAngleMilsNato = vertAngleRad * (6400.0 / (2.0 * math.pi));
final vertAngleMilsSoviet = vertAngleRad * (6000.0 / (2.0 * math.pi));
final deadGroundPercent = effectiveSamples > 0
? ((deadGroundCount / effectiveSamples) * 100).round()
: 0;
final report = OfflineLosReport(
totalDistanceM: totalDistM.roundToDouble(),
azimuthDeg: double.parse(azimuthDeg.toStringAsFixed(1)),
azimuthMilsNato: double.parse(
((azimuthDeg * 17.7777777778) % 6400).toStringAsFixed(1)),
azimuthMilsSoviet: double.parse(
((azimuthDeg * 16.6666666667) % 6000).toStringAsFixed(1)),
observerHeightM: observerHeightM,
targetHeightM: targetHeightM,
observerGroundElevM: observerGround.roundToDouble(),
targetGroundElevM: targetGround.roundToDouble(),
observerTotalElevM: observerTotal.roundToDouble(),
targetTotalElevM: targetTotal.roundToDouble(),
minElevationM: minElev.roundToDouble(),
maxElevationM: maxElev.roundToDouble(),
isDirectlyVisible: isDirectlyVisible,
deadGroundPercent: deadGroundPercent,
verticalAngleDeg: double.parse(vertAngleDeg.toStringAsFixed(2)),
verticalAngleMils: double.parse(vertAngleMilsNato.toStringAsFixed(1)),
verticalAngleMilsSoviet:
double.parse(vertAngleMilsSoviet.toStringAsFixed(1)),
highestObstacle: highestObstacle,
profile: samples,
);
report.printTacticalTelemetry(tag: 'SOVEREIGN OFFLINE DEM LOS');
return report;
}
}