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

949 lines
38 KiB
Dart

import 'dart:async';
import 'dart:math' as math;
import 'package:camera/camera.dart';
import 'package:flutter/material.dart';
import 'package:flutter_compass/flutter_compass.dart';
import 'package:sensors_plus/sensors_plus.dart';
import '../models/angle_unit.dart';
import '../models/landmark.dart';
import '../services/landmark_database.dart';
import '../services/local_sqlite_db.dart';
import '../services/resection_calculator.dart';
import '../services/tactical_api_service.dart';
import 'tactical_crosshair.dart';
class CameraResectionView extends StatefulWidget {
final List<ResectionObservation> observations;
final AngleUnit angleUnit;
final Function(ResectionObservation) onObservationAdded;
final VoidCallback onCalculatePressed;
final VoidCallback onResetPressed;
final VoidCallback? onPickFromMap;
final VoidCallback onClose;
final TacticalLandmark? initialSelectedLandmark;
const CameraResectionView({
super.key,
required this.observations,
required this.angleUnit,
required this.onObservationAdded,
required this.onCalculatePressed,
required this.onResetPressed,
this.onPickFromMap,
required this.onClose,
this.initialSelectedLandmark,
});
@override
State<CameraResectionView> createState() => _CameraResectionViewState();
}
class _CameraResectionViewState extends State<CameraResectionView> {
CameraController? _cameraController;
bool _isCameraReady = false;
double _headingDeg = 0.0;
bool _isLockedAnim = false;
double _currentZoom = 1.0;
double _minZoom = 1.0;
double _maxZoom = 8.0;
double _baseScale = 1.0;
// ── Single Landmark 15m Mode State ─────────────────────────────────────────
bool _isSingleLandmarkMode = false;
int _singlePhase = 0; // 0: Aim 1, 1: Walk 15m, 2: Aim 2
double _firstAzimuth = 0.0;
double _secondAzimuth = 0.0;
double _walkedDistanceMeters = 0.0;
int _stepCount = 0;
dynamic _accelSubscription;
DateTime _lastStepTime = DateTime.now();
// Search state
final TextEditingController _searchController = TextEditingController();
List<TacticalLandmark> _searchResults = [];
bool _isSearching = false;
TacticalLandmark? _selectedLandmark;
@override
void initState() {
super.initState();
_initCamera();
_initCompass();
_initDefaultLandmark();
}
void _initDefaultLandmark() {
if (widget.initialSelectedLandmark != null) {
_selectedLandmark = widget.initialSelectedLandmark;
_searchResults = [widget.initialSelectedLandmark!, ...LandmarkDatabase.allLandmarks.take(9)];
return;
}
final all = LandmarkDatabase.allLandmarks;
if (all.isNotEmpty) {
_selectedLandmark = all[0];
_searchResults = all.take(10).toList();
}
}
Future<void> _initCamera() async {
try {
final cameras = await availableCameras();
if (cameras.isNotEmpty) {
_cameraController = CameraController(
cameras.first,
ResolutionPreset.high,
enableAudio: false,
);
await _cameraController!.initialize();
_minZoom = await _cameraController!.getMinZoomLevel();
_maxZoom = math.min(10.0, await _cameraController!.getMaxZoomLevel());
_currentZoom = _minZoom;
if (mounted) {
setState(() => _isCameraReady = true);
}
}
} catch (e) {
debugPrint('Camera init exception: $e');
}
}
Future<void> _setZoom(double zoom) async {
final clamped = zoom.clamp(_minZoom, _maxZoom);
if (clamped != _currentZoom) {
setState(() => _currentZoom = clamped);
try {
await _cameraController?.setZoomLevel(clamped);
} catch (_) {}
}
}
bool _isFrameFrozen = false;
void _initCompass() {
FlutterCompass.events?.listen((event) {
if (event.heading != null && mounted) {
if (_isFrameFrozen) return; // Keep heading locked while frame is frozen
final raw = (event.heading! + 360) % 360;
final diff = ((raw - _headingDeg + 180) % 360) - 180;
final smoothed = (_headingDeg + diff * 0.35 + 360) % 360;
setState(() {
_headingDeg = smoothed;
});
}
});
}
Future<void> _toggleFreezeFrame() async {
if (_isFrameFrozen) {
try {
await _cameraController?.resumePreview();
} catch (_) {}
if (mounted) {
setState(() => _isFrameFrozen = false);
}
} else {
try {
await _cameraController?.pausePreview();
if (mounted) {
setState(() => _isFrameFrozen = true);
}
} catch (e) {
debugPrint('Freeze frame pause error: $e');
}
}
}
@override
void dispose() {
_accelSubscription?.cancel();
_cameraController?.dispose();
_searchController.dispose();
super.dispose();
}
Future<void> _onSearchChanged(String query) async {
final cleanQuery = query.trim();
if (cleanQuery.isEmpty) {
setState(() {
_searchResults = LandmarkDatabase.allLandmarks.take(10).toList();
_isSearching = false;
});
return;
}
setState(() => _isSearching = true);
List<TacticalLandmark> dbResults = [];
try {
dbResults = await LocalSqliteDb.searchPlaces(cleanQuery, limit: 30);
} catch (_) {}
final staticResults = LandmarkDatabase.search(cleanQuery);
List<TacticalLandmark> onlineResults = [];
if (dbResults.length < 5) {
try {
onlineResults = await TacticalApiService.searchOnlinePlaces(cleanQuery);
} catch (_) {}
}
final combined = <TacticalLandmark>[...dbResults, ...onlineResults, ...staticResults];
final seen = <String>{};
final unique = <TacticalLandmark>[];
for (final r in combined) {
if (seen.add('${r.name}_${r.lat}_${r.lng}')) {
unique.add(r);
}
}
if (mounted) {
setState(() {
_searchResults = unique;
_isSearching = false;
if (_searchResults.isNotEmpty && !_searchResults.any((l) => l.id == _selectedLandmark?.id)) {
_selectedLandmark = _searchResults[0];
}
});
}
}
void _startPedometer() {
_stepCount = 0;
_walkedDistanceMeters = 0.0;
_accelSubscription?.cancel();
_accelSubscription = accelerometerEventStream().listen((event) {
final mag = math.sqrt(event.x * event.x + event.y * event.y + event.z * event.z);
final now = DateTime.now();
if (mag > 11.5 && now.difference(_lastStepTime).inMilliseconds > 300) {
_lastStepTime = now;
if (mounted) {
setState(() {
_stepCount++;
_walkedDistanceMeters = math.min(15.0, _stepCount * 0.75);
if (_walkedDistanceMeters >= 15.0 && _singlePhase == 1) {
_singlePhase = 2;
}
});
}
}
});
}
void _lockSingleLandmarkPhase() {
if (_selectedLandmark == null) return;
if (_singlePhase == 0) {
_firstAzimuth = _headingDeg;
_singlePhase = 1;
_startPedometer();
setState(() => _isLockedAnim = true);
Future.delayed(const Duration(milliseconds: 300), () {
if (mounted) setState(() => _isLockedAnim = false);
});
} else if (_singlePhase == 1) {
_singlePhase = 2;
_accelSubscription?.cancel();
setState(() {});
} else if (_singlePhase == 2) {
_secondAzimuth = _headingDeg;
_accelSubscription?.cancel();
final obs = ResectionObservation(
landmark: _selectedLandmark!,
observedAzimuthDeg: _firstAzimuth,
trueAzimuthDeg: ResectionCalculator.getTrueAzimuth(_firstAzimuth),
);
final obs2 = ResectionObservation(
landmark: _selectedLandmark!,
observedAzimuthDeg: _secondAzimuth,
trueAzimuthDeg: ResectionCalculator.getTrueAzimuth(_secondAzimuth),
);
widget.onObservationAdded(obs);
widget.onObservationAdded(obs2);
setState(() => _isLockedAnim = true);
Future.delayed(const Duration(milliseconds: 400), () {
if (mounted) {
widget.onCalculatePressed();
}
});
}
}
void _lockCurrentObservation() {
if (_isSingleLandmarkMode) {
_lockSingleLandmarkPhase();
return;
}
if (_selectedLandmark == null) return;
final trueAzimuth = ResectionCalculator.getTrueAzimuth(_headingDeg);
final obs = ResectionObservation(
landmark: _selectedLandmark!,
observedAzimuthDeg: _headingDeg,
trueAzimuthDeg: trueAzimuth,
);
setState(() => _isLockedAnim = true);
Future.delayed(const Duration(milliseconds: 300), () {
if (mounted) setState(() => _isLockedAnim = false);
});
widget.onObservationAdded(obs);
// Auto-advance to map calculation if we have 2 observations
final nextObsCount = widget.observations.length +
(widget.observations.any((o) => o.landmark.id == obs.landmark.id) ? 0 : 1);
if (nextObsCount >= 2) {
Future.delayed(const Duration(milliseconds: 400), () {
if (mounted) {
widget.onCalculatePressed();
}
});
return;
}
// Reset search for second landmark
_searchController.clear();
setState(() {
_searchResults = LandmarkDatabase.allLandmarks.where((l) => l.id != _selectedLandmark!.id).take(10).toList();
if (_searchResults.isNotEmpty) {
_selectedLandmark = _searchResults[0];
}
});
}
@override
Widget build(BuildContext context) {
final trueAzimuth = ResectionCalculator.getTrueAzimuth(_headingDeg);
final obsCount = widget.observations.length;
return Scaffold(
backgroundColor: Colors.black,
body: Stack(
children: [
// ── 1. Camera Feed / Viewfinder Background with Pinch Zoom ─
if (_isCameraReady && _cameraController != null)
GestureDetector(
onScaleStart: (_) => _baseScale = _currentZoom,
onScaleUpdate: (details) => _setZoom(_baseScale * details.scale),
child: SizedBox.expand(
child: FittedBox(
fit: BoxFit.cover,
child: SizedBox(
width: _cameraController!.value.previewSize?.height ?? 1,
height: _cameraController!.value.previewSize?.width ?? 1,
child: CameraPreview(_cameraController!),
),
),
),
)
else
Container(
decoration: const BoxDecoration(
gradient: RadialGradient(
center: Alignment.center,
radius: 1.2,
colors: [Color(0xFF0F172A), Color(0xFF020617)],
),
),
child: const Center(
child: Icon(Icons.camera_alt, size: 48, color: Color(0xFF38BDF8)),
),
),
// ── Controls on Right Side (Freeze Frame & Zoom Slider) ───
if (_isCameraReady)
Positioned(
right: 14,
top: 210,
bottom: 230,
child: Column(
children: [
// Freeze Frame Button
GestureDetector(
onTap: _toggleFreezeFrame,
child: Container(
width: 38,
height: 38,
decoration: BoxDecoration(
color: _isFrameFrozen ? const Color(0xFFEF4444) : const Color(0xFF090E17).withAlpha(200),
shape: BoxShape.circle,
border: Border.all(
color: _isFrameFrozen ? const Color(0xFFFF7171) : const Color(0xFF00F0FF),
width: 1.5,
),
boxShadow: [
if (_isFrameFrozen)
const BoxShadow(color: Color(0x99EF4444), blurRadius: 10),
],
),
child: Icon(
_isFrameFrozen ? Icons.pause : Icons.camera_alt_outlined,
color: Colors.white,
size: 18,
),
),
),
const SizedBox(height: 8),
// Zoom Slider Container
Expanded(
child: Container(
width: 38,
padding: const EdgeInsets.symmetric(vertical: 6),
decoration: BoxDecoration(
color: const Color(0xFF090E17).withAlpha(200),
borderRadius: BorderRadius.circular(18),
border: Border.all(color: Colors.white24),
),
child: Column(
children: [
const Text('8x', style: TextStyle(color: Colors.white54, fontSize: 8.5)),
Expanded(
child: RotatedBox(
quarterTurns: 3,
child: SliderTheme(
data: SliderThemeData(
trackHeight: 2.5,
thumbShape: const RoundSliderThumbShape(enabledThumbRadius: 6),
activeTrackColor: const Color(0xFF00F0FF),
inactiveTrackColor: Colors.white24,
thumbColor: Colors.white,
),
child: Slider(
value: _currentZoom,
min: _minZoom,
max: _maxZoom,
onChanged: (val) => _setZoom(val),
),
),
),
),
const Text('1x', style: TextStyle(color: Colors.white54, fontSize: 8.5)),
],
),
),
),
],
),
),
// ── Frozen Frame Indicator Banner ─────────────────────────
if (_isFrameFrozen)
Positioned(
top: 155,
left: 20,
right: 20,
child: Center(
child: Container(
padding: const EdgeInsets.symmetric(horizontal: 14, vertical: 6),
decoration: BoxDecoration(
color: const Color(0xEEEF4444),
borderRadius: BorderRadius.circular(10),
boxShadow: const [BoxShadow(color: Color(0x66EF4444), blurRadius: 10)],
),
child: const Row(
mainAxisSize: MainAxisSize.min,
children: [
Icon(Icons.ac_unit, color: Colors.white, size: 14),
SizedBox(width: 6),
Text(
'تم تجميد الصورة لمنع الاهتزاز — سدد واقفل الرصد',
style: TextStyle(color: Colors.white, fontSize: 11, fontWeight: FontWeight.bold),
),
],
),
),
),
),
// ── 2. Tactical Military Crosshair & Reticle ────────────────
TacticalCrosshair(
headingDeg: _headingDeg,
trueAzimuthDeg: trueAzimuth,
angleUnit: widget.angleUnit,
isAimLocked: _isLockedAnim,
targetName: _selectedLandmark?.name,
),
// ── 3. Top Header: Step Flow + Search Bar ────────────────────
Positioned(
top: 44,
left: 16,
right: 16,
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
// Top Action Row
Row(
mainAxisAlignment: MainAxisAlignment.spaceBetween,
children: [
// Step Indicator Badge
Container(
padding: const EdgeInsets.symmetric(horizontal: 12, vertical: 6),
decoration: BoxDecoration(
color: const Color(0xF00F172A),
borderRadius: BorderRadius.circular(20),
border: Border.all(
color: (_isSingleLandmarkMode ? _singlePhase >= 2 : obsCount >= 2)
? const Color(0xFF22C55E)
: const Color(0xFF0071E3),
width: 1.5,
),
),
child: Row(
children: [
Icon(
(_isSingleLandmarkMode ? _singlePhase >= 2 : obsCount >= 2)
? Icons.check_circle
: Icons.gps_fixed,
size: 15,
color: (_isSingleLandmarkMode ? _singlePhase >= 2 : obsCount >= 2)
? const Color(0xFF4ADE80)
: const Color(0xFF38BDF8),
),
const SizedBox(width: 6),
Text(
_isSingleLandmarkMode
? (_singlePhase == 0
? '1. سدد على المعلم واقفل الزاوية الأولى'
: (_singlePhase == 1
? '2. تحرك 15 متراً عمودياً على خط النظر'
: '3. سدد مرة أخرى واقفل الزاوية الثانية'))
: (obsCount == 0
? '1. ابحث وسدد على المعلم الأول'
: (obsCount == 1
? '2. ابحث وسدد على المعلم الثاني'
: 'تم رصد $obsCount معالم • جاري الحساب')),
style: TextStyle(
color: (_isSingleLandmarkMode ? _singlePhase >= 2 : obsCount >= 2)
? const Color(0xFF4ADE80)
: Colors.white,
fontSize: 11,
fontWeight: FontWeight.bold,
),
),
],
),
),
// Close Button
IconButton.filled(
onPressed: widget.onClose,
icon: const Icon(Icons.close, size: 18),
style: IconButton.styleFrom(
backgroundColor: const Color(0xCC0F172A),
foregroundColor: Colors.white,
),
),
],
),
const SizedBox(height: 8),
// Mode Selector Toggle (رصد متعدد vs معلم واحد + 15م بالحساسات)
Container(
padding: const EdgeInsets.all(3),
decoration: BoxDecoration(
color: const Color(0xCC0F172A),
borderRadius: BorderRadius.circular(12),
border: Border.all(color: Colors.white12),
),
child: Row(
children: [
Expanded(
child: GestureDetector(
onTap: () {
setState(() {
_isSingleLandmarkMode = false;
_singlePhase = 0;
_accelSubscription?.cancel();
});
},
child: Container(
padding: const EdgeInsets.symmetric(vertical: 6),
decoration: BoxDecoration(
color: !_isSingleLandmarkMode ? const Color(0xFF0071E3) : Colors.transparent,
borderRadius: BorderRadius.circular(9),
),
alignment: Alignment.center,
child: Text(
'🎯 تقاطع متعدد (2-3 معالم)',
style: TextStyle(
color: !_isSingleLandmarkMode ? Colors.white : const Color(0xFF94A3B8),
fontSize: 11,
fontWeight: FontWeight.bold,
),
),
),
),
),
Expanded(
child: GestureDetector(
onTap: () {
setState(() {
_isSingleLandmarkMode = true;
_singlePhase = 0;
});
},
child: Container(
padding: const EdgeInsets.symmetric(vertical: 6),
decoration: BoxDecoration(
color: _isSingleLandmarkMode ? const Color(0xFF10B981) : Colors.transparent,
borderRadius: BorderRadius.circular(9),
),
alignment: Alignment.center,
child: Text(
'📏 معلم واحد (خط أساس 15م)',
style: TextStyle(
color: _isSingleLandmarkMode ? Colors.white : const Color(0xFF94A3B8),
fontSize: 11,
fontWeight: FontWeight.bold,
),
),
),
),
),
],
),
),
const SizedBox(height: 8),
// Interactive Map Picker Trigger Button
if (widget.onPickFromMap != null)
Padding(
padding: const EdgeInsets.only(bottom: 8.0),
child: Material(
color: Colors.transparent,
child: InkWell(
onTap: widget.onPickFromMap,
borderRadius: BorderRadius.circular(12),
child: Container(
padding: const EdgeInsets.symmetric(horizontal: 14, vertical: 8),
decoration: BoxDecoration(
gradient: const LinearGradient(
colors: [Color(0xFF7C3AED), Color(0xFF4F46E5)],
),
borderRadius: BorderRadius.circular(12),
border: Border.all(color: const Color(0xFFA78BFA), width: 1.2),
boxShadow: const [
BoxShadow(
color: Color(0x667C3AED),
blurRadius: 8,
offset: Offset(0, 2),
),
],
),
child: const Row(
mainAxisAlignment: MainAxisAlignment.center,
children: [
Icon(Icons.touch_app, color: Colors.white, size: 16),
SizedBox(width: 8),
Text(
'📍 تأشير واختيار المعلم مباشرة من الخريطة',
style: TextStyle(
color: Colors.white,
fontSize: 11.5,
fontWeight: FontWeight.bold,
),
),
],
),
),
),
),
),
// Unified Place / Mosque / Landmark Search Field
Container(
decoration: BoxDecoration(
color: const Color(0xF50F172A),
borderRadius: BorderRadius.circular(14),
border: Border.all(color: Colors.white24),
),
child: TextField(
controller: _searchController,
onChanged: _onSearchChanged,
style: const TextStyle(color: Colors.white, fontSize: 13),
decoration: InputDecoration(
hintText: 'ابحث عن معلم، مئذنة، مدرسة، قمة جبل...',
hintStyle: const TextStyle(color: Color(0xFF64748B), fontSize: 12),
prefixIcon: const Icon(Icons.search, color: Color(0xFF38BDF8), size: 18),
suffixIcon: _searchController.text.isNotEmpty
? IconButton(
icon: const Icon(Icons.clear, color: Color(0xFF94A3B8), size: 16),
onPressed: () {
_searchController.clear();
_onSearchChanged('');
},
)
: null,
contentPadding: const EdgeInsets.symmetric(vertical: 10),
border: InputBorder.none,
),
),
),
if (_isSearching) ...[
const SizedBox(height: 4),
const LinearProgressIndicator(minHeight: 2, color: Color(0xFF0071E3), backgroundColor: Colors.transparent),
],
// Search Results Dropdown List (if search has text or active)
if (_searchController.text.isNotEmpty && _searchResults.isNotEmpty)
Container(
margin: const EdgeInsets.only(top: 6),
constraints: const BoxConstraints(maxHeight: 180),
decoration: BoxDecoration(
color: const Color(0xF80F172A),
borderRadius: BorderRadius.circular(14),
border: Border.all(color: Colors.white12),
boxShadow: const [BoxShadow(color: Colors.black87, blurRadius: 20)],
),
child: ListView.separated(
padding: const EdgeInsets.symmetric(vertical: 4),
shrinkWrap: true,
itemCount: _searchResults.length,
separatorBuilder: (_, __) => const Divider(color: Colors.white10, height: 1),
itemBuilder: (context, idx) {
final lm = _searchResults[idx];
final isSelected = lm.id == _selectedLandmark?.id;
return ListTile(
dense: true,
visualDensity: VisualDensity.compact,
leading: Icon(
lm.typeIcon,
color: isSelected ? const Color(0xFF38BDF8) : const Color(0xFF94A3B8),
size: 18,
),
title: Text(
lm.name,
style: TextStyle(
color: isSelected ? const Color(0xFF38BDF8) : Colors.white,
fontSize: 12,
fontWeight: FontWeight.bold,
),
),
subtitle: Text(
'${lm.region} • ${lm.elevationM}م',
style: const TextStyle(color: Color(0xFF64748B), fontSize: 10),
),
trailing: isSelected ? const Icon(Icons.check, color: Color(0xFF38BDF8), size: 16) : null,
onTap: () {
setState(() => _selectedLandmark = lm);
_searchController.text = lm.name;
},
);
},
),
),
],
),
),
// ── 4. Bottom HUD: Active Target Details & Lock Action ───────
Positioned(
bottom: 24,
left: 16,
right: 16,
child: Container(
padding: const EdgeInsets.all(14),
decoration: BoxDecoration(
color: const Color(0xF50F172A),
borderRadius: BorderRadius.circular(20),
border: Border.all(
color: _isSingleLandmarkMode ? const Color(0xFF10B981) : const Color(0xFF0071E3),
width: 1.5,
),
boxShadow: const [BoxShadow(color: Colors.black87, blurRadius: 25)],
),
child: Column(
mainAxisSize: MainAxisSize.min,
children: [
// Live Pedometer & Sensor 15m Guidance Card (When in Single Landmark Phase 1)
if (_isSingleLandmarkMode && _singlePhase == 1)
Container(
margin: const EdgeInsets.only(bottom: 12),
padding: const EdgeInsets.all(12),
decoration: BoxDecoration(
gradient: const LinearGradient(
colors: [Color(0x3310B981), Color(0x22059669)],
),
borderRadius: BorderRadius.circular(14),
border: Border.all(color: const Color(0xFF10B981), width: 1.2),
),
child: Column(
children: [
Row(
mainAxisAlignment: MainAxisAlignment.spaceBetween,
children: [
Row(
children: [
const Icon(Icons.directions_walk, color: Color(0xFF34D399), size: 20),
const SizedBox(width: 8),
Text(
'تحرك 15 متراً عمودياً على خط النظر ($_stepCount خطوة)',
style: const TextStyle(
color: Colors.white,
fontSize: 11.5,
fontWeight: FontWeight.bold,
),
),
],
),
Text(
'${_walkedDistanceMeters.toStringAsFixed(1)}م / 15.0م',
style: const TextStyle(
color: Color(0xFF34D399),
fontSize: 13,
fontWeight: FontWeight.w900,
),
),
],
),
const SizedBox(height: 8),
ClipRRect(
borderRadius: BorderRadius.circular(6),
child: LinearProgressIndicator(
value: (_walkedDistanceMeters / 15.0).clamp(0.0, 1.0),
minHeight: 8,
backgroundColor: Colors.white12,
valueColor: const AlwaysStoppedAnimation<Color>(Color(0xFF10B981)),
),
),
],
),
),
// Targeted Landmark Card
if (_selectedLandmark != null)
Container(
margin: const EdgeInsets.only(bottom: 12),
padding: const EdgeInsets.symmetric(horizontal: 12, vertical: 8),
decoration: BoxDecoration(
color: const Color(0x661E293B),
borderRadius: BorderRadius.circular(12),
border: Border.all(color: Colors.white12),
),
child: Row(
children: [
Icon(_selectedLandmark!.typeIcon, color: const Color(0xFF38BDF8), size: 22),
const SizedBox(width: 10),
Expanded(
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Text(
_selectedLandmark!.name,
style: const TextStyle(
color: Colors.white,
fontSize: 13,
fontWeight: FontWeight.bold,
),
overflow: TextOverflow.ellipsis,
),
Text(
'${_selectedLandmark!.region} • إحداثيات: ${_selectedLandmark!.lat.toStringAsFixed(4)}, ${_selectedLandmark!.lng.toStringAsFixed(4)}',
style: const TextStyle(color: Color(0xFF94A3B8), fontSize: 10),
),
],
),
),
Container(
padding: const EdgeInsets.symmetric(horizontal: 8, vertical: 4),
decoration: BoxDecoration(
color: const Color(0x330071E3),
borderRadius: BorderRadius.circular(8),
),
child: Text(
AngleFormatter.format(trueAzimuth, widget.angleUnit, includeLabel: false),
style: const TextStyle(
color: Color(0xFF38BDF8),
fontSize: 12,
fontWeight: FontWeight.w900,
),
),
),
],
),
),
// Main Lock / Action Button
Row(
children: [
Expanded(
child: ElevatedButton.icon(
onPressed: _selectedLandmark != null ? _lockCurrentObservation : null,
icon: Icon(
_isSingleLandmarkMode
? (_singlePhase == 1 ? Icons.check : (_singlePhase == 2 ? Icons.my_location : Icons.lock_outline))
: (obsCount >= 1 ? Icons.my_location : Icons.lock_outline),
size: 18,
),
label: Text(
_isSingleLandmarkMode
? (_singlePhase == 0
? 'تثبيت الرصد الأول وبدء خط الأساس 15م'
: (_singlePhase == 1
? 'وصلت 15 متراً (انتقل لتسديد الرصد الثاني)'
: 'تثبيت الرصد الثاني (حساب المسافة والموقع فوراً)'))
: (obsCount == 0
? 'تثبيت رصد المعلم الأول'
: 'تثبيت رصد المعلم الثاني (استخراج الموقع فوراً)'),
style: const TextStyle(fontSize: 12, fontWeight: FontWeight.w900),
),
style: ElevatedButton.styleFrom(
backgroundColor: _isSingleLandmarkMode
? (_singlePhase == 1 ? const Color(0xFFF59E0B) : const Color(0xFF10B981))
: (obsCount >= 1 ? const Color(0xFF22C55E) : const Color(0xFF0071E3)),
foregroundColor: Colors.white,
padding: const EdgeInsets.symmetric(vertical: 13),
shape: RoundedRectangleBorder(borderRadius: BorderRadius.circular(12)),
elevation: 4,
),
),
),
if (obsCount > 0 || (_isSingleLandmarkMode && _singlePhase > 0)) ...[
const SizedBox(width: 8),
IconButton.filledTonal(
onPressed: () {
if (_isSingleLandmarkMode) {
setState(() {
_singlePhase = 0;
_stepCount = 0;
_walkedDistanceMeters = 0.0;
_accelSubscription?.cancel();
});
} else {
widget.onResetPressed();
}
},
icon: const Icon(Icons.refresh, size: 18),
style: IconButton.styleFrom(
backgroundColor: const Color(0x33EF4444),
foregroundColor: const Color(0xFFF87171),
),
),
],
],
),
],
),
),
),
],
),
);
}
}