232 lines
8.2 KiB
Dart
232 lines
8.2 KiB
Dart
import 'dart:math' as math;
|
|
import 'package:flutter/foundation.dart';
|
|
import 'package:intaleq_maps/intaleq_maps.dart';
|
|
import 'offline_los_engine.dart';
|
|
import 'dem_tile_elevation_service.dart';
|
|
|
|
/// One radial sight ray from observer at a specific azimuth angle
|
|
class RadialRayResult {
|
|
final double azimuthDeg;
|
|
final double maxRangeMeters;
|
|
final double visibleDistanceMeters;
|
|
final LatLng endpoint;
|
|
final bool isBlockedByTerrain;
|
|
final LatLng? obstacleCoord;
|
|
final double? obstacleElevation;
|
|
|
|
const RadialRayResult({
|
|
required this.azimuthDeg,
|
|
required this.maxRangeMeters,
|
|
required this.visibleDistanceMeters,
|
|
required this.endpoint,
|
|
required this.isBlockedByTerrain,
|
|
this.obstacleCoord,
|
|
this.obstacleElevation,
|
|
});
|
|
}
|
|
|
|
/// 360° Comprehensive Tactical Viewshed (حقل الرؤية والرصد الدائري 360 درجة)
|
|
class Viewshed360Report {
|
|
final LatLng center;
|
|
final double observerHeightM;
|
|
final double radiusMeters;
|
|
final double observerGroundElevM;
|
|
final double observerTotalElevM;
|
|
final List<RadialRayResult> rays;
|
|
final List<LatLng> polygonVertices;
|
|
final double visibleAreaSqKm;
|
|
final double totalPotentialAreaSqKm;
|
|
final int visibilityPercentage;
|
|
final double averageSightRangeKm;
|
|
|
|
const Viewshed360Report({
|
|
required this.center,
|
|
required this.observerHeightM,
|
|
required this.radiusMeters,
|
|
required this.observerGroundElevM,
|
|
required this.observerTotalElevM,
|
|
required this.rays,
|
|
required this.polygonVertices,
|
|
required this.visibleAreaSqKm,
|
|
required this.totalPotentialAreaSqKm,
|
|
required this.visibilityPercentage,
|
|
required this.averageSightRangeKm,
|
|
});
|
|
|
|
void printTelemetry() {
|
|
debugPrint(
|
|
'════════════════════════════════════════════════════════════════════');
|
|
debugPrint('🌐 [TACTICAL 360° VIEWSHED & FAN COVERAGE REPORT]');
|
|
debugPrint(
|
|
'• مركز الرصد: ${center.latitude.toStringAsFixed(5)}, ${center.longitude.toStringAsFixed(5)}');
|
|
debugPrint(
|
|
'• ارتفاع سطح الأرض: ${observerGroundElevM.round()}م | إجمالي مع البصريات: ${observerTotalElevM.round()}م');
|
|
debugPrint(
|
|
'• نصف قطر المسح: ${(radiusMeters / 1000.0).toStringAsFixed(1)} كم');
|
|
debugPrint('• عدد أشعة المسح: ${rays.length} شعاع (360° Radial Sweep)');
|
|
debugPrint(
|
|
'• مساحة الرؤية المكشوفة: ${visibleAreaSqKm.toStringAsFixed(2)} كم² من أصل ${totalPotentialAreaSqKm.toStringAsFixed(2)} كم²');
|
|
debugPrint('• كفاءة التغطية البصرية: $visibilityPercentage%');
|
|
debugPrint(
|
|
'• متوسط مدى الرؤية الفعلي: ${averageSightRangeKm.toStringAsFixed(2)} كم');
|
|
debugPrint(
|
|
'════════════════════════════════════════════════════════════════════');
|
|
}
|
|
}
|
|
|
|
/// Sovereign Ultra-Fast 360° Tactical Viewshed Engine
|
|
|
|
class Viewshed360Engine {
|
|
Viewshed360Engine._();
|
|
|
|
/// Calculate 360° Viewshed asynchronously after pre-fetching real satellite DEM tiles
|
|
static Future<Viewshed360Report> calculateAsync({
|
|
required LatLng observer,
|
|
double radiusMeters = 5000.0,
|
|
double observerHeightM = 2.0,
|
|
double targetHeightM = 1.5,
|
|
int rayCount = 120,
|
|
int samplesPerRay = 30,
|
|
}) async {
|
|
final dLat = (radiusMeters / 111132.95);
|
|
final dLng = (radiusMeters /
|
|
(111412.84 * math.cos(observer.latitude * math.pi / 180.0)));
|
|
final boundingCoords = [
|
|
LatLng(observer.latitude + dLat, observer.longitude + dLng),
|
|
LatLng(observer.latitude - dLat, observer.longitude + dLng),
|
|
LatLng(observer.latitude + dLat, observer.longitude - dLng),
|
|
LatLng(observer.latitude - dLat, observer.longitude - dLng),
|
|
];
|
|
await DemTileElevationService.prefetchTilesForCoords(boundingCoords,
|
|
zoom: 12);
|
|
return calculate(
|
|
observer: observer,
|
|
radiusMeters: radiusMeters,
|
|
observerHeightM: observerHeightM,
|
|
targetHeightM: targetHeightM,
|
|
rayCount: rayCount,
|
|
samplesPerRay: samplesPerRay,
|
|
);
|
|
}
|
|
|
|
/// Calculate 360° Viewshed in real-time on device (Takes 10-30ms)
|
|
static Viewshed360Report calculate({
|
|
required LatLng observer,
|
|
double radiusMeters = 5000.0,
|
|
double observerHeightM = 2.0,
|
|
double targetHeightM = 1.5,
|
|
int rayCount =
|
|
120, // 1 ray every 3 degrees (120 rays) or 360 rays for maximum density
|
|
int samplesPerRay = 30,
|
|
}) {
|
|
final obsGround = DemTileElevationService.getElevationSync(
|
|
observer.latitude, observer.longitude);
|
|
final obsTotal = obsGround + observerHeightM;
|
|
|
|
final rays = <RadialRayResult>[];
|
|
final polygon = <LatLng>[];
|
|
double sumDist = 0.0;
|
|
double visibleAreaAccumulator = 0.0;
|
|
|
|
final degStep = 360.0 / rayCount;
|
|
final latRad = observer.latitude * (math.pi / 180.0);
|
|
const earthMetersPerLatDeg = 111132.95;
|
|
final earthMetersPerLngDeg = 111412.84 * math.cos(latRad);
|
|
|
|
const int samples = 30;
|
|
|
|
for (int i = 0; i < rayCount; i++) {
|
|
final azDeg = i * degStep;
|
|
final azRad = azDeg * (math.pi / 180.0);
|
|
|
|
final dLat = (radiusMeters * math.cos(azRad)) / earthMetersPerLatDeg;
|
|
final dLng = (radiusMeters * math.sin(azRad)) / earthMetersPerLngDeg;
|
|
|
|
double maxAngleSoFar = -double.infinity;
|
|
double visibleHorizonDist = radiusMeters;
|
|
double visibleHorizonLat = observer.latitude + dLat;
|
|
double visibleHorizonLng = observer.longitude + dLng;
|
|
bool isBlocked = false;
|
|
LatLng? blockCoord;
|
|
double? blockElev;
|
|
|
|
for (int step = 1; step <= samples; step++) {
|
|
final frac = step / samples;
|
|
final sDist = radiusMeters * frac;
|
|
final sLat = observer.latitude + dLat * frac;
|
|
final sLng = observer.longitude + dLng * frac;
|
|
|
|
final sElev = DemTileElevationService.getElevationSync(sLat, sLng);
|
|
final sagitta = JordanDemSurface.sagitta(sDist, radiusMeters - sDist);
|
|
final apparentElev = sElev + targetHeightM - sagitta;
|
|
final angle = (apparentElev - obsTotal) / sDist;
|
|
|
|
if (angle >= maxAngleSoFar) {
|
|
maxAngleSoFar = angle;
|
|
visibleHorizonDist = sDist;
|
|
visibleHorizonLat = sLat;
|
|
visibleHorizonLng = sLng;
|
|
} else {
|
|
if (!isBlocked) {
|
|
isBlocked = true;
|
|
blockCoord = LatLng(sLat, sLng);
|
|
blockElev = sElev;
|
|
}
|
|
}
|
|
}
|
|
|
|
final endPoint = LatLng(visibleHorizonLat, visibleHorizonLng);
|
|
|
|
rays.add(RadialRayResult(
|
|
azimuthDeg: azDeg,
|
|
maxRangeMeters: radiusMeters,
|
|
visibleDistanceMeters: visibleHorizonDist,
|
|
endpoint: endPoint,
|
|
isBlockedByTerrain: isBlocked,
|
|
obstacleCoord: blockCoord,
|
|
obstacleElevation: blockElev,
|
|
));
|
|
|
|
polygon.add(endPoint);
|
|
sumDist += visibleHorizonDist;
|
|
|
|
// Sector area accumulation = 0.5 * r^2 * dTheta
|
|
final dThetaRad = degStep * (math.pi / 180.0);
|
|
visibleAreaAccumulator += 0.5 *
|
|
(visibleHorizonDist / 1000.0) *
|
|
(visibleHorizonDist / 1000.0) *
|
|
dThetaRad;
|
|
}
|
|
|
|
if (polygon.isNotEmpty) {
|
|
polygon.add(polygon.first); // Close the polygon ring
|
|
}
|
|
|
|
final totalPotentialArea =
|
|
math.pi * (radiusMeters / 1000.0) * (radiusMeters / 1000.0);
|
|
final visPercent = totalPotentialArea > 0
|
|
? ((visibleAreaAccumulator / totalPotentialArea) * 100)
|
|
.round()
|
|
.clamp(0, 100)
|
|
: 0;
|
|
final avgRange = rayCount > 0 ? (sumDist / rayCount) / 1000.0 : 0.0;
|
|
|
|
final report = Viewshed360Report(
|
|
center: observer,
|
|
observerHeightM: observerHeightM,
|
|
radiusMeters: radiusMeters,
|
|
observerGroundElevM: obsGround,
|
|
observerTotalElevM: obsTotal,
|
|
rays: rays,
|
|
polygonVertices: polygon,
|
|
visibleAreaSqKm: visibleAreaAccumulator,
|
|
totalPotentialAreaSqKm: totalPotentialArea,
|
|
visibilityPercentage: visPercent,
|
|
averageSightRangeKm: avgRange,
|
|
);
|
|
|
|
report.printTelemetry();
|
|
return report;
|
|
}
|
|
}
|