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 rays; final List 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 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 = []; final polygon = []; 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; } }