Files
maps-saas/packages/tactical_app/lib/widgets/tactical_artillery_sheet.dart
T

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