import 'dart:math' as math; import 'package:flutter/material.dart'; import 'package:intaleq_maps/intaleq_maps.dart'; import '../models/military_operations_models.dart'; import 'dem_tile_elevation_service.dart'; /// Sovereign On-Device Helicopter Landing Zone (HLZ) Suitability Engine class HlzAssessmentEngine { HlzAssessmentEngine._(); /// Assess proposed landing site terrain slope, obstacle clearance, and landing corridors static Future assessLandingZone({ required LatLng center, required HelicopterType helicopterType, double approachAzimuthDeg = 0.0, }) async { // 1. Determine recommended pad radius based on helicopter airframe size double padRadiusM; double maxAllowableSlopePct; switch (helicopterType) { case HelicopterType.lightUtility: padRadiusM = 25.0; // 50m diameter maxAllowableSlopePct = 15.0; // 15% slope max break; case HelicopterType.mediumLift: padRadiusM = 40.0; // 80m diameter (UH-60 / AH-64) maxAllowableSlopePct = 10.0; // 10% slope max break; case HelicopterType.heavyTransport: padRadiusM = 60.0; // 120m diameter (CH-47 Chinook) maxAllowableSlopePct = 7.0; // 7% slope max break; } // 2. Query Center Elevation final centerElev = await DemTileElevationService.getElevation(center.latitude, center.longitude); // 3. Sample 16 cardinal points around the perimeter to calculate maximum terrain slope final List perimeterElevs = []; final List padBoundary = []; const int samplePoints = 16; for (int i = 0; i < samplePoints; i++) { final angleRad = (i * 2 * math.pi) / samplePoints; final dLat = (padRadiusM / 6371000.0) * (180.0 / math.pi) * math.cos(angleRad); final dLng = (padRadiusM / (6371000.0 * math.cos(center.latitude * math.pi / 180.0))) * (180.0 / math.pi) * math.sin(angleRad); final pLat = center.latitude + dLat; final pLng = center.longitude + dLng; padBoundary.add(LatLng(pLat, pLng)); final elev = await DemTileElevationService.getElevation(pLat, pLng); perimeterElevs.add(elev); } // Close polygon if (padBoundary.isNotEmpty) padBoundary.add(padBoundary.first); // Calculate maximum slope percentage double maxSlope = 0.0; double slopeSum = 0.0; for (final pElev in perimeterElevs) { final slopePct = (pElev - centerElev).abs() / padRadiusM * 100.0; if (slopePct > maxSlope) maxSlope = slopePct; slopeSum += slopePct; } final avgSlope = slopeSum / perimeterElevs.length; // 4. Generate 500m Approach/Departure Funnel final List funnel = []; const double funnelLengthM = 500.0; const double funnelWidthM = 120.0; final approachRad = (approachAzimuthDeg * math.pi) / 180.0; final perpRad = approachRad + (math.pi / 2); // Base point at pad edge final baseLat = center.latitude + (padRadiusM / 6371000.0) * (180.0 / math.pi) * math.cos(approachRad); final baseLng = center.longitude + (padRadiusM / (6371000.0 * math.cos(center.latitude * math.pi / 180.0))) * (180.0 / math.pi) * math.sin(approachRad); // Funnel End Center final endCenterLat = center.latitude + (funnelLengthM / 6371000.0) * (180.0 / math.pi) * math.cos(approachRad); final endCenterLng = center.longitude + (funnelLengthM / (6371000.0 * math.cos(center.latitude * math.pi / 180.0))) * (180.0 / math.pi) * math.sin(approachRad); // Funnel Left & Right final leftEndLat = endCenterLat + (funnelWidthM / 2 / 6371000.0) * (180.0 / math.pi) * math.cos(perpRad); final leftEndLng = endCenterLng + (funnelWidthM / 2 / (6371000.0 * math.cos(endCenterLat * math.pi / 180.0))) * (180.0 / math.pi) * math.sin(perpRad); final rightEndLat = endCenterLat - (funnelWidthM / 2 / 6371000.0) * (180.0 / math.pi) * math.cos(perpRad); final rightEndLng = endCenterLng - (funnelWidthM / 2 / (6371000.0 * math.cos(endCenterLat * math.pi / 180.0))) * (180.0 / math.pi) * math.sin(perpRad); funnel.addAll([ LatLng(baseLat, baseLng), LatLng(leftEndLat, leftEndLng), LatLng(rightEndLat, rightEndLng), LatLng(baseLat, baseLng), ]); // 5. Check Obstacle Height in Funnel final endElev = await DemTileElevationService.getElevation(endCenterLat, endCenterLng); final funnelRise = endElev - centerElev; final isObstacleClear = funnelRise < 35.0; // Less than 35m rise over 500m approach // 6. Grade Suitability final isSlopeAcceptable = maxSlope <= maxAllowableSlopePct; String grade; Color gradeColor; if (isSlopeAcceptable && maxSlope < (maxAllowableSlopePct * 0.6) && isObstacleClear) { grade = 'صالح ومثالي (OPTIMAL GO)'; gradeColor = const Color(0xFF10B981); // Emerald } else if (isSlopeAcceptable && isObstacleClear) { grade = 'مقبول بحذر (MARGINAL SLOW-GO)'; gradeColor = const Color(0xFFF59E0B); // Amber } else { grade = 'غير صالح للهبوط (UNSUITABLE NO-GO)'; gradeColor = const Color(0xFFEF4444); // Red } return HlzAssessmentResult( center: center, helicopterType: helicopterType, groundElevationM: centerElev, maxSlopePercent: math.min(100.0, (maxSlope * 10).round() / 10.0), avgSlopePercent: (avgSlope * 10).round() / 10.0, recommendedClearanceRadiusM: padRadiusM, isSlopeAcceptable: isSlopeAcceptable, isObstacleClear: isObstacleClear, suitabilityGrade: grade, gradeColor: gradeColor, approachAzimuthDeg: approachAzimuthDeg, padBoundary: padBoundary, approachFunnel: funnel, ); } }