Navigáció javítása, fejlesztése
This commit is contained in:
@@ -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();
|
|
||||||
},
|
},
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
|
|||||||
@@ -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());
|
||||||
|
}))
|
||||||
];
|
];
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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;
|
||||||
|
}
|
||||||
|
}
|
||||||
Reference in New Issue
Block a user