diff --git a/lib/pages/vibro_nav/presentation/controllers/vibro_nav_controller.dart b/lib/pages/vibro_nav/presentation/controllers/vibro_nav_controller.dart index d4e1b60..a300d0f 100644 --- a/lib/pages/vibro_nav/presentation/controllers/vibro_nav_controller.dart +++ b/lib/pages/vibro_nav/presentation/controllers/vibro_nav_controller.dart @@ -1,7 +1,9 @@ import 'dart:async'; import 'dart:math' as math; +import 'package:flutter_map/flutter_map.dart'; import 'package:get/get.dart'; +import 'package:latlong2/latlong.dart'; import 'package:terepi_seged/models/source_point.dart'; import 'package:terepi_seged/models/vechicle_position_log.dart'; import 'package:terepi_seged/services/app_database.dart'; @@ -50,12 +52,17 @@ class VibroNavController extends GetxController { // ── Periodikus napló ─────────────────────────────────────────────── final isLogging = false.obs; final logIntervalSec = 60.obs; // felhasználó által állítható + final logs = [].obs; Timer? _logTimer; + // ── Térkép — a MapController itt él, nem a View State-jében ──────── + final mapController = MapController(); + @override void onInit() { super.onInit(); load(); + refreshLogs(); if (Get.isRegistered()) { ever(GnssService.to.lastGgaLine, (_) => _onPosition()); } @@ -67,6 +74,10 @@ class VibroNavController extends GetxController { super.onClose(); } + Future refreshLogs() async { + logs.value = await loadLogs(); + } + Future load() async { final projectId = ProjectService.to.activeProjectId; if (projectId == null) { @@ -212,5 +223,6 @@ class VibroNavController extends GetxController { : null, ); await _db.insertVehiclePositionLog(entry); + logs.add(entry); } } diff --git a/lib/pages/vibro_nav/presentation/views/vibro_nav_view.dart b/lib/pages/vibro_nav/presentation/views/vibro_nav_view.dart index be726e7..5ab3ae0 100644 --- a/lib/pages/vibro_nav/presentation/views/vibro_nav_view.dart +++ b/lib/pages/vibro_nav/presentation/views/vibro_nav_view.dart @@ -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/sps_import_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 '../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 /// MapSurveyMode — saját MapController kell a forgó (track-up) /// térképhez, hogy ez semmilyen más módot ne érintsen. -class VibroNavView extends StatefulWidget { +class VibroNavView extends GetView { const VibroNavView({super.key}); - @override - State createState() => _VibroNavViewState(); -} - -class _VibroNavViewState extends State { - late final VibroNavController controller; - final _mapController = MapController(); - List _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(); - super.dispose(); - } - - Future _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()) { - return; - } - final gnss = GnssService.to; - try { - _mapController.move(LatLng(gnss.latitude.value, gnss.longitude.value), - _mapController.camera.zoom); - } catch (_) {} - } - @override Widget build(BuildContext context) { return Scaffold( @@ -94,6 +45,17 @@ class _VibroNavViewState extends State { body: Stack( children: [ _buildMap(), + Obx(() { + final target = + controller.hasPosition.value && Get.isRegistered() + ? LatLng(GnssService.to.latitude.value, + GnssService.to.longitude.value) + : null; + return AnimatedMapFollow( + mapController: controller.mapController, + target: target, + heading: controller.travelHeading.value); + }), Positioned( left: 8, right: 8, @@ -104,8 +66,7 @@ class _VibroNavViewState extends State { left: 8, right: 8, bottom: 8, - child: _NavPanel( - controller: controller, onLoggingToggled: _refreshLogs), + child: _NavPanel(controller: controller), ), ], ), @@ -118,16 +79,22 @@ class _VibroNavViewState extends State { final nearest = controller.nearestPoint.value; return FlutterMap( - mapController: _mapController, + mapController: controller.mapController, options: const MapOptions( initialCenter: LatLng(47.5, 19.05), initialZoom: 15, ), children: [ + // TileLayer( + // urlTemplate: 'https://tile.openstreetmap.org/{z}/{x}/{y}.png', + // userAgentPackageName: 'hu.app_dev.terepi_seged', + // ), TileLayer( - urlTemplate: 'https://tile.openstreetmap.org/{z}/{x}/{y}.png', - userAgentPackageName: 'hu.app_dev.terepi_seged', + urlTemplate: 'http://{s}.google.com/vt/lyrs=s,h&x={x}&y={y}&z={z}', + subdomains: const ['mt0', 'mt1', 'mt2', 'mt3'], + maxNativeZoom: 18, ), + // Tervezett útvonal + minden más importált háttérréteg — a // MEGLÉVŐ rétegimport-mechanizmus, nincs hozzá új kód. const ImportedLayerOverlay(), @@ -140,7 +107,7 @@ class _VibroNavViewState extends State { child: _SourcePointMarker( point: sp, isNearest: sp.uuid == nearest?.uuid, - isVerified: controller.isVerified(sp, _logs), + isVerified: controller.isVerified(sp, controller.logs), ), ), if (controller.hasPosition.value) @@ -237,6 +204,7 @@ class _VibroNavViewState extends State { } final inserted = await AppDatabase.instance.insertSourcePoints(saved); await controller.load(); + await controller.refreshLogs(); Get.snackbar('Import kész', '$inserted forráspont importálva', snackPosition: SnackPosition.BOTTOM); } catch (e) { @@ -252,8 +220,7 @@ class _VibroNavViewState extends State { class _NavPanel extends StatelessWidget { final VibroNavController controller; - final VoidCallback onLoggingToggled; - const _NavPanel({required this.controller, required this.onLoggingToggled}); + const _NavPanel({required this.controller}); @override Widget build(BuildContext context) { @@ -298,7 +265,6 @@ class _NavPanel extends StatelessWidget { '(${controller.logIntervalSec.value} mp)'), onPressed: () { controller.stopLogging(); - onLoggingToggled(); }, ) : FilledButton.icon( @@ -308,7 +274,6 @@ class _NavPanel extends StatelessWidget { '(${controller.logIntervalSec.value} mp-enként)'), onPressed: () { controller.startLogging(); - onLoggingToggled(); }, ), ), diff --git a/lib/routes/app_pages.dart b/lib/routes/app_pages.dart index f8fc772..508b424 100644 --- a/lib/routes/app_pages.dart +++ b/lib/routes/app_pages.dart @@ -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/tracking/bindings/tracking_bindings.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 '../pages/map_test/bindings/map_test_bindings.dart'; @@ -108,6 +109,11 @@ class AppPages { GetPage( name: Routes.STAKEOUT_IMPORT, page: () => const StakeoutImportView()), 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()); + })) ]; } diff --git a/lib/widgets/map/animated_map_follow.dart b/lib/widgets/map/animated_map_follow.dart new file mode 100644 index 0000000..0947065 --- /dev/null +++ b/lib/widgets/map/animated_map_follow.dart @@ -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( + 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( + // 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 { + _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 { + _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; + } +}