568 lines
23 KiB
Dart
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;
|
|
}
|
|
}
|