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

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;
}
}