137 lines
5.6 KiB
Dart
137 lines
5.6 KiB
Dart
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<HlzAssessmentResult> 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<double> perimeterElevs = [];
|
|
final List<LatLng> 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<LatLng> 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,
|
|
);
|
|
}
|
|
}
|