Navigációs térkép és útvoanlrögzítés hibáinak javítása

This commit is contained in:
2026-07-15 04:21:30 +02:00
parent 204d848ba4
commit 4660873350
4 changed files with 276 additions and 48 deletions
@@ -51,26 +51,48 @@ class VibroNavController extends GetxController {
// ── Periodikus napló ───────────────────────────────────────────────
final isLogging = false.obs;
final logIntervalSec = 60.obs; // felhasználó által állítható
final logIntervalSec = 5.obs; // felhasználó által állítható
final logs = <VehiclePositionLog>[].obs;
Timer? _logTimer;
// ── Térkép — a MapController itt él, nem a View State-jében ────────
final mapController = MapController();
StreamSubscription? _gnssUpdateSub;
/// A kezdő térképközéppont lekérdezése (gyors, telefon-GPS-alapú)
/// befejeződött-e — a View ez alapján dönti el, mikor építse fel a
/// térképet, hogy ne induljon rossz (pl. budapesti) középponttal.
final mapReady = false.obs;
@override
void onInit() {
super.onInit();
load();
refreshLogs();
if (Get.isRegistered<GnssService>()) {
ever(GnssService.to.lastGgaLine, (_) => _onPosition());
// FONTOS: a lastGgaLine csak NMEA (külső BT/BLE vevő) esetén
// frissül — telefon GPS-nél sosem. Az onDataUpdated viszont
// MINDKÉT forrásnál lefut, ez a helyes, egységes jelzés.
_gnssUpdateSub =
GnssService.to.onDataUpdated.listen((_) => _onPosition());
GnssService.to.determineInitialPosition().whenComplete(() {
mapReady.value = true;
});
} else {
mapReady.value = true;
}
// Automatikus indítás: amint van pozíció ÉS ki van választva jármű,
// induljon a rögzítés — korábban ez a bekötés hiányzott, a
// _tryAutoStart() metódust semmi nem hívta meg.
ever(hasPosition, (_) => _tryAutoStart());
ever(VehicleIdentityService.to.selectedVehicle, (_) => _tryAutoStart());
}
@override
void onClose() {
_logTimer?.cancel();
_gnssUpdateSub?.cancel();
super.onClose();
}
@@ -194,6 +216,13 @@ class VibroNavController extends GetxController {
_logTimer = null;
}
void _tryAutoStart() {
if (isLogging.value) return;
if (!hasPosition.value) return;
if (VehicleIdentityService.to.selectedVehicle.value == null) return;
startLogging();
}
Future<void> _logTick() async {
if (!hasPosition.value) return;
final projectId = ProjectService.to.activeProjectId;
@@ -9,7 +9,10 @@ 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';
import 'package:terepi_seged/services/coord_converter_service.dart';
import 'package:terepi_seged/services/gnss/gnss_connection.dart';
import 'package:terepi_seged/services/gnss/gnss_device_service.dart';
import 'package:terepi_seged/services/gnss/gnss_service.dart';
import 'package:terepi_seged/services/layer_import_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';
@@ -27,50 +30,80 @@ class VibroNavView extends GetView<VibroNavController> {
@override
Widget build(BuildContext context) {
return Scaffold(
appBar: AppBar(
title: const Text('Vibrátor navigáció'),
actions: [
IconButton(
icon: const Icon(Icons.upload_file),
tooltip: 'Forráspontok importja (SPS S-fájl)',
onPressed: _importSourcePoints,
),
IconButton(
icon: const Icon(Icons.local_shipping_outlined),
tooltip: 'Jármű kiválasztása',
onPressed: _pickVehicle,
),
],
),
body: Stack(
children: [
_buildMap(),
Obx(() {
final target =
controller.hasPosition.value && Get.isRegistered<GnssService>()
appBar: AppBar(
title: const Text('Navigáció'),
actions: [
IconButton(
icon: const Icon(Icons.bluetooth_connected),
tooltip: "Gyors újracsatlakozás a GNSS vevőhöz",
onPressed: _quickReconnectGnss,
),
IconButton(
icon: const Icon(Icons.layers_outlined),
tooltip: 'Réteg importja (GeoJSON/KML/KMZ)',
onPressed: _importLayer,
),
IconButton(
icon: const Icon(Icons.upload_file),
tooltip: 'Forráspontok importja (SPS S-fájl)',
onPressed: _importSourcePoints,
),
IconButton(
icon: const Icon(Icons.local_shipping_outlined),
tooltip: 'Jármű kiválasztása',
onPressed: _pickVehicle,
),
],
),
body: Obx(() {
if (!controller.mapReady.value) {
return const Center(
child: Column(
mainAxisSize: MainAxisSize.min,
children: [
CircularProgressIndicator(),
SizedBox(height: 12),
Text('Pozíció lekérése…'),
],
),
);
}
return Stack(
children: [
_buildMap(),
Builder(builder: (context) {
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(
left: 8,
right: 8,
top: 8,
child: _VehicleBadge(),
),
Positioned(
left: 8,
right: 8,
bottom: 8,
child: _NavPanel(controller: controller),
),
],
),
);
return AnimatedMapFollow(
mapController: controller.mapController,
target: target,
heading: controller.travelHeading.value,
);
}),
Positioned(
left: 8,
right: 8,
top: 8,
child: Row(
children: [
Expanded(child: _VehicleBadge()),
const SizedBox(width: 8),
const _GnssStatusBadge(),
],
),
),
Positioned(
left: 8,
right: 8,
bottom: 8,
child: _NavPanel(controller: controller),
),
],
);
}));
}
Widget _buildMap() {
@@ -78,10 +111,17 @@ class VibroNavView extends GetView<VibroNavController> {
final points = controller.sourcePoints;
final nearest = controller.nearestPoint.value;
final gnssPos =
Get.isRegistered<GnssService>() && GnssService.to.latitude.value != 0
? LatLng(
GnssService.to.latitude.value, GnssService.to.longitude.value)
: null;
final center = gnssPos ?? const LatLng(47.5, 19.05);
return FlutterMap(
mapController: controller.mapController,
options: const MapOptions(
initialCenter: LatLng(47.5, 19.05),
options: MapOptions(
initialCenter: center,
initialZoom: 15,
),
children: [
@@ -147,6 +187,38 @@ class VibroNavView extends GetView<VibroNavController> {
}
}
Future<void> _quickReconnectGnss() async {
final device = GnssDeviceService.to.selectedDevice.value;
if (device == null) {
Get.snackbar(
'Nincs korábbi GNSS-eszköz',
'Előbb válassz ki egyet a Kitűzés/Mérés beállításaiban.',
snackPosition: SnackPosition.BOTTOM,
);
return;
}
Get.snackbar(
'Újracsatlakozás',
'${device.name}" (${device.typeLabel}) — folyamatban…',
snackPosition: SnackPosition.BOTTOM,
duration: const Duration(seconds: 2),
);
await GnssService.to.reconnect();
}
Future<void> _importLayer() async {
try {
final layer = await LayerImportService.to.importFile();
if (layer != null) {
Get.snackbar('Réteg importálva', layer.name,
snackPosition: SnackPosition.BOTTOM);
}
} catch (e) {
Get.snackbar('Import hiba', e.toString(),
snackPosition: SnackPosition.BOTTOM);
}
}
// ── SPS S-fájl (forráspont) import — kompakt, egyetlen dialógus ────
Future<void> _importSourcePoints() async {
@@ -419,3 +491,59 @@ class _VehicleMarker extends StatelessWidget {
);
}
}
class _GnssStatusBadge extends StatelessWidget {
const _GnssStatusBadge();
@override
Widget build(BuildContext context) {
if (!Get.isRegistered<GnssService>()) return const SizedBox.shrink();
return Obx(() {
final gnss = GnssService.to;
final state = gnss.connectionState.value;
final quality = gnss.gpsQuality.value;
Color color;
IconData icon;
String label;
if (state == GnssConnectionState.connected && quality > 0) {
color = Colors.green;
icon = Icons.gps_fixed;
label = quality >= 4 ? 'RTK fix' : (quality == 2 ? 'RTK float' : 'GPS');
} else if (state == GnssConnectionState.connected) {
color = Colors.orange;
icon = Icons.gps_not_fixed;
label = 'Nincs fix';
} else if (state == GnssConnectionState.connecting) {
color = Colors.orange;
icon = Icons.gps_not_fixed;
label = 'Csatlakozás…';
} else {
color = Colors.red;
icon = Icons.gps_off;
label = 'Nincs GNSS';
}
return Container(
padding: const EdgeInsets.symmetric(horizontal: 8, vertical: 4),
decoration: BoxDecoration(
color: color.withOpacity(0.15),
borderRadius: BorderRadius.circular(6),
border: Border.all(color: color, width: 1),
),
child: Row(
mainAxisSize: MainAxisSize.min,
children: [
Icon(icon, size: 14, color: color),
const SizedBox(width: 4),
Text(label,
style: TextStyle(
fontSize: 11, fontWeight: FontWeight.w600, color: color)),
],
),
);
});
}
}
+3
View File
@@ -113,6 +113,9 @@ class AppPages {
name: Routes.VIBRONAV,
page: () => const VibroNavView(),
binding: BindingsBuilder(() {
if (Get.isRegistered<VibroNavController>()) {
Get.delete<VibroNavController>(force: true);
}
Get.put(VibroNavController());
}))
];
+71 -3
View File
@@ -10,7 +10,9 @@ import 'dart:io';
import 'dart:typed_data';
import 'package:file_picker/file_picker.dart';
import 'package:flutter/material.dart';
import 'package:flutter_map/flutter_map.dart';
import 'package:get/get.dart';
import 'package:latlong2/latlong.dart';
import 'package:path/path.dart' as p;
import 'package:path_provider/path_provider.dart';
import 'package:terepi_seged/enums/layer_import_source_type.dart';
@@ -300,10 +302,11 @@ class LayerImportService extends GetxService {
defaultPolygonBorderColor: const Color(0xCC1565C0),
defaultPolygonBorderStroke: 1.5,
defaultPolygonIsFilled: true,
markerCreationCallback: _pointMarkerWithLabel,
onMarkerTapCallback: (props) {
final label = props['name'] ?? props['title'] ?? '';
if (label.toString().isNotEmpty) {
Get.snackbar(label.toString(), props['description']?.toString() ?? '',
final label = _extractFeatureName(props);
if (label != null) {
Get.snackbar(label, props['description']?.toString() ?? '',
snackPosition: SnackPosition.BOTTOM,
duration: const Duration(seconds: 3));
}
@@ -323,6 +326,71 @@ class LayerImportService extends GetxService {
);
}
// ── Rugalmas névkinyerés — az attribútum neve forrásonként eltérhet ──
static const _nameKeys = [
'name',
'nev',
'név',
'label',
'title',
'megnevezes',
'megnevezés',
'ref',
'point'
];
/// A properties-ből megpróbál egy "nevet" kinyerni, a kulcs pontos
/// nevétől (kis/nagybetű) függetlenül, több elterjedt változatot is
/// figyelembe véve. Null, ha semelyik ismert kulcs alatt nincs
/// érdemi (nem üres) érték.
static String? _extractFeatureName(Map<String, dynamic> properties) {
final lower = {
for (final e in properties.entries) e.key.toLowerCase(): e.value,
};
for (final key in _nameKeys) {
final v = lower[key];
if (v != null && v.toString().trim().isNotEmpty) {
return v.toString().trim();
}
}
return null;
}
/// Pont-marker, ami — ha talál nevet az attribútumok közt — egy kis
/// feliratot is mutat a jelölő fölött, nem csak koppintásra.
static Marker _pointMarkerWithLabel(
LatLng point, Map<String, dynamic> properties) {
final label = _extractFeatureName(properties);
return Marker(
point: point,
width: 100,
height: 46,
child: Column(
mainAxisSize: MainAxisSize.min,
children: [
if (label != null)
Container(
padding: const EdgeInsets.symmetric(horizontal: 5, vertical: 1),
decoration: BoxDecoration(
color: Colors.white.withOpacity(0.9),
borderRadius: BorderRadius.circular(4),
border: Border.all(color: Colors.red.withOpacity(0.7)),
),
child: Text(
label,
maxLines: 1,
overflow: TextOverflow.ellipsis,
style:
const TextStyle(fontSize: 10, fontWeight: FontWeight.w600),
),
),
const Icon(Icons.location_pin, color: Colors.red, size: 26),
],
),
);
}
LayerImportSourceType _sourceType(String ext) => switch (ext.toLowerCase()) {
'kml' => LayerImportSourceType.kml,
'kmz' => LayerImportSourceType.kmz,