Navigáció javítása, fejlesztése

This commit is contained in:
2026-07-15 03:13:52 +02:00
parent 0810c528ec
commit 204d848ba4
4 changed files with 143 additions and 62 deletions
@@ -1,7 +1,9 @@
import 'dart:async'; import 'dart:async';
import 'dart:math' as math; import 'dart:math' as math;
import 'package:flutter_map/flutter_map.dart';
import 'package:get/get.dart'; import 'package:get/get.dart';
import 'package:latlong2/latlong.dart';
import 'package:terepi_seged/models/source_point.dart'; import 'package:terepi_seged/models/source_point.dart';
import 'package:terepi_seged/models/vechicle_position_log.dart'; import 'package:terepi_seged/models/vechicle_position_log.dart';
import 'package:terepi_seged/services/app_database.dart'; import 'package:terepi_seged/services/app_database.dart';
@@ -50,12 +52,17 @@ class VibroNavController extends GetxController {
// ── Periodikus napló ─────────────────────────────────────────────── // ── Periodikus napló ───────────────────────────────────────────────
final isLogging = false.obs; final isLogging = false.obs;
final logIntervalSec = 60.obs; // felhasználó által állítható final logIntervalSec = 60.obs; // felhasználó által állítható
final logs = <VehiclePositionLog>[].obs;
Timer? _logTimer; Timer? _logTimer;
// ── Térkép — a MapController itt él, nem a View State-jében ────────
final mapController = MapController();
@override @override
void onInit() { void onInit() {
super.onInit(); super.onInit();
load(); load();
refreshLogs();
if (Get.isRegistered<GnssService>()) { if (Get.isRegistered<GnssService>()) {
ever(GnssService.to.lastGgaLine, (_) => _onPosition()); ever(GnssService.to.lastGgaLine, (_) => _onPosition());
} }
@@ -67,6 +74,10 @@ class VibroNavController extends GetxController {
super.onClose(); super.onClose();
} }
Future<void> refreshLogs() async {
logs.value = await loadLogs();
}
Future<void> load() async { Future<void> load() async {
final projectId = ProjectService.to.activeProjectId; final projectId = ProjectService.to.activeProjectId;
if (projectId == null) { if (projectId == null) {
@@ -212,5 +223,6 @@ class VibroNavController extends GetxController {
: null, : null,
); );
await _db.insertVehiclePositionLog(entry); await _db.insertVehiclePositionLog(entry);
logs.add(entry);
} }
} }
@@ -13,6 +13,7 @@ import 'package:terepi_seged/services/gnss/gnss_service.dart';
import 'package:terepi_seged/services/project_service.dart'; import 'package:terepi_seged/services/project_service.dart';
import 'package:terepi_seged/services/sps_import_service.dart'; import 'package:terepi_seged/services/sps_import_service.dart';
import 'package:terepi_seged/services/vechicle_identity_service.dart'; import 'package:terepi_seged/services/vechicle_identity_service.dart';
import 'package:terepi_seged/widgets/map/animated_map_follow.dart';
import 'package:terepi_seged/widgets/map/imported_layer_overlay.dart'; import 'package:terepi_seged/widgets/map/imported_layer_overlay.dart';
import '../controllers/vibro_nav_controller.dart'; import '../controllers/vibro_nav_controller.dart';
@@ -20,59 +21,9 @@ import '../controllers/vibro_nav_controller.dart';
/// Egyszerű navigációs oldal a vibrátor-járműhöz. ÖNÁLLÓ oldal, nem /// Egyszerű navigációs oldal a vibrátor-járműhöz. ÖNÁLLÓ oldal, nem
/// MapSurveyMode — saját MapController kell a forgó (track-up) /// MapSurveyMode — saját MapController kell a forgó (track-up)
/// térképhez, hogy ez semmilyen más módot ne érintsen. /// térképhez, hogy ez semmilyen más módot ne érintsen.
class VibroNavView extends StatefulWidget { class VibroNavView extends GetView<VibroNavController> {
const VibroNavView({super.key}); const VibroNavView({super.key});
@override
State<VibroNavView> createState() => _VibroNavViewState();
}
class _VibroNavViewState extends State<VibroNavView> {
late final VibroNavController controller;
final _mapController = MapController();
List<VehiclePositionLog> _logs = [];
@override
void initState() {
super.initState();
controller = Get.put(VibroNavController());
_refreshLogs();
// Forgatás + a naplók frissítése a pozíció-változásokra.
ever(controller.travelHeading, (_) => _applyRotation());
ever(controller.hasPosition, (_) => _followPosition());
}
@override
void dispose() {
Get.delete<VibroNavController>();
super.dispose();
}
Future<void> _refreshLogs() async {
_logs = await controller.loadLogs();
if (mounted) setState(() {});
}
void _applyRotation() {
final h = controller.travelHeading.value;
if (h == null) return;
try {
_mapController.rotate(-h);
} catch (_) {}
}
void _followPosition() {
if (!controller.hasPosition.value ||
!Get.isRegistered<CoordConverterService>()) {
return;
}
final gnss = GnssService.to;
try {
_mapController.move(LatLng(gnss.latitude.value, gnss.longitude.value),
_mapController.camera.zoom);
} catch (_) {}
}
@override @override
Widget build(BuildContext context) { Widget build(BuildContext context) {
return Scaffold( return Scaffold(
@@ -94,6 +45,17 @@ class _VibroNavViewState extends State<VibroNavView> {
body: Stack( body: Stack(
children: [ children: [
_buildMap(), _buildMap(),
Obx(() {
final target =
controller.hasPosition.value && Get.isRegistered<GnssService>()
? LatLng(GnssService.to.latitude.value,
GnssService.to.longitude.value)
: null;
return AnimatedMapFollow(
mapController: controller.mapController,
target: target,
heading: controller.travelHeading.value);
}),
Positioned( Positioned(
left: 8, left: 8,
right: 8, right: 8,
@@ -104,8 +66,7 @@ class _VibroNavViewState extends State<VibroNavView> {
left: 8, left: 8,
right: 8, right: 8,
bottom: 8, bottom: 8,
child: _NavPanel( child: _NavPanel(controller: controller),
controller: controller, onLoggingToggled: _refreshLogs),
), ),
], ],
), ),
@@ -118,16 +79,22 @@ class _VibroNavViewState extends State<VibroNavView> {
final nearest = controller.nearestPoint.value; final nearest = controller.nearestPoint.value;
return FlutterMap( return FlutterMap(
mapController: _mapController, mapController: controller.mapController,
options: const MapOptions( options: const MapOptions(
initialCenter: LatLng(47.5, 19.05), initialCenter: LatLng(47.5, 19.05),
initialZoom: 15, initialZoom: 15,
), ),
children: [ children: [
// TileLayer(
// urlTemplate: 'https://tile.openstreetmap.org/{z}/{x}/{y}.png',
// userAgentPackageName: 'hu.app_dev.terepi_seged',
// ),
TileLayer( TileLayer(
urlTemplate: 'https://tile.openstreetmap.org/{z}/{x}/{y}.png', urlTemplate: 'http://{s}.google.com/vt/lyrs=s,h&x={x}&y={y}&z={z}',
userAgentPackageName: 'hu.app_dev.terepi_seged', subdomains: const ['mt0', 'mt1', 'mt2', 'mt3'],
maxNativeZoom: 18,
), ),
// Tervezett útvonal + minden más importált háttérréteg — a // Tervezett útvonal + minden más importált háttérréteg — a
// MEGLÉVŐ rétegimport-mechanizmus, nincs hozzá új kód. // MEGLÉVŐ rétegimport-mechanizmus, nincs hozzá új kód.
const ImportedLayerOverlay(), const ImportedLayerOverlay(),
@@ -140,7 +107,7 @@ class _VibroNavViewState extends State<VibroNavView> {
child: _SourcePointMarker( child: _SourcePointMarker(
point: sp, point: sp,
isNearest: sp.uuid == nearest?.uuid, isNearest: sp.uuid == nearest?.uuid,
isVerified: controller.isVerified(sp, _logs), isVerified: controller.isVerified(sp, controller.logs),
), ),
), ),
if (controller.hasPosition.value) if (controller.hasPosition.value)
@@ -237,6 +204,7 @@ class _VibroNavViewState extends State<VibroNavView> {
} }
final inserted = await AppDatabase.instance.insertSourcePoints(saved); final inserted = await AppDatabase.instance.insertSourcePoints(saved);
await controller.load(); await controller.load();
await controller.refreshLogs();
Get.snackbar('Import kész', '$inserted forráspont importálva', Get.snackbar('Import kész', '$inserted forráspont importálva',
snackPosition: SnackPosition.BOTTOM); snackPosition: SnackPosition.BOTTOM);
} catch (e) { } catch (e) {
@@ -252,8 +220,7 @@ class _VibroNavViewState extends State<VibroNavView> {
class _NavPanel extends StatelessWidget { class _NavPanel extends StatelessWidget {
final VibroNavController controller; final VibroNavController controller;
final VoidCallback onLoggingToggled; const _NavPanel({required this.controller});
const _NavPanel({required this.controller, required this.onLoggingToggled});
@override @override
Widget build(BuildContext context) { Widget build(BuildContext context) {
@@ -298,7 +265,6 @@ class _NavPanel extends StatelessWidget {
'(${controller.logIntervalSec.value} mp)'), '(${controller.logIntervalSec.value} mp)'),
onPressed: () { onPressed: () {
controller.stopLogging(); controller.stopLogging();
onLoggingToggled();
}, },
) )
: FilledButton.icon( : FilledButton.icon(
@@ -308,7 +274,6 @@ class _NavPanel extends StatelessWidget {
'(${controller.logIntervalSec.value} mp-enként)'), '(${controller.logIntervalSec.value} mp-enként)'),
onPressed: () { onPressed: () {
controller.startLogging(); controller.startLogging();
onLoggingToggled();
}, },
), ),
), ),
+7 -1
View File
@@ -27,6 +27,7 @@ import 'package:terepi_seged/pages/start/bindings/start_page_bindings.dart';
import 'package:terepi_seged/pages/start/presentation/views/start_page.dart'; import 'package:terepi_seged/pages/start/presentation/views/start_page.dart';
import 'package:terepi_seged/pages/tracking/bindings/tracking_bindings.dart'; import 'package:terepi_seged/pages/tracking/bindings/tracking_bindings.dart';
import 'package:terepi_seged/pages/tracking/presentation/views/tracking_view.dart'; import 'package:terepi_seged/pages/tracking/presentation/views/tracking_view.dart';
import 'package:terepi_seged/pages/vibro_nav/presentation/controllers/vibro_nav_controller.dart';
import 'package:terepi_seged/pages/vibro_nav/presentation/views/vibro_nav_view.dart'; import 'package:terepi_seged/pages/vibro_nav/presentation/views/vibro_nav_view.dart';
import '../pages/map_test/bindings/map_test_bindings.dart'; import '../pages/map_test/bindings/map_test_bindings.dart';
@@ -108,6 +109,11 @@ class AppPages {
GetPage( GetPage(
name: Routes.STAKEOUT_IMPORT, page: () => const StakeoutImportView()), name: Routes.STAKEOUT_IMPORT, page: () => const StakeoutImportView()),
GetPage(name: Routes.CONTACTS, page: () => const ContactsView()), GetPage(name: Routes.CONTACTS, page: () => const ContactsView()),
GetPage(name: Routes.VIBRONAV, page: () => const VibroNavView()) GetPage(
name: Routes.VIBRONAV,
page: () => const VibroNavView(),
binding: BindingsBuilder(() {
Get.put(VibroNavController());
}))
]; ];
} }
+98
View File
@@ -0,0 +1,98 @@
import 'package:flutter/material.dart';
import 'package:flutter_map/flutter_map.dart';
import 'package:latlong2/latlong.dart';
/// Egyenletes, ANIMÁLT térkép-követés — a `mapController.move()`/
/// `.rotate()` közvetlen hívása helyett, amik AZONNAL ugranak az új
/// pozícióra/irányba.
///
/// Ez a widget semmit nem jelenít meg (0 méretű) — kizárólag arra való,
/// hogy a [target] (és opcionálisan [heading]) VÁLTOZÁSAKOR sima
/// animációval, ne ugrással vigye át a térképet a régi pontból az újba.
///
/// Belül a Flutter beépített `TweenAnimationBuilder`-ét használja — ez
/// IMPLICIT módon animál (a cél-érték változásakor magától, a jelenlegi
/// állásból indulva animál az újig). A ticker a Flutter-widget
/// belsejében él, nekünk nem kell saját State/AnimationController-t
/// írnunk hozzá — ez az EGYETLEN pont, ahol animáció miatt egyáltalán
/// szóba kerül egy ticker, de azt is a keretrendszer adja.
class AnimatedMapFollow extends StatelessWidget {
final MapController mapController;
final LatLng? target;
final double? heading; // fok — ha null, nincs forgatás
final Duration duration;
const AnimatedMapFollow({
super.key,
required this.mapController,
required this.target,
this.heading,
this.duration = const Duration(milliseconds: 400),
});
@override
Widget build(BuildContext context) {
final t = target;
if (t == null) return const SizedBox.shrink();
return Stack(
children: [
TweenAnimationBuilder<LatLng>(
tween: _LatLngTween(begin: t, end: t),
duration: duration,
curve: Curves.easeOut,
builder: (context, animatedPos, _) {
try {
mapController.move(animatedPos, mapController.camera.zoom);
} catch (_) {}
return const SizedBox.shrink();
},
),
if (heading != null)
TweenAnimationBuilder<double>(
// A térkép a haladási irány ELLENTETTJÉVEL forog (track-up).
tween: _DegreesTween(begin: -heading!, end: -heading!),
duration: duration,
curve: Curves.easeOut,
builder: (context, animatedRotation, _) {
try {
mapController.rotate(animatedRotation);
} catch (_) {}
return const SizedBox.shrink();
},
),
],
);
}
}
class _LatLngTween extends Tween<LatLng> {
_LatLngTween({required LatLng begin, required LatLng end})
: super(begin: begin, end: end);
@override
LatLng lerp(double t) {
final b = begin!, e = end!;
return LatLng(
b.latitude + (e.latitude - b.latitude) * t,
b.longitude + (e.longitude - b.longitude) * t,
);
}
}
/// Fok-interpoláció a "legrövidebb úton" — enélkül egy 350°→10° váltás
/// hosszan, "visszafelé" animálna 340°-ot ahelyett hogy egyenesen 20°-ot
/// lépne előre.
class _DegreesTween extends Tween<double> {
_DegreesTween({required double begin, required double end})
: super(begin: begin, end: end);
@override
double lerp(double t) {
final b = begin!, e = end!;
var diff = (e - b) % 360;
if (diff > 180) diff -= 360;
if (diff < -180) diff += 360;
return b + diff * t;
}
}