2026-07-06 11:47:41 +02:00
|
|
|
|
import 'dart:math' as math;
|
|
|
|
|
|
import 'dart:ui' show FontFeature;
|
|
|
|
|
|
|
|
|
|
|
|
import 'package:flutter/material.dart';
|
2026-07-08 22:13:30 +02:00
|
|
|
|
//import 'package:flutter/widget_previews.dart';
|
2026-07-06 11:47:41 +02:00
|
|
|
|
import 'package:get/get.dart';
|
2026-07-06 14:26:16 +02:00
|
|
|
|
import 'package:terepi_seged/pages/map_survey/presentations/widgets/stakeout_list_sheet.dart';
|
|
|
|
|
|
import 'package:terepi_seged/pages/map_survey/presentations/widgets/stakeout_settings_dialog.dart';
|
2026-07-07 02:48:37 +02:00
|
|
|
|
import 'package:terepi_seged/pages/map_survey/presentations/widgets/tilt_bubble.dart';
|
2026-07-06 11:47:41 +02:00
|
|
|
|
import 'package:terepi_seged/routes/app_pages.dart';
|
|
|
|
|
|
import 'package:terepi_seged/services/gnss/gnss_service.dart';
|
|
|
|
|
|
import 'package:terepi_seged/services/stakeout_service.dart';
|
|
|
|
|
|
|
|
|
|
|
|
/// Kitűzési alsó panel — a map_survey nézet overlay-jébe illesztendő
|
|
|
|
|
|
/// (mode == MapSurveyMode.stakeout esetén). Kétfázisú: 5 m felett nagy
|
|
|
|
|
|
/// irány-nyíl + távolság, alatta céltábla cm-es eltérésekkel. A tárolás,
|
|
|
|
|
|
/// kihagyás és transzverzális eltolás gombjai is itt vannak.
|
|
|
|
|
|
class StakeoutPanel extends StatelessWidget {
|
|
|
|
|
|
const StakeoutPanel({super.key});
|
|
|
|
|
|
|
|
|
|
|
|
@override
|
|
|
|
|
|
Widget build(BuildContext context) {
|
|
|
|
|
|
if (!Get.isRegistered<StakeoutService>()) return const SizedBox.shrink();
|
|
|
|
|
|
final svc = StakeoutService.to;
|
|
|
|
|
|
|
|
|
|
|
|
return Obx(() {
|
|
|
|
|
|
// Üres állapot: import-hívás.
|
|
|
|
|
|
if (svc.points.isEmpty) {
|
|
|
|
|
|
return Card(
|
|
|
|
|
|
margin: const EdgeInsets.all(8),
|
|
|
|
|
|
child: Padding(
|
|
|
|
|
|
padding: const EdgeInsets.all(16),
|
|
|
|
|
|
child: Row(
|
|
|
|
|
|
children: [
|
|
|
|
|
|
const Expanded(
|
|
|
|
|
|
child: Text('Nincsenek kitűzési pontok az aktív '
|
|
|
|
|
|
'projektben.'),
|
|
|
|
|
|
),
|
|
|
|
|
|
FilledButton.icon(
|
|
|
|
|
|
onPressed: () => Get.toNamed(Routes.STAKEOUT_IMPORT),
|
|
|
|
|
|
icon: const Icon(Icons.upload_file, size: 18),
|
|
|
|
|
|
label: const Text('Import'),
|
|
|
|
|
|
),
|
|
|
|
|
|
],
|
|
|
|
|
|
),
|
|
|
|
|
|
),
|
|
|
|
|
|
);
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
final t = svc.target.value;
|
|
|
|
|
|
if (t == null) {
|
|
|
|
|
|
return Card(
|
|
|
|
|
|
margin: const EdgeInsets.all(8),
|
|
|
|
|
|
child: Padding(
|
|
|
|
|
|
padding: const EdgeInsets.all(16),
|
|
|
|
|
|
child: Text('Minden pont kitűzve vagy kihagyva. '
|
|
|
|
|
|
'(${svc.points.length} pont)'),
|
|
|
|
|
|
),
|
|
|
|
|
|
);
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
final within = svc.withinTolerance.value;
|
|
|
|
|
|
final progress = svc.lineProgress[t.lineId];
|
|
|
|
|
|
|
|
|
|
|
|
return Card(
|
|
|
|
|
|
margin: const EdgeInsets.all(8),
|
|
|
|
|
|
elevation: 6,
|
|
|
|
|
|
color: within
|
|
|
|
|
|
? Colors.green.shade50
|
|
|
|
|
|
: Theme.of(context).colorScheme.surface,
|
|
|
|
|
|
shape: RoundedRectangleBorder(
|
|
|
|
|
|
borderRadius: BorderRadius.circular(16),
|
|
|
|
|
|
side: within
|
|
|
|
|
|
? const BorderSide(color: Colors.green, width: 2)
|
|
|
|
|
|
: BorderSide.none,
|
|
|
|
|
|
),
|
|
|
|
|
|
child: Padding(
|
|
|
|
|
|
padding: const EdgeInsets.fromLTRB(12, 4, 12, 10),
|
|
|
|
|
|
child: Column(
|
|
|
|
|
|
mainAxisSize: MainAxisSize.min,
|
|
|
|
|
|
children: [
|
|
|
|
|
|
// ── Fejléc ───────────────────────────────────────────
|
|
|
|
|
|
Row(
|
|
|
|
|
|
children: [
|
|
|
|
|
|
IconButton(
|
|
|
|
|
|
icon: const Icon(Icons.skip_previous),
|
|
|
|
|
|
visualDensity: VisualDensity.compact,
|
2026-07-08 14:47:32 +02:00
|
|
|
|
padding: EdgeInsets.zero,
|
|
|
|
|
|
constraints:
|
|
|
|
|
|
const BoxConstraints(minWidth: 36, minHeight: 36),
|
2026-07-06 11:47:41 +02:00
|
|
|
|
onPressed: () => svc.advance(backwards: true),
|
|
|
|
|
|
),
|
2026-07-08 14:47:32 +02:00
|
|
|
|
// A cím rugalmas: szűk helyen ZSUGORODIK (FittedBox),
|
|
|
|
|
|
// sosem tördelődik karakterenként.
|
2026-07-06 11:47:41 +02:00
|
|
|
|
Expanded(
|
|
|
|
|
|
child: Column(
|
|
|
|
|
|
children: [
|
2026-07-08 14:47:32 +02:00
|
|
|
|
FittedBox(
|
|
|
|
|
|
fit: BoxFit.scaleDown,
|
|
|
|
|
|
child: Text(
|
|
|
|
|
|
t.displayId,
|
|
|
|
|
|
maxLines: 1,
|
2026-07-06 11:47:41 +02:00
|
|
|
|
style: const TextStyle(
|
2026-07-08 14:47:32 +02:00
|
|
|
|
fontSize: 17, fontWeight: FontWeight.w700),
|
|
|
|
|
|
),
|
|
|
|
|
|
),
|
2026-07-06 11:47:41 +02:00
|
|
|
|
if (progress != null)
|
|
|
|
|
|
Text(
|
|
|
|
|
|
'${progress.staked}/${progress.total} kitűzve'
|
2026-07-08 14:47:32 +02:00
|
|
|
|
'${t.isOffset ? ' · ELTOLT' : ''}',
|
|
|
|
|
|
maxLines: 1,
|
|
|
|
|
|
overflow: TextOverflow.ellipsis,
|
2026-07-06 11:47:41 +02:00
|
|
|
|
style: TextStyle(
|
|
|
|
|
|
fontSize: 11, color: Colors.grey.shade600),
|
|
|
|
|
|
),
|
|
|
|
|
|
],
|
|
|
|
|
|
),
|
|
|
|
|
|
),
|
|
|
|
|
|
IconButton(
|
|
|
|
|
|
icon: const Icon(Icons.skip_next),
|
|
|
|
|
|
visualDensity: VisualDensity.compact,
|
2026-07-08 14:47:32 +02:00
|
|
|
|
padding: EdgeInsets.zero,
|
|
|
|
|
|
constraints:
|
|
|
|
|
|
const BoxConstraints(minWidth: 36, minHeight: 36),
|
2026-07-06 11:47:41 +02:00
|
|
|
|
onPressed: () => svc.advance(),
|
|
|
|
|
|
),
|
2026-07-08 14:47:32 +02:00
|
|
|
|
const TiltBubble(),
|
2026-07-06 11:47:41 +02:00
|
|
|
|
PopupMenuButton<StakeoutOrder>(
|
|
|
|
|
|
icon: Icon(switch (svc.orderMode.value) {
|
|
|
|
|
|
StakeoutOrder.idAsc => Icons.trending_up,
|
|
|
|
|
|
StakeoutOrder.idDesc => Icons.trending_down,
|
|
|
|
|
|
StakeoutOrder.nearest => Icons.near_me,
|
|
|
|
|
|
}),
|
2026-07-08 14:47:32 +02:00
|
|
|
|
padding: EdgeInsets.zero,
|
2026-07-06 11:47:41 +02:00
|
|
|
|
tooltip: 'Sorrend',
|
|
|
|
|
|
onSelected: (m) => svc.orderMode.value = m,
|
|
|
|
|
|
itemBuilder: (_) => const [
|
|
|
|
|
|
PopupMenuItem(
|
|
|
|
|
|
value: StakeoutOrder.idAsc,
|
|
|
|
|
|
child: Text('Állomás növekvő')),
|
|
|
|
|
|
PopupMenuItem(
|
|
|
|
|
|
value: StakeoutOrder.idDesc,
|
|
|
|
|
|
child: Text('Állomás csökkenő')),
|
|
|
|
|
|
PopupMenuItem(
|
|
|
|
|
|
value: StakeoutOrder.nearest,
|
|
|
|
|
|
child: Text('Legközelebbi')),
|
|
|
|
|
|
],
|
|
|
|
|
|
),
|
2026-07-08 14:47:32 +02:00
|
|
|
|
// A ritkán használt műveletek EGY menüben — a fejléc
|
|
|
|
|
|
// így keskeny kijelzőn / nagy betűméretnél is kifér.
|
|
|
|
|
|
PopupMenuButton<String>(
|
|
|
|
|
|
icon: const Icon(Icons.more_vert),
|
|
|
|
|
|
padding: EdgeInsets.zero,
|
|
|
|
|
|
tooltip: 'További műveletek',
|
|
|
|
|
|
onSelected: (a) {
|
|
|
|
|
|
switch (a) {
|
|
|
|
|
|
case 'list':
|
|
|
|
|
|
StakeoutListSheet.show();
|
|
|
|
|
|
case 'settings':
|
|
|
|
|
|
showStakeoutSettingsDialog();
|
|
|
|
|
|
case 'import':
|
|
|
|
|
|
Get.toNamed(Routes.STAKEOUT_IMPORT);
|
|
|
|
|
|
case 'haptics':
|
|
|
|
|
|
svc.hapticsEnabled.toggle();
|
|
|
|
|
|
}
|
|
|
|
|
|
},
|
|
|
|
|
|
itemBuilder: (_) => [
|
|
|
|
|
|
const PopupMenuItem(
|
|
|
|
|
|
value: 'list', child: Text('Pontlista / export')),
|
|
|
|
|
|
const PopupMenuItem(
|
|
|
|
|
|
value: 'settings',
|
|
|
|
|
|
child: Text('Kitűzési beállítások')),
|
|
|
|
|
|
const PopupMenuItem(
|
|
|
|
|
|
value: 'import', child: Text('Pontok importja')),
|
|
|
|
|
|
CheckedPopupMenuItem(
|
|
|
|
|
|
value: 'haptics',
|
|
|
|
|
|
checked: svc.hapticsEnabled.value,
|
|
|
|
|
|
child: const Text('Haptikus visszajelzés')),
|
|
|
|
|
|
],
|
2026-07-06 11:47:41 +02:00
|
|
|
|
),
|
|
|
|
|
|
],
|
|
|
|
|
|
),
|
|
|
|
|
|
// ── Fázis-tartalom ───────────────────────────────────
|
|
|
|
|
|
if (!svc.hasPosition.value)
|
|
|
|
|
|
const Padding(
|
|
|
|
|
|
padding: EdgeInsets.all(10),
|
|
|
|
|
|
child: Text('Várakozás GNSS pozícióra…',
|
|
|
|
|
|
style: TextStyle(color: Colors.orange)),
|
|
|
|
|
|
)
|
|
|
|
|
|
else if (svc.nearPhase.value)
|
|
|
|
|
|
_NearPhase(svc: svc)
|
|
|
|
|
|
else
|
|
|
|
|
|
_FarPhase(svc: svc),
|
|
|
|
|
|
|
|
|
|
|
|
const SizedBox(height: 4),
|
|
|
|
|
|
|
|
|
|
|
|
// ── Gombsor ──────────────────────────────────────────
|
|
|
|
|
|
Row(
|
|
|
|
|
|
children: [
|
|
|
|
|
|
IconButton(
|
|
|
|
|
|
icon: Icon(svc.nearPhase.value ? Icons.map : Icons.adjust),
|
2026-07-06 14:26:16 +02:00
|
|
|
|
visualDensity: VisualDensity.compact,
|
|
|
|
|
|
padding: EdgeInsets.zero,
|
|
|
|
|
|
constraints:
|
|
|
|
|
|
const BoxConstraints(minWidth: 36, minHeight: 36),
|
2026-07-06 11:47:41 +02:00
|
|
|
|
tooltip:
|
|
|
|
|
|
svc.nearPhase.value ? 'Térkép-nézet' : 'Céltábla-nézet',
|
|
|
|
|
|
onPressed: svc.togglePhase,
|
|
|
|
|
|
),
|
|
|
|
|
|
OutlinedButton(
|
|
|
|
|
|
onPressed: svc.skipCurrent,
|
2026-07-06 14:26:16 +02:00
|
|
|
|
style: OutlinedButton.styleFrom(
|
|
|
|
|
|
visualDensity: VisualDensity.compact,
|
|
|
|
|
|
padding: const EdgeInsets.symmetric(horizontal: 10),
|
|
|
|
|
|
tapTargetSize: MaterialTapTargetSize.shrinkWrap,
|
|
|
|
|
|
),
|
2026-07-06 11:47:41 +02:00
|
|
|
|
child: const Text('Kihagy'),
|
|
|
|
|
|
),
|
2026-07-06 14:26:16 +02:00
|
|
|
|
const SizedBox(width: 6),
|
2026-07-06 11:47:41 +02:00
|
|
|
|
OutlinedButton.icon(
|
|
|
|
|
|
onPressed: () => _offsetDialog(svc),
|
2026-07-06 14:26:16 +02:00
|
|
|
|
style: OutlinedButton.styleFrom(
|
|
|
|
|
|
visualDensity: VisualDensity.compact,
|
|
|
|
|
|
padding: const EdgeInsets.symmetric(horizontal: 10),
|
|
|
|
|
|
tapTargetSize: MaterialTapTargetSize.shrinkWrap,
|
|
|
|
|
|
),
|
2026-07-06 11:47:41 +02:00
|
|
|
|
icon: const Icon(Icons.alt_route, size: 18),
|
|
|
|
|
|
label: const Text('Eltolás'),
|
|
|
|
|
|
),
|
2026-07-06 14:26:16 +02:00
|
|
|
|
const SizedBox(width: 6),
|
|
|
|
|
|
// Expanded: a Tárol tölti ki a maradék helyet —
|
|
|
|
|
|
// fix-szélesség-összeg helyett, így nincs overflow.
|
|
|
|
|
|
Expanded(
|
|
|
|
|
|
child: FilledButton.icon(
|
|
|
|
|
|
onPressed:
|
|
|
|
|
|
svc.isAveraging.value ? null : () => _store(svc),
|
|
|
|
|
|
style: FilledButton.styleFrom(
|
|
|
|
|
|
backgroundColor: within ? Colors.green : null,
|
|
|
|
|
|
visualDensity: VisualDensity.compact,
|
|
|
|
|
|
padding: const EdgeInsets.symmetric(horizontal: 10),
|
|
|
|
|
|
),
|
|
|
|
|
|
icon: svc.isAveraging.value
|
|
|
|
|
|
? const SizedBox(
|
|
|
|
|
|
width: 18,
|
|
|
|
|
|
height: 18,
|
|
|
|
|
|
child: CircularProgressIndicator(strokeWidth: 2))
|
|
|
|
|
|
: const Icon(Icons.push_pin, size: 18),
|
|
|
|
|
|
label: FittedBox(
|
|
|
|
|
|
fit: BoxFit.scaleDown,
|
|
|
|
|
|
child: Text(svc.isAveraging.value
|
|
|
|
|
|
? 'Átlagolás ${svc.averagingElapsed.value}/'
|
|
|
|
|
|
'${svc.averagingSeconds.value} s'
|
|
|
|
|
|
: 'Tárol'),
|
|
|
|
|
|
),
|
|
|
|
|
|
),
|
2026-07-06 11:47:41 +02:00
|
|
|
|
),
|
|
|
|
|
|
],
|
|
|
|
|
|
),
|
|
|
|
|
|
],
|
|
|
|
|
|
),
|
|
|
|
|
|
),
|
|
|
|
|
|
);
|
|
|
|
|
|
});
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
// ═════════════════════════════════════════════════════════════════
|
|
|
|
|
|
// Tárolás — figyelmeztetés, ha nem RTK fixed (de NEM tiltás)
|
|
|
|
|
|
// ═════════════════════════════════════════════════════════════════
|
|
|
|
|
|
|
|
|
|
|
|
Future<void> _store(StakeoutService svc) async {
|
|
|
|
|
|
if (svc.target.value == null || !svc.hasPosition.value) return;
|
|
|
|
|
|
|
|
|
|
|
|
final quality = GnssService.to.gpsQuality.value;
|
|
|
|
|
|
if (quality != 4) {
|
|
|
|
|
|
final proceed = await Get.dialog<bool>(AlertDialog(
|
|
|
|
|
|
icon: const Icon(Icons.warning_amber, color: Colors.orange),
|
|
|
|
|
|
title: const Text('Nincs RTK fixed'),
|
|
|
|
|
|
content: Text(
|
|
|
|
|
|
'A jelenlegi megoldás: ${_fixLabel(quality)}.\n'
|
|
|
|
|
|
'A pont tárolható, de a pontosság csökkent lehet — a '
|
|
|
|
|
|
'fix-minőség bekerül a jegyzőkönyvbe.',
|
|
|
|
|
|
),
|
|
|
|
|
|
actions: [
|
|
|
|
|
|
TextButton(
|
|
|
|
|
|
onPressed: () => Get.back(result: false),
|
|
|
|
|
|
child: const Text('Mégse')),
|
|
|
|
|
|
FilledButton(
|
|
|
|
|
|
onPressed: () => Get.back(result: true),
|
|
|
|
|
|
child: const Text('Tárolás így is')),
|
|
|
|
|
|
],
|
|
|
|
|
|
));
|
|
|
|
|
|
if (proceed != true) return;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
final stored = await svc.storeCurrent();
|
|
|
|
|
|
if (stored == null) return;
|
|
|
|
|
|
|
|
|
|
|
|
Get.snackbar(
|
|
|
|
|
|
'✓ ${stored.displayId} kitűzve',
|
|
|
|
|
|
'inline ${_cm(stored.devInline)} · crossline ${_cm(stored.devCrossline)}'
|
|
|
|
|
|
'${stored.devDz != null ? ' · dZ ${_cm(stored.devDz)}' : ''}'
|
|
|
|
|
|
' · ${_fixLabel(stored.fixQuality ?? 0)}',
|
|
|
|
|
|
snackPosition: SnackPosition.BOTTOM,
|
|
|
|
|
|
backgroundColor: const Color(0xFF2E7D32),
|
|
|
|
|
|
colorText: const Color(0xFFFFFFFF),
|
|
|
|
|
|
duration: const Duration(seconds: 3),
|
|
|
|
|
|
);
|
|
|
|
|
|
|
|
|
|
|
|
svc.advance();
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
Future<void> _offsetDialog(StakeoutService svc) async {
|
|
|
|
|
|
final distCtrl = TextEditingController(text: '2');
|
|
|
|
|
|
final toRight = true.obs;
|
2026-07-06 14:26:16 +02:00
|
|
|
|
final both = false.obs;
|
2026-07-06 11:47:41 +02:00
|
|
|
|
|
|
|
|
|
|
await Get.dialog(AlertDialog(
|
|
|
|
|
|
title: const Text('Eltolt pont (transzverzális)'),
|
|
|
|
|
|
content: Column(
|
|
|
|
|
|
mainAxisSize: MainAxisSize.min,
|
|
|
|
|
|
crossAxisAlignment: CrossAxisAlignment.start,
|
|
|
|
|
|
children: [
|
|
|
|
|
|
const Text(
|
|
|
|
|
|
'A vonalra merőlegesen, a tervponttól mért távolságra. '
|
|
|
|
|
|
'Az eltolás iránya és nagysága bekerül a jegyzőkönyvbe.',
|
|
|
|
|
|
style: TextStyle(fontSize: 12),
|
|
|
|
|
|
),
|
|
|
|
|
|
const SizedBox(height: 12),
|
|
|
|
|
|
Wrap(
|
|
|
|
|
|
spacing: 8,
|
|
|
|
|
|
children: [
|
|
|
|
|
|
for (final d in ['0.5', '1', '2', '5'])
|
|
|
|
|
|
ActionChip(
|
|
|
|
|
|
label: Text('$d m'), onPressed: () => distCtrl.text = d),
|
|
|
|
|
|
],
|
|
|
|
|
|
),
|
|
|
|
|
|
const SizedBox(height: 8),
|
|
|
|
|
|
TextField(
|
|
|
|
|
|
controller: distCtrl,
|
|
|
|
|
|
keyboardType: const TextInputType.numberWithOptions(decimal: true),
|
|
|
|
|
|
decoration:
|
|
|
|
|
|
const InputDecoration(labelText: 'Távolság (m)', isDense: true),
|
|
|
|
|
|
),
|
|
|
|
|
|
const SizedBox(height: 8),
|
|
|
|
|
|
Obx(() => SegmentedButton<bool>(
|
|
|
|
|
|
segments: const [
|
|
|
|
|
|
ButtonSegment(
|
|
|
|
|
|
value: false,
|
|
|
|
|
|
label: Text('Balra'),
|
|
|
|
|
|
icon: Icon(Icons.west)),
|
|
|
|
|
|
ButtonSegment(
|
|
|
|
|
|
value: true,
|
|
|
|
|
|
label: Text('Jobbra'),
|
|
|
|
|
|
icon: Icon(Icons.east)),
|
|
|
|
|
|
],
|
|
|
|
|
|
selected: {toRight.value},
|
|
|
|
|
|
onSelectionChanged: (s) => toRight.value = s.first,
|
|
|
|
|
|
)),
|
2026-07-06 14:26:16 +02:00
|
|
|
|
Obx(() => CheckboxListTile(
|
|
|
|
|
|
contentPadding: EdgeInsets.zero,
|
|
|
|
|
|
dense: true,
|
|
|
|
|
|
title: const Text('Pár létrehozása (bal ÉS jobb)'),
|
|
|
|
|
|
subtitle: const Text('az akadály két oldalára',
|
|
|
|
|
|
style: TextStyle(fontSize: 11)),
|
|
|
|
|
|
value: both.value,
|
|
|
|
|
|
onChanged: (v) => both.value = v ?? false,
|
|
|
|
|
|
))
|
2026-07-06 11:47:41 +02:00
|
|
|
|
],
|
|
|
|
|
|
),
|
|
|
|
|
|
actions: [
|
|
|
|
|
|
TextButton(onPressed: Get.back, child: const Text('Mégse')),
|
|
|
|
|
|
FilledButton(
|
|
|
|
|
|
onPressed: () async {
|
|
|
|
|
|
final d = double.tryParse(distCtrl.text.replaceAll(',', '.'));
|
|
|
|
|
|
if (d == null || d <= 0) return;
|
2026-07-06 14:26:16 +02:00
|
|
|
|
|
|
|
|
|
|
// Megrendelői offset-korlát ellenőrzése (beállításokból).
|
|
|
|
|
|
if (svc.maxOffset.value > 0 && d > svc.maxOffset.value) {
|
|
|
|
|
|
final ok = await Get.dialog<bool>(AlertDialog(
|
|
|
|
|
|
icon: const Icon(Icons.warning_amber, color: Colors.orange),
|
|
|
|
|
|
title: const Text('Offset-korlát túllépése'),
|
|
|
|
|
|
content: Text('A megadott ${d.toStringAsFixed(1)} m nagyobb '
|
|
|
|
|
|
'a beállított max. ${svc.maxOffset.value.toStringAsFixed(1)} m '
|
|
|
|
|
|
'crossline eltolásnál.'),
|
|
|
|
|
|
actions: [
|
|
|
|
|
|
TextButton(
|
|
|
|
|
|
onPressed: () => Get.back(result: false),
|
|
|
|
|
|
child: const Text('Mégse')),
|
|
|
|
|
|
FilledButton(
|
|
|
|
|
|
onPressed: () => Get.back(result: true),
|
|
|
|
|
|
child: const Text('Mégis')),
|
|
|
|
|
|
],
|
|
|
|
|
|
));
|
|
|
|
|
|
if (ok != true) return;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
2026-07-06 11:47:41 +02:00
|
|
|
|
Get.back();
|
2026-07-06 14:26:16 +02:00
|
|
|
|
if (both.value) {
|
|
|
|
|
|
// Előbb az ellenoldali, aztán a választott — így a
|
|
|
|
|
|
// választott oldal lesz az aktív cél.
|
|
|
|
|
|
await svc.createOffset(dist: d, toRight: !toRight.value);
|
|
|
|
|
|
await svc.createOffset(dist: d, toRight: toRight.value);
|
|
|
|
|
|
Get.snackbar('Eltolt pontpár létrehozva',
|
|
|
|
|
|
'±${d.toStringAsFixed(2)} m a vonalra merőlegesen.',
|
2026-07-06 11:47:41 +02:00
|
|
|
|
snackPosition: SnackPosition.BOTTOM);
|
2026-07-06 14:26:16 +02:00
|
|
|
|
} else {
|
|
|
|
|
|
final p = await svc.createOffset(dist: d, toRight: toRight.value);
|
|
|
|
|
|
if (p != null) {
|
|
|
|
|
|
Get.snackbar(
|
|
|
|
|
|
'Eltolt pont létrehozva',
|
|
|
|
|
|
'${p.displayId} · ${d.toStringAsFixed(2)} m '
|
|
|
|
|
|
'${toRight.value ? 'jobbra' : 'balra'} — ez az új cél.',
|
|
|
|
|
|
snackPosition: SnackPosition.BOTTOM);
|
|
|
|
|
|
}
|
2026-07-06 11:47:41 +02:00
|
|
|
|
}
|
|
|
|
|
|
},
|
|
|
|
|
|
child: const Text('Létrehozás'),
|
|
|
|
|
|
),
|
|
|
|
|
|
],
|
|
|
|
|
|
));
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
static String _cm(double? v) => v == null
|
|
|
|
|
|
? '—'
|
|
|
|
|
|
: v.abs() < 1
|
|
|
|
|
|
? '${(v * 100).toStringAsFixed(0)} cm'
|
|
|
|
|
|
: '${v.toStringAsFixed(2)} m';
|
|
|
|
|
|
|
|
|
|
|
|
static String _fixLabel(int q) => switch (q) {
|
|
|
|
|
|
4 => 'RTK FIXED',
|
|
|
|
|
|
5 => 'RTK FLOAT',
|
|
|
|
|
|
2 => 'DGPS',
|
|
|
|
|
|
1 => 'GPS',
|
|
|
|
|
|
0 => 'nincs fix',
|
|
|
|
|
|
_ => 'fix: $q',
|
|
|
|
|
|
};
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
// ═══════════════════════════════════════════════════════════════════
|
|
|
|
|
|
// Távoli fázis: nagy nyíl + távolság
|
|
|
|
|
|
// ═══════════════════════════════════════════════════════════════════
|
|
|
|
|
|
|
|
|
|
|
|
class _FarPhase extends StatelessWidget {
|
|
|
|
|
|
final StakeoutService svc;
|
|
|
|
|
|
const _FarPhase({required this.svc});
|
|
|
|
|
|
|
|
|
|
|
|
static const _compass = ['É', 'ÉK', 'K', 'DK', 'D', 'DNy', 'Ny', 'ÉNy'];
|
|
|
|
|
|
|
|
|
|
|
|
@override
|
|
|
|
|
|
Widget build(BuildContext context) {
|
|
|
|
|
|
return Obx(() {
|
|
|
|
|
|
final heading = svc.travelHeading.value;
|
|
|
|
|
|
final bearing = svc.bearingToTarget.value;
|
|
|
|
|
|
final compass = _compass[((bearing + 22.5) % 360 ~/ 45)];
|
|
|
|
|
|
|
|
|
|
|
|
return Padding(
|
|
|
|
|
|
padding: const EdgeInsets.symmetric(vertical: 2),
|
|
|
|
|
|
child: Row(
|
|
|
|
|
|
mainAxisAlignment: MainAxisAlignment.center,
|
|
|
|
|
|
children: [
|
|
|
|
|
|
// A nyíl a HALADÁSI IRÁNYHOZ képest forog; álló helyzetben
|
|
|
|
|
|
// iránytű-ikon + égtáj-szöveg.
|
|
|
|
|
|
SizedBox(
|
|
|
|
|
|
width: 64,
|
|
|
|
|
|
height: 64,
|
|
|
|
|
|
child: heading != null
|
|
|
|
|
|
? Transform.rotate(
|
|
|
|
|
|
angle: (bearing - heading) * math.pi / 180,
|
|
|
|
|
|
child: const Icon(Icons.navigation,
|
|
|
|
|
|
size: 58, color: Colors.blue),
|
|
|
|
|
|
)
|
|
|
|
|
|
: const Icon(Icons.explore, size: 52, color: Colors.blueGrey),
|
|
|
|
|
|
),
|
|
|
|
|
|
const SizedBox(width: 16),
|
|
|
|
|
|
Column(
|
|
|
|
|
|
crossAxisAlignment: CrossAxisAlignment.start,
|
|
|
|
|
|
children: [
|
|
|
|
|
|
Text(
|
|
|
|
|
|
_fmtDist(svc.distance.value),
|
|
|
|
|
|
style: const TextStyle(
|
|
|
|
|
|
fontSize: 30,
|
|
|
|
|
|
fontWeight: FontWeight.w800,
|
|
|
|
|
|
fontFeatures: [FontFeature.tabularFigures()],
|
|
|
|
|
|
),
|
|
|
|
|
|
),
|
|
|
|
|
|
Text(
|
|
|
|
|
|
heading != null
|
|
|
|
|
|
? '$compass felé (${bearing.toStringAsFixed(0)}°)'
|
|
|
|
|
|
: 'Indulj el — a nyíl követi a haladásod · $compass',
|
|
|
|
|
|
style: TextStyle(fontSize: 12, color: Colors.grey.shade600),
|
|
|
|
|
|
),
|
|
|
|
|
|
if (svc.devDz.value != null)
|
|
|
|
|
|
Text(
|
|
|
|
|
|
'dZ: ${svc.devDz.value! >= 0 ? 'fel' : 'le'} '
|
|
|
|
|
|
'${_fmtDist(svc.devDz.value!.abs())}',
|
|
|
|
|
|
style: TextStyle(fontSize: 12, color: Colors.grey.shade600),
|
|
|
|
|
|
),
|
|
|
|
|
|
],
|
|
|
|
|
|
),
|
|
|
|
|
|
],
|
|
|
|
|
|
),
|
|
|
|
|
|
);
|
|
|
|
|
|
});
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
static String _fmtDist(double d) => d >= 1000
|
|
|
|
|
|
? '${(d / 1000).toStringAsFixed(2)} km'
|
|
|
|
|
|
: d >= 10
|
|
|
|
|
|
? '${d.toStringAsFixed(1)} m'
|
|
|
|
|
|
: '${d.toStringAsFixed(2)} m';
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
// ═══════════════════════════════════════════════════════════════════
|
|
|
|
|
|
// Közeli fázis: céltábla + cm-es eltérések
|
|
|
|
|
|
// ═══════════════════════════════════════════════════════════════════
|
|
|
|
|
|
|
|
|
|
|
|
class _NearPhase extends StatelessWidget {
|
|
|
|
|
|
final StakeoutService svc;
|
|
|
|
|
|
const _NearPhase({required this.svc});
|
|
|
|
|
|
|
|
|
|
|
|
@override
|
|
|
|
|
|
Widget build(BuildContext context) {
|
|
|
|
|
|
return Obx(() {
|
|
|
|
|
|
final fwd = svc.devForward.value;
|
|
|
|
|
|
final right = svc.devRight.value;
|
|
|
|
|
|
final dz = svc.devDz.value;
|
|
|
|
|
|
final within = svc.withinTolerance.value;
|
|
|
|
|
|
final ne = svc.deviationMode.value == DeviationMode.northEast;
|
|
|
|
|
|
|
|
|
|
|
|
final (fwdLabel, rightLabel) = switch (svc.deviationMode.value) {
|
|
|
|
|
|
DeviationMode.line => ('Inline', 'Crossline'),
|
|
|
|
|
|
DeviationMode.travel => ('Előre/hátra', 'Jobbra/balra'),
|
|
|
|
|
|
DeviationMode.northEast => ('Észak', 'Kelet'),
|
|
|
|
|
|
};
|
|
|
|
|
|
|
|
|
|
|
|
return Column(
|
|
|
|
|
|
children: [
|
|
|
|
|
|
SegmentedButton<DeviationMode>(
|
|
|
|
|
|
segments: [
|
|
|
|
|
|
for (final m in DeviationMode.values)
|
|
|
|
|
|
ButtonSegment(
|
|
|
|
|
|
value: m,
|
|
|
|
|
|
label: Text(m.label, style: const TextStyle(fontSize: 10))),
|
|
|
|
|
|
],
|
|
|
|
|
|
selected: {svc.deviationMode.value},
|
|
|
|
|
|
onSelectionChanged: (s) => svc.deviationMode.value = s.first,
|
|
|
|
|
|
showSelectedIcon: false,
|
|
|
|
|
|
style: const ButtonStyle(
|
|
|
|
|
|
visualDensity: VisualDensity.compact,
|
|
|
|
|
|
tapTargetSize: MaterialTapTargetSize.shrinkWrap),
|
|
|
|
|
|
),
|
|
|
|
|
|
const SizedBox(height: 6),
|
|
|
|
|
|
Row(
|
|
|
|
|
|
children: [
|
|
|
|
|
|
SizedBox(
|
|
|
|
|
|
width: 140,
|
|
|
|
|
|
height: 140,
|
|
|
|
|
|
child: CustomPaint(
|
|
|
|
|
|
painter: _BullseyePainter(
|
|
|
|
|
|
devForward: fwd,
|
|
|
|
|
|
devRight: right,
|
|
|
|
|
|
tolerance: svc.toleranceXY.value,
|
|
|
|
|
|
within: within,
|
|
|
|
|
|
),
|
|
|
|
|
|
),
|
|
|
|
|
|
),
|
|
|
|
|
|
const SizedBox(width: 10),
|
|
|
|
|
|
Expanded(
|
|
|
|
|
|
child: Column(
|
|
|
|
|
|
crossAxisAlignment: CrossAxisAlignment.start,
|
|
|
|
|
|
children: [
|
|
|
|
|
|
_DevRow(
|
|
|
|
|
|
label: fwdLabel,
|
|
|
|
|
|
value: fwd,
|
|
|
|
|
|
posText: ne ? 'É' : 'előre',
|
|
|
|
|
|
negText: ne ? 'D' : 'hátra'),
|
|
|
|
|
|
_DevRow(
|
|
|
|
|
|
label: rightLabel,
|
|
|
|
|
|
value: right,
|
|
|
|
|
|
posText: ne ? 'K' : 'jobbra',
|
|
|
|
|
|
negText: ne ? 'Ny' : 'balra'),
|
|
|
|
|
|
if (dz != null)
|
|
|
|
|
|
_DevRow(
|
|
|
|
|
|
label: 'Magasság',
|
|
|
|
|
|
value: dz,
|
|
|
|
|
|
posText: 'fel',
|
|
|
|
|
|
negText: 'le'),
|
|
|
|
|
|
const SizedBox(height: 4),
|
|
|
|
|
|
Text(
|
|
|
|
|
|
within
|
|
|
|
|
|
? '✓ Tűrésen belül '
|
|
|
|
|
|
'(${(svc.toleranceXY.value * 100).toStringAsFixed(0)} cm)'
|
|
|
|
|
|
: 'Távolság: '
|
|
|
|
|
|
'${(svc.distance.value * 100).toStringAsFixed(0)} cm',
|
|
|
|
|
|
style: TextStyle(
|
|
|
|
|
|
fontSize: 13,
|
|
|
|
|
|
fontWeight: FontWeight.w700,
|
|
|
|
|
|
color: within ? Colors.green : null,
|
|
|
|
|
|
),
|
|
|
|
|
|
),
|
|
|
|
|
|
],
|
|
|
|
|
|
),
|
|
|
|
|
|
),
|
|
|
|
|
|
],
|
|
|
|
|
|
),
|
|
|
|
|
|
],
|
|
|
|
|
|
);
|
|
|
|
|
|
});
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
class _DevRow extends StatelessWidget {
|
|
|
|
|
|
final String label;
|
|
|
|
|
|
final double value;
|
|
|
|
|
|
final String posText;
|
|
|
|
|
|
final String negText;
|
|
|
|
|
|
const _DevRow(
|
|
|
|
|
|
{required this.label,
|
|
|
|
|
|
required this.value,
|
|
|
|
|
|
required this.posText,
|
|
|
|
|
|
required this.negText});
|
|
|
|
|
|
|
2026-07-08 22:13:30 +02:00
|
|
|
|
//@Preview(name: 'Example')
|
2026-07-06 11:47:41 +02:00
|
|
|
|
@override
|
|
|
|
|
|
Widget build(BuildContext context) {
|
|
|
|
|
|
final cm = value.abs() * 100;
|
|
|
|
|
|
final txt = cm < 100
|
|
|
|
|
|
? '${cm.toStringAsFixed(0)} cm'
|
|
|
|
|
|
: '${value.abs().toStringAsFixed(2)} m';
|
|
|
|
|
|
return Padding(
|
|
|
|
|
|
padding: const EdgeInsets.symmetric(vertical: 2),
|
|
|
|
|
|
child: Row(
|
|
|
|
|
|
children: [
|
|
|
|
|
|
SizedBox(
|
|
|
|
|
|
width: 78,
|
|
|
|
|
|
child: Text(label,
|
|
|
|
|
|
style: TextStyle(fontSize: 11, color: Colors.grey.shade600)),
|
|
|
|
|
|
),
|
|
|
|
|
|
Text(
|
|
|
|
|
|
'${value >= 0 ? posText : negText} $txt',
|
|
|
|
|
|
style: const TextStyle(
|
|
|
|
|
|
fontSize: 16,
|
|
|
|
|
|
fontWeight: FontWeight.w700,
|
|
|
|
|
|
fontFeatures: [FontFeature.tabularFigures()],
|
|
|
|
|
|
),
|
|
|
|
|
|
),
|
|
|
|
|
|
],
|
|
|
|
|
|
),
|
|
|
|
|
|
);
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
/// Céltábla: középen a CÉL, a kék pötty a MI pozíciónk a célhoz képest.
|
|
|
|
|
|
/// A "felfelé" tengely az aktuális eltérés-mód "előre" iránya.
|
|
|
|
|
|
class _BullseyePainter extends CustomPainter {
|
|
|
|
|
|
final double devForward;
|
|
|
|
|
|
final double devRight;
|
|
|
|
|
|
final double tolerance;
|
|
|
|
|
|
final bool within;
|
|
|
|
|
|
|
|
|
|
|
|
_BullseyePainter({
|
|
|
|
|
|
required this.devForward,
|
|
|
|
|
|
required this.devRight,
|
|
|
|
|
|
required this.tolerance,
|
|
|
|
|
|
required this.within,
|
|
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
|
|
@override
|
|
|
|
|
|
void paint(Canvas canvas, Size size) {
|
|
|
|
|
|
final c = Offset(size.width / 2, size.height / 2);
|
|
|
|
|
|
final maxR = size.width / 2 - 4;
|
|
|
|
|
|
const viewRadiusM = 1.2;
|
|
|
|
|
|
final scale = maxR / viewRadiusM;
|
|
|
|
|
|
|
|
|
|
|
|
final ring = Paint()
|
|
|
|
|
|
..style = PaintingStyle.stroke
|
|
|
|
|
|
..strokeWidth = 1
|
|
|
|
|
|
..color = Colors.grey.shade400;
|
|
|
|
|
|
|
|
|
|
|
|
for (final r in [1.0, 0.5]) {
|
|
|
|
|
|
canvas.drawCircle(c, r * scale, ring);
|
|
|
|
|
|
}
|
|
|
|
|
|
canvas.drawCircle(
|
|
|
|
|
|
c,
|
|
|
|
|
|
tolerance * scale,
|
|
|
|
|
|
Paint()
|
|
|
|
|
|
..style = PaintingStyle.stroke
|
|
|
|
|
|
..strokeWidth = 2
|
|
|
|
|
|
..color = within ? Colors.green : Colors.grey.shade600);
|
|
|
|
|
|
|
|
|
|
|
|
canvas.drawLine(Offset(c.dx - maxR, c.dy), Offset(c.dx + maxR, c.dy), ring);
|
|
|
|
|
|
canvas.drawLine(Offset(c.dx, c.dy - maxR), Offset(c.dx, c.dy + maxR), ring);
|
|
|
|
|
|
|
|
|
|
|
|
canvas.drawCircle(
|
|
|
|
|
|
c, 4, Paint()..color = within ? Colors.green : Colors.red);
|
|
|
|
|
|
|
|
|
|
|
|
// A MI pozíciónk a célhoz képest: offset = −(eltérés) az
|
|
|
|
|
|
// (előre, jobbra) bázisban; képernyő-y felfelé = előre.
|
|
|
|
|
|
var px = -devRight * scale;
|
|
|
|
|
|
var py = devForward * scale;
|
|
|
|
|
|
final d = math.sqrt(px * px + py * py);
|
|
|
|
|
|
if (d > maxR) {
|
|
|
|
|
|
px = px / d * maxR;
|
|
|
|
|
|
py = py / d * maxR;
|
|
|
|
|
|
}
|
|
|
|
|
|
final me = c + Offset(px, py);
|
|
|
|
|
|
canvas.drawCircle(me, 7, Paint()..color = Colors.blue);
|
|
|
|
|
|
canvas.drawCircle(
|
|
|
|
|
|
me,
|
|
|
|
|
|
7,
|
|
|
|
|
|
Paint()
|
|
|
|
|
|
..style = PaintingStyle.stroke
|
|
|
|
|
|
..strokeWidth = 2
|
|
|
|
|
|
..color = Colors.white);
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
@override
|
|
|
|
|
|
bool shouldRepaint(_BullseyePainter old) =>
|
|
|
|
|
|
old.devForward != devForward ||
|
|
|
|
|
|
old.devRight != devRight ||
|
|
|
|
|
|
old.within != within ||
|
|
|
|
|
|
old.tolerance != tolerance;
|
|
|
|
|
|
}
|