Files
MobilApp/lib/pages/map_survey/presentations/widgets/tilt_bubble.dart
T

336 lines
11 KiB
Dart

import 'dart:math' as math;
import 'package:flutter/material.dart';
import 'package:flutter/services.dart';
import 'package:get/get.dart';
import '../../../../services/tilt_service.dart';
/// Rúd-libella buborék — kis kör alakú widget a kitűzőpanel fejlécébe.
///
/// Színküszöbök a DŐLÉSBŐL SZÁMOLT VÍZSZINTES HIBA szerint (a rúdmagasság
/// figyelembevételével), mert terepen ez a beszédes érték:
/// zöld < 1,5 cm · sárga < 3,5 cm · piros felette.
/// Mozgás közben és kalibrálatlanul szürke/kérdőjeles.
/// Koppintásra részletes nézet: nagy buborék, számok, rúdmagasság,
/// vezetett (1 vagy 2 lépéses) kalibráció.
class TiltBubble extends StatelessWidget {
const TiltBubble({super.key, this.size = 34});
final double size;
@override
Widget build(BuildContext context) {
if (!Get.isRegistered<TiltService>()) return const SizedBox.shrink();
return Obx(() {
final t = TiltService.to;
final inactive = !t.isCalibrated.value || t.isMoving.value;
final errCm = t.horizontalErrorCm;
final color = inactive
? Colors.grey
: errCm < 1.5
? Colors.green
: errCm < 3.5
? Colors.orange
: Colors.red;
return Tooltip(
message: !t.isCalibrated.value
? 'Libella: nincs kalibrálva — koppints!'
: t.isMoving.value
? 'Libella: mozgásban'
: 'Dőlés ${t.tiltDeg.value.toStringAsFixed(1)}° '
'≈ ${errCm.toStringAsFixed(1)} cm',
child: InkWell(
onTap: () => _showDetail(context),
customBorder: const CircleBorder(),
child: SizedBox(
width: size,
height: size,
child: _BubbleFace(
size: size,
color: color,
inactive: inactive,
uncalibrated: !t.isCalibrated.value,
// A buborék a "lejtés" irányába csúszik — skála: 1 fok
// dőlés ≈ a sugár ~25%-a, telítésig.
dx: t.bubbleX.value,
dy: t.bubbleY.value,
),
),
),
);
});
}
// ═════════════════════════════════════════════════════════════════
// Részletes nézet + kalibráció
// ═════════════════════════════════════════════════════════════════
Future<void> _showDetail(BuildContext context) async {
final t = TiltService.to;
final poleCtrl =
TextEditingController(text: t.poleHeight.value.toStringAsFixed(2));
await Get.dialog(AlertDialog(
title: const Text('Rúd-libella'),
content: SingleChildScrollView(
child: Obx(() {
final errCm = t.horizontalErrorCm;
final inactive = !t.isCalibrated.value || t.isMoving.value;
final color = inactive
? Colors.grey
: errCm < 1.5
? Colors.green
: errCm < 3.5
? Colors.orange
: Colors.red;
return Column(
mainAxisSize: MainAxisSize.min,
children: [
SizedBox(
width: 140,
height: 140,
child: _BubbleFace(
size: 140,
color: color,
inactive: inactive,
uncalibrated: !t.isCalibrated.value,
dx: t.bubbleX.value,
dy: t.bubbleY.value,
),
),
const SizedBox(height: 10),
Text(
!t.isCalibrated.value
? 'Nincs kalibrálva'
: t.isMoving.value
? 'Mozgásban — állj meg a leolvasáshoz'
: '${t.tiltDeg.value.toStringAsFixed(2)}° ≈ '
'${errCm.toStringAsFixed(1)} cm vízszintes hiba',
style: TextStyle(
fontWeight: FontWeight.w700,
color: inactive ? Colors.grey : color),
),
Text(
'@ ${t.poleHeight.value.toStringAsFixed(2)} m rúdmagasság',
style: TextStyle(fontSize: 11, color: Colors.grey.shade600),
),
const SizedBox(height: 12),
TextField(
controller: poleCtrl,
keyboardType:
const TextInputType.numberWithOptions(decimal: true),
decoration: const InputDecoration(
labelText: 'Rúdmagasság (m)',
isDense: true,
),
onSubmitted: (v) {
final m = double.tryParse(v.replaceAll(',', '.'));
if (m != null) t.setPoleHeight(m);
},
),
const SizedBox(height: 8),
Text(
'A jelzés csak akkor érvényes, ha a telefon MEREVEN, '
'tartóban van a rúdon. Tartó-áthelyezés után kalibrálj újra.',
style: TextStyle(fontSize: 11, color: Colors.grey.shade600),
),
],
);
}),
),
actions: [
if (TiltService.to.isCalibrated.value)
TextButton(
onPressed: () => TiltService.to.clearCalibration(),
child: const Text('Kalibráció törlése'),
),
TextButton(onPressed: Get.back, child: const Text('Bezár')),
FilledButton.icon(
icon: const Icon(Icons.architecture, size: 18),
label: const Text('Kalibrálás'),
onPressed: () async {
final m = double.tryParse(poleCtrl.text.replaceAll(',', '.'));
if (m != null) await TiltService.to.setPoleHeight(m);
Get.back();
await _calibrationFlow();
},
),
],
));
}
/// Vezetett kalibráció: 1. lépés kötelező, 2. (180°-os átforgatásos)
/// opcionális — utóbbi a tartó ferdeségét is kiejti.
Future<void> _calibrationFlow() async {
final t = TiltService.to;
final step1Ok = await _captureStep(
title: 'Kalibráció — 1/2',
text: 'Állítsd a rudat FÜGGŐLEGESRE a szelencés libella szerint, '
'és tartsd mozdulatlanul. A mérés 2 másodpercig tart.',
);
if (step1Ok != true) return;
final v1 = await t.captureVector();
HapticFeedback.mediumImpact();
final doStep2 = await Get.dialog<bool>(AlertDialog(
title: const Text('Kalibráció — pontosítás?'),
content: const Text(
'A pontosabb (tartó-ferdeséget is kiejtő) kalibrációhoz forgasd '
'át a telefont a RÚD TENGELYE körül 180°-kal (a rúd maradjon '
'függőleges), majd folytasd. Ki is hagyhatod — akkor az 1. '
'lépés lesz a referencia.'),
actions: [
TextButton(
onPressed: () => Get.back(result: false),
child: const Text('Kihagyom')),
FilledButton(
onPressed: () => Get.back(result: true),
child: const Text('Átforgattam — folytatás')),
],
));
if (doStep2 == true) {
final v2 = await t.captureVector();
HapticFeedback.mediumImpact();
await t.saveReference(v1, v2);
} else {
await t.saveReference(v1);
}
Get.snackbar('Libella', 'Kalibráció kész — a buborék él.',
snackPosition: SnackPosition.BOTTOM,
backgroundColor: const Color(0xFF2E7D32),
colorText: const Color(0xFFFFFFFF));
}
Future<bool?> _captureStep({required String title, required String text}) {
return Get.dialog<bool>(AlertDialog(
title: Text(title),
content: Text(text),
actions: [
TextButton(
onPressed: () => Get.back(result: false),
child: const Text('Mégse')),
FilledButton(
onPressed: () => Get.back(result: true),
child: const Text('Indítás')),
],
));
}
}
// ═══════════════════════════════════════════════════════════════════
class _BubbleFace extends StatelessWidget {
final double size;
final Color color;
final bool inactive;
final bool uncalibrated;
final double dx;
final double dy;
const _BubbleFace({
required this.size,
required this.color,
required this.inactive,
required this.uncalibrated,
required this.dx,
required this.dy,
});
@override
Widget build(BuildContext context) {
// Skála: sin(4°) dőlésnél ér a széléhez.
final r = size / 2;
final scale = (r - size * 0.14) / 0.07;
var ox = dx * scale;
var oy = -dy * scale; // képernyő-y lefelé nő
final d = (ox * ox + oy * oy);
final maxR = r - size * 0.14;
if (d > maxR * maxR) {
final f = maxR / math.sqrt(d);
ox *= f;
oy *= f;
}
return CustomPaint(
painter: _BubblePainter(
color: color,
inactive: inactive,
ox: ox,
oy: oy,
dotRadius: size * 0.13,
),
child: uncalibrated
? Center(
child: Text('?',
style: TextStyle(
fontSize: size * 0.45,
fontWeight: FontWeight.w700,
color: Colors.grey)))
: null,
);
}
}
class _BubblePainter extends CustomPainter {
final Color color;
final bool inactive;
final double ox;
final double oy;
final double dotRadius;
_BubblePainter({
required this.color,
required this.inactive,
required this.ox,
required this.oy,
required this.dotRadius,
});
@override
void paint(Canvas canvas, Size size) {
final c = Offset(size.width / 2, size.height / 2);
final r = size.width / 2;
canvas.drawCircle(
c,
r - 1,
Paint()
..color = color.withOpacity(inactive ? 0.06 : 0.12)
..style = PaintingStyle.fill);
canvas.drawCircle(
c,
r - 1,
Paint()
..color = color.withOpacity(inactive ? 0.5 : 0.9)
..strokeWidth = 1.6
..style = PaintingStyle.stroke);
// Belső célkör
canvas.drawCircle(
c,
r * 0.35,
Paint()
..color = color.withOpacity(0.5)
..strokeWidth = 1
..style = PaintingStyle.stroke);
if (!inactive) {
canvas.drawCircle(c + Offset(ox, oy), dotRadius, Paint()..color = color);
}
}
@override
bool shouldRepaint(_BubblePainter old) =>
old.ox != ox ||
old.oy != oy ||
old.color != color ||
old.inactive != inactive;
}