544 lines
21 KiB
Dart
544 lines
21 KiB
Dart
import 'package:flutter/material.dart';
|
|
import 'package:intaleq_maps/intaleq_maps.dart';
|
|
import '../models/angle_unit.dart';
|
|
import '../models/military_operations_models.dart';
|
|
import '../services/artillery_ballistics_engine.dart';
|
|
import '../services/military_grid_utils.dart';
|
|
|
|
/// Dedicated Military Artillery Fire Mission HUD Sheet
|
|
class TacticalArtillerySheet extends StatefulWidget {
|
|
final LatLng? gunPosition;
|
|
final LatLng? targetPosition;
|
|
final AngleUnit angleUnit;
|
|
final VoidCallback onPickGun;
|
|
final VoidCallback onPickTarget;
|
|
final VoidCallback onSwap;
|
|
final VoidCallback onClose;
|
|
final Function(ArtilleryFiringSolution) onSolutionCalculated;
|
|
|
|
const TacticalArtillerySheet({
|
|
super.key,
|
|
required this.gunPosition,
|
|
required this.targetPosition,
|
|
required this.angleUnit,
|
|
required this.onPickGun,
|
|
required this.onPickTarget,
|
|
required this.onSwap,
|
|
required this.onClose,
|
|
required this.onSolutionCalculated,
|
|
});
|
|
|
|
@override
|
|
State<TacticalArtillerySheet> createState() => _TacticalArtillerySheetState();
|
|
}
|
|
|
|
class _TacticalArtillerySheetState extends State<TacticalArtillerySheet> {
|
|
ArtilleryWeaponSystem _selectedWeapon = ArtilleryWeaponSystem.standardSystems[0];
|
|
bool _highAngle = false;
|
|
bool _isLoading = false;
|
|
ArtilleryFiringSolution? _solution;
|
|
|
|
@override
|
|
void initState() {
|
|
super.initState();
|
|
_recalculate();
|
|
}
|
|
|
|
@override
|
|
void didUpdateWidget(covariant TacticalArtillerySheet oldWidget) {
|
|
super.didUpdateWidget(oldWidget);
|
|
if (widget.gunPosition != oldWidget.gunPosition ||
|
|
widget.targetPosition != oldWidget.targetPosition) {
|
|
_recalculate();
|
|
}
|
|
}
|
|
|
|
Future<void> _recalculate() async {
|
|
if (widget.gunPosition == null || widget.targetPosition == null) {
|
|
setState(() => _solution = null);
|
|
return;
|
|
}
|
|
|
|
setState(() => _isLoading = true);
|
|
|
|
try {
|
|
final sol = await ArtilleryBallisticsEngine.calculateFireMission(
|
|
weapon: _selectedWeapon,
|
|
gunPos: widget.gunPosition!,
|
|
targetPos: widget.targetPosition!,
|
|
highAngle: _highAngle,
|
|
);
|
|
|
|
if (mounted) {
|
|
setState(() {
|
|
_solution = sol;
|
|
_isLoading = false;
|
|
});
|
|
widget.onSolutionCalculated(sol);
|
|
}
|
|
} catch (e) {
|
|
if (mounted) setState(() => _isLoading = false);
|
|
}
|
|
}
|
|
|
|
@override
|
|
Widget build(BuildContext context) {
|
|
return Container(
|
|
decoration: const BoxDecoration(
|
|
color: Color(0xFF090E1A),
|
|
borderRadius: BorderRadius.vertical(top: Radius.circular(20)),
|
|
border: Border(top: BorderSide(color: Color(0xFF0071E3), width: 1.5)),
|
|
boxShadow: [
|
|
BoxShadow(color: Colors.black87, blurRadius: 24, spreadRadius: 4),
|
|
],
|
|
),
|
|
child: SafeArea(
|
|
top: false,
|
|
child: SingleChildScrollView(
|
|
padding: const EdgeInsets.all(16),
|
|
child: Column(
|
|
mainAxisSize: MainAxisSize.min,
|
|
crossAxisAlignment: CrossAxisAlignment.stretch,
|
|
children: [
|
|
// ── Header ───────────────────────────────────────────
|
|
Row(
|
|
children: [
|
|
Container(
|
|
padding: const EdgeInsets.all(8),
|
|
decoration: BoxDecoration(
|
|
color: const Color(0xFF0071E3).withAlpha(40),
|
|
borderRadius: BorderRadius.circular(10),
|
|
border: Border.all(color: const Color(0xFF0071E3)),
|
|
),
|
|
child: const Icon(Icons.gps_fixed, color: Color(0xFF38BDF8), size: 20),
|
|
),
|
|
const SizedBox(width: 10),
|
|
const Expanded(
|
|
child: Column(
|
|
crossAxisAlignment: CrossAxisAlignment.start,
|
|
children: [
|
|
Text(
|
|
'حساب رماية المدفعية وقوس القذيفة (Fire Mission)',
|
|
style: TextStyle(
|
|
color: Colors.white,
|
|
fontSize: 14,
|
|
fontWeight: FontWeight.bold,
|
|
),
|
|
),
|
|
Text(
|
|
'حل الرماية البالستية مع فحص قمم الجبال والعوائق التضاريسية',
|
|
style: TextStyle(color: Color(0xFF94A3B8), fontSize: 10),
|
|
),
|
|
],
|
|
),
|
|
),
|
|
IconButton(
|
|
icon: const Icon(Icons.close, color: Colors.white70, size: 20),
|
|
onPressed: widget.onClose,
|
|
),
|
|
],
|
|
),
|
|
|
|
const SizedBox(height: 14),
|
|
|
|
// ── Weapon Selector ───────────────────────────────────
|
|
Container(
|
|
padding: const EdgeInsets.all(16),
|
|
decoration: BoxDecoration(
|
|
color: const Color(0xFF0F172A),
|
|
borderRadius: BorderRadius.circular(10),
|
|
border: Border.all(color: Colors.white12),
|
|
),
|
|
child: DropdownButtonHideUnderline(
|
|
child: DropdownButton<ArtilleryWeaponSystem>(
|
|
value: _selectedWeapon,
|
|
dropdownColor: const Color(0xFF0F172A),
|
|
isExpanded: true,
|
|
icon: const Icon(Icons.keyboard_arrow_down, color: Color(0xFF38BDF8)),
|
|
items: ArtilleryWeaponSystem.standardSystems.map((w) {
|
|
return DropdownMenuItem(
|
|
value: w,
|
|
child: Text(
|
|
'${w.nameAr} • مدى ${w.maxRangeMeters ~/ 1000} كم',
|
|
style: const TextStyle(color: Colors.white, fontSize: 12),
|
|
),
|
|
);
|
|
}).toList(),
|
|
onChanged: (w) {
|
|
if (w != null) {
|
|
setState(() => _selectedWeapon = w);
|
|
_recalculate();
|
|
}
|
|
},
|
|
),
|
|
),
|
|
),
|
|
|
|
const SizedBox(height: 8),
|
|
|
|
// ── High Angle / Low Angle Toggle ────────────────────
|
|
Container(
|
|
decoration: BoxDecoration(
|
|
color: const Color(0xFF0F172A),
|
|
borderRadius: BorderRadius.circular(10),
|
|
border: Border.all(color: Colors.white12),
|
|
),
|
|
child: SwitchListTile(
|
|
value: _highAngle,
|
|
dense: true,
|
|
activeTrackColor: const Color(0xFFEF4444),
|
|
activeThumbColor: Colors.white,
|
|
title: const Text(
|
|
'قوس الرماية العالي (High-Angle Fire)',
|
|
style: TextStyle(color: Colors.white, fontSize: 11.5, fontWeight: FontWeight.bold),
|
|
),
|
|
subtitle: const Text(
|
|
'تجاوز الجبال الشاهقة والضرب في الوديان العميقة',
|
|
style: TextStyle(color: Color(0xFF94A3B8), fontSize: 9.5),
|
|
),
|
|
onChanged: (v) {
|
|
setState(() => _highAngle = v);
|
|
_recalculate();
|
|
},
|
|
),
|
|
),
|
|
|
|
const SizedBox(height: 12),
|
|
|
|
// ── Positions Picker Card ─────────────────────────────
|
|
Container(
|
|
padding: const EdgeInsets.all(12),
|
|
decoration: BoxDecoration(
|
|
color: const Color(0xFF0F172A),
|
|
borderRadius: BorderRadius.circular(12),
|
|
border: Border.all(color: Colors.white12),
|
|
),
|
|
child: Row(
|
|
children: [
|
|
Expanded(
|
|
child: InkWell(
|
|
onTap: widget.onPickGun,
|
|
borderRadius: BorderRadius.circular(8),
|
|
child: Container(
|
|
padding: const EdgeInsets.all(8),
|
|
decoration: BoxDecoration(
|
|
color: widget.gunPosition != null
|
|
? const Color(0x330071E3)
|
|
: Colors.white.withAlpha(10),
|
|
borderRadius: BorderRadius.circular(8),
|
|
border: Border.all(
|
|
color: widget.gunPosition != null
|
|
? const Color(0xFF0071E3)
|
|
: Colors.white24,
|
|
),
|
|
),
|
|
child: Column(
|
|
crossAxisAlignment: CrossAxisAlignment.start,
|
|
children: [
|
|
const Row(
|
|
children: [
|
|
Icon(Icons.adjust, color: Color(0xFF38BDF8), size: 14),
|
|
SizedBox(width: 4),
|
|
Text(
|
|
'مربض المدفعية (A):',
|
|
style: TextStyle(color: Color(0xFF94A3B8), fontSize: 10),
|
|
),
|
|
],
|
|
),
|
|
const SizedBox(height: 4),
|
|
Text(
|
|
widget.gunPosition != null
|
|
? MilitaryGridUtils.latLngToMgrs(
|
|
widget.gunPosition!.latitude,
|
|
widget.gunPosition!.longitude,
|
|
)
|
|
: 'حدد المربض...',
|
|
style: TextStyle(
|
|
color: widget.gunPosition != null ? Colors.white : Colors.white54,
|
|
fontSize: 11,
|
|
fontFamily: 'monospace',
|
|
fontWeight: FontWeight.bold,
|
|
),
|
|
),
|
|
],
|
|
),
|
|
),
|
|
),
|
|
),
|
|
IconButton(
|
|
icon: const Icon(Icons.swap_horiz, color: Color(0xFF38BDF8), size: 20),
|
|
onPressed: widget.onSwap,
|
|
),
|
|
Expanded(
|
|
child: InkWell(
|
|
onTap: widget.onPickTarget,
|
|
borderRadius: BorderRadius.circular(8),
|
|
child: Container(
|
|
padding: const EdgeInsets.all(8),
|
|
decoration: BoxDecoration(
|
|
color: widget.targetPosition != null
|
|
? const Color(0x33EF4444)
|
|
: Colors.white.withAlpha(10),
|
|
borderRadius: BorderRadius.circular(8),
|
|
border: Border.all(
|
|
color: widget.targetPosition != null
|
|
? const Color(0xFFEF4444)
|
|
: Colors.white24,
|
|
),
|
|
),
|
|
child: Column(
|
|
crossAxisAlignment: CrossAxisAlignment.start,
|
|
children: [
|
|
const Row(
|
|
children: [
|
|
Icon(Icons.track_changes, color: Color(0xFFEF4444), size: 14),
|
|
SizedBox(width: 4),
|
|
Text(
|
|
'الهدف المعادي (B):',
|
|
style: TextStyle(color: Color(0xFF94A3B8), fontSize: 10),
|
|
),
|
|
],
|
|
),
|
|
const SizedBox(height: 4),
|
|
Text(
|
|
widget.targetPosition != null
|
|
? MilitaryGridUtils.latLngToMgrs(
|
|
widget.targetPosition!.latitude,
|
|
widget.targetPosition!.longitude,
|
|
)
|
|
: 'حدد الهدف...',
|
|
style: TextStyle(
|
|
color: widget.targetPosition != null ? Colors.white : Colors.white54,
|
|
fontSize: 11,
|
|
fontFamily: 'monospace',
|
|
fontWeight: FontWeight.bold,
|
|
),
|
|
),
|
|
],
|
|
),
|
|
),
|
|
),
|
|
),
|
|
],
|
|
),
|
|
),
|
|
|
|
const SizedBox(height: 12),
|
|
|
|
// ── Ballistic Solution Output ─────────────────────────
|
|
if (_isLoading)
|
|
const Center(
|
|
child: Padding(
|
|
padding: EdgeInsets.all(16),
|
|
child: CircularProgressIndicator(color: Color(0xFF0071E3)),
|
|
),
|
|
)
|
|
else if (_solution != null) ...[
|
|
// Ballistic Trajectory Mini Graph
|
|
Container(
|
|
height: 120,
|
|
padding: const EdgeInsets.all(8),
|
|
decoration: BoxDecoration(
|
|
color: const Color(0xFF0F172A),
|
|
borderRadius: BorderRadius.circular(12),
|
|
border: Border.all(color: Colors.white12),
|
|
),
|
|
child: CustomPaint(
|
|
painter: _BallisticProfilePainter(solution: _solution!),
|
|
child: Container(),
|
|
),
|
|
),
|
|
|
|
const SizedBox(height: 12),
|
|
|
|
// Metrics Grid
|
|
Row(
|
|
children: [
|
|
_buildMetricCard(
|
|
title: 'السمت التكتيكي (Azimuth)',
|
|
value: '${_solution!.azimuthMilsNato.toInt()} ₥',
|
|
subvalue: '${_solution!.azimuthDeg.toStringAsFixed(1)}°',
|
|
color: const Color(0xFF38BDF8),
|
|
),
|
|
const SizedBox(width: 8),
|
|
_buildMetricCard(
|
|
title: 'زاوية الارتفاع (QE)',
|
|
value: '${_solution!.quadrantElevationMilsNato.toInt()} ₥',
|
|
subvalue: '${_solution!.quadrantElevationDeg.toStringAsFixed(1)}°',
|
|
color: const Color(0xFFF59E0B),
|
|
),
|
|
],
|
|
),
|
|
|
|
const SizedBox(height: 8),
|
|
|
|
Row(
|
|
children: [
|
|
_buildMetricCard(
|
|
title: 'المسافة المباشرة',
|
|
value: '${(_solution!.distanceMeters / 1000).toStringAsFixed(2)} كم',
|
|
subvalue: '${_solution!.distanceMeters.toInt()} متر',
|
|
color: Colors.white,
|
|
),
|
|
const SizedBox(width: 8),
|
|
_buildMetricCard(
|
|
title: 'زمن الطيران / الذروة',
|
|
value: '${_solution!.timeOfFlightSeconds.toStringAsFixed(1)} ثانية',
|
|
subvalue: 'الذروة: ${_solution!.apogeeAltitudeMeters.toInt()}م',
|
|
color: const Color(0xFF10B981),
|
|
),
|
|
],
|
|
),
|
|
|
|
const SizedBox(height: 10),
|
|
|
|
// Crest Clearance Status
|
|
Container(
|
|
padding: const EdgeInsets.all(16),
|
|
decoration: BoxDecoration(
|
|
color: _solution!.isCrestClear
|
|
? const Color(0x3310B981)
|
|
: const Color(0x33EF4444),
|
|
borderRadius: BorderRadius.circular(8),
|
|
border: Border.all(
|
|
color: _solution!.isCrestClear
|
|
? const Color(0xFF10B981)
|
|
: const Color(0xFFEF4444),
|
|
),
|
|
),
|
|
child: Row(
|
|
children: [
|
|
Icon(
|
|
_solution!.isCrestClear ? Icons.check_circle : Icons.warning,
|
|
color: _solution!.isCrestClear
|
|
? const Color(0xFF10B981)
|
|
: const Color(0xFFEF4444),
|
|
size: 16,
|
|
),
|
|
const SizedBox(width: 8),
|
|
Expanded(
|
|
child: Text(
|
|
_solution!.isCrestClear
|
|
? 'مسار القذيفة آمن ويعلو قمم الجبال بـ ${_solution!.minCrestClearanceMeters.toInt()}م'
|
|
: 'تحذير: القذيفة تصطدم بقمة جبل عائقة في مسار الرماية!',
|
|
style: TextStyle(
|
|
color: _solution!.isCrestClear
|
|
? const Color(0xFF10B981)
|
|
: const Color(0xFFEF4444),
|
|
fontSize: 11,
|
|
fontWeight: FontWeight.bold,
|
|
),
|
|
),
|
|
),
|
|
],
|
|
),
|
|
),
|
|
],
|
|
],
|
|
),
|
|
),
|
|
),
|
|
);
|
|
}
|
|
|
|
Widget _buildMetricCard({
|
|
required String title,
|
|
required String value,
|
|
required String subvalue,
|
|
required Color color,
|
|
}) {
|
|
return Expanded(
|
|
child: Container(
|
|
padding: const EdgeInsets.all(10),
|
|
decoration: BoxDecoration(
|
|
color: const Color(0xFF0F172A),
|
|
borderRadius: BorderRadius.circular(10),
|
|
border: Border.all(color: Colors.white12),
|
|
),
|
|
child: Column(
|
|
crossAxisAlignment: CrossAxisAlignment.start,
|
|
children: [
|
|
Text(title, style: const TextStyle(color: Color(0xFF94A3B8), fontSize: 9.5)),
|
|
const SizedBox(height: 3),
|
|
Text(
|
|
value,
|
|
style: TextStyle(color: color, fontSize: 14, fontWeight: FontWeight.bold),
|
|
),
|
|
Text(subvalue, style: const TextStyle(color: Colors.white54, fontSize: 9.5)),
|
|
],
|
|
),
|
|
),
|
|
);
|
|
}
|
|
}
|
|
|
|
/// Canvas Painter for Artillery Ballistic Arc Profile
|
|
class _BallisticProfilePainter extends CustomPainter {
|
|
final ArtilleryFiringSolution solution;
|
|
|
|
_BallisticProfilePainter({required this.solution});
|
|
|
|
@override
|
|
void paint(Canvas canvas, Size size) {
|
|
if (solution.trajectoryProfile.isEmpty) return;
|
|
|
|
double minAlt = double.infinity;
|
|
double maxAlt = -double.infinity;
|
|
|
|
for (final p in solution.trajectoryProfile) {
|
|
if (p.groundElevationMeters < minAlt) minAlt = p.groundElevationMeters;
|
|
if (p.altitudeMeters > maxAlt) maxAlt = p.altitudeMeters;
|
|
}
|
|
|
|
final altRange = (maxAlt - minAlt).clamp(50.0, 10000.0);
|
|
|
|
final terrainPath = Path();
|
|
final arcPath = Path();
|
|
|
|
for (int i = 0; i < solution.trajectoryProfile.length; i++) {
|
|
final p = solution.trajectoryProfile[i];
|
|
final x = (i / (solution.trajectoryProfile.length - 1)) * size.width;
|
|
final terrainY = size.height - ((p.groundElevationMeters - minAlt) / altRange * (size.height - 20)) - 10;
|
|
final arcY = size.height - ((p.altitudeMeters - minAlt) / altRange * (size.height - 20)) - 10;
|
|
|
|
if (i == 0) {
|
|
terrainPath.moveTo(x, terrainY);
|
|
arcPath.moveTo(x, arcY);
|
|
} else {
|
|
terrainPath.lineTo(x, terrainY);
|
|
arcPath.lineTo(x, arcY);
|
|
}
|
|
}
|
|
|
|
// Draw Terrain Fill
|
|
final terrainFill = Path.from(terrainPath)
|
|
..lineTo(size.width, size.height)
|
|
..lineTo(0, size.height)
|
|
..close();
|
|
|
|
final terrainPaint = Paint()
|
|
..shader = LinearGradient(
|
|
begin: Alignment.topCenter,
|
|
end: Alignment.bottomCenter,
|
|
colors: [const Color(0xFF475569).withAlpha(120), const Color(0xFF1E293B).withAlpha(40)],
|
|
).createShader(Rect.fromLTWH(0, 0, size.width, size.height));
|
|
canvas.drawPath(terrainFill, terrainPaint);
|
|
|
|
final terrainLinePaint = Paint()
|
|
..color = const Color(0xFF94A3B8)
|
|
..strokeWidth = 1.5
|
|
..style = PaintingStyle.stroke;
|
|
canvas.drawPath(terrainPath, terrainLinePaint);
|
|
|
|
// Draw Ballistic Arc
|
|
final arcPaint = Paint()
|
|
..color = solution.isCrestClear ? const Color(0xFF00F0FF) : const Color(0xFFEF4444)
|
|
..strokeWidth = 2.0
|
|
..style = PaintingStyle.stroke;
|
|
canvas.drawPath(arcPath, arcPaint);
|
|
}
|
|
|
|
@override
|
|
bool shouldRepaint(covariant _BallisticProfilePainter oldDelegate) => true;
|
|
}
|