Navigációs térkép és útvoanlrögzítés hibáinak javítása
This commit is contained in:
@@ -51,26 +51,48 @@ 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 = 5.obs; // felhasználó által állítható
|
||||||
final logs = <VehiclePositionLog>[].obs;
|
final logs = <VehiclePositionLog>[].obs;
|
||||||
Timer? _logTimer;
|
Timer? _logTimer;
|
||||||
|
|
||||||
// ── Térkép — a MapController itt él, nem a View State-jében ────────
|
// ── Térkép — a MapController itt él, nem a View State-jében ────────
|
||||||
final mapController = MapController();
|
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
|
@override
|
||||||
void onInit() {
|
void onInit() {
|
||||||
super.onInit();
|
super.onInit();
|
||||||
load();
|
load();
|
||||||
refreshLogs();
|
refreshLogs();
|
||||||
if (Get.isRegistered<GnssService>()) {
|
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
|
@override
|
||||||
void onClose() {
|
void onClose() {
|
||||||
_logTimer?.cancel();
|
_logTimer?.cancel();
|
||||||
|
_gnssUpdateSub?.cancel();
|
||||||
super.onClose();
|
super.onClose();
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -194,6 +216,13 @@ class VibroNavController extends GetxController {
|
|||||||
_logTimer = null;
|
_logTimer = null;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
void _tryAutoStart() {
|
||||||
|
if (isLogging.value) return;
|
||||||
|
if (!hasPosition.value) return;
|
||||||
|
if (VehicleIdentityService.to.selectedVehicle.value == null) return;
|
||||||
|
startLogging();
|
||||||
|
}
|
||||||
|
|
||||||
Future<void> _logTick() async {
|
Future<void> _logTick() async {
|
||||||
if (!hasPosition.value) return;
|
if (!hasPosition.value) return;
|
||||||
final projectId = ProjectService.to.activeProjectId;
|
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/models/vechicle_position_log.dart';
|
||||||
import 'package:terepi_seged/services/app_database.dart';
|
import 'package:terepi_seged/services/app_database.dart';
|
||||||
import 'package:terepi_seged/services/coord_converter_service.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/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/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';
|
||||||
@@ -27,50 +30,80 @@ class VibroNavView extends GetView<VibroNavController> {
|
|||||||
@override
|
@override
|
||||||
Widget build(BuildContext context) {
|
Widget build(BuildContext context) {
|
||||||
return Scaffold(
|
return Scaffold(
|
||||||
appBar: AppBar(
|
appBar: AppBar(
|
||||||
title: const Text('Vibrátor navigáció'),
|
title: const Text('Navigáció'),
|
||||||
actions: [
|
actions: [
|
||||||
IconButton(
|
IconButton(
|
||||||
icon: const Icon(Icons.upload_file),
|
icon: const Icon(Icons.bluetooth_connected),
|
||||||
tooltip: 'Forráspontok importja (SPS S-fájl)',
|
tooltip: "Gyors újracsatlakozás a GNSS vevőhöz",
|
||||||
onPressed: _importSourcePoints,
|
onPressed: _quickReconnectGnss,
|
||||||
),
|
),
|
||||||
IconButton(
|
IconButton(
|
||||||
icon: const Icon(Icons.local_shipping_outlined),
|
icon: const Icon(Icons.layers_outlined),
|
||||||
tooltip: 'Jármű kiválasztása',
|
tooltip: 'Réteg importja (GeoJSON/KML/KMZ)',
|
||||||
onPressed: _pickVehicle,
|
onPressed: _importLayer,
|
||||||
),
|
),
|
||||||
],
|
IconButton(
|
||||||
),
|
icon: const Icon(Icons.upload_file),
|
||||||
body: Stack(
|
tooltip: 'Forráspontok importja (SPS S-fájl)',
|
||||||
children: [
|
onPressed: _importSourcePoints,
|
||||||
_buildMap(),
|
),
|
||||||
Obx(() {
|
IconButton(
|
||||||
final target =
|
icon: const Icon(Icons.local_shipping_outlined),
|
||||||
controller.hasPosition.value && Get.isRegistered<GnssService>()
|
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,
|
? LatLng(GnssService.to.latitude.value,
|
||||||
GnssService.to.longitude.value)
|
GnssService.to.longitude.value)
|
||||||
: null;
|
: null;
|
||||||
return AnimatedMapFollow(
|
return AnimatedMapFollow(
|
||||||
mapController: controller.mapController,
|
mapController: controller.mapController,
|
||||||
target: target,
|
target: target,
|
||||||
heading: controller.travelHeading.value);
|
heading: controller.travelHeading.value,
|
||||||
}),
|
);
|
||||||
Positioned(
|
}),
|
||||||
left: 8,
|
Positioned(
|
||||||
right: 8,
|
left: 8,
|
||||||
top: 8,
|
right: 8,
|
||||||
child: _VehicleBadge(),
|
top: 8,
|
||||||
),
|
child: Row(
|
||||||
Positioned(
|
children: [
|
||||||
left: 8,
|
Expanded(child: _VehicleBadge()),
|
||||||
right: 8,
|
const SizedBox(width: 8),
|
||||||
bottom: 8,
|
const _GnssStatusBadge(),
|
||||||
child: _NavPanel(controller: controller),
|
],
|
||||||
),
|
),
|
||||||
],
|
),
|
||||||
),
|
Positioned(
|
||||||
);
|
left: 8,
|
||||||
|
right: 8,
|
||||||
|
bottom: 8,
|
||||||
|
child: _NavPanel(controller: controller),
|
||||||
|
),
|
||||||
|
],
|
||||||
|
);
|
||||||
|
}));
|
||||||
}
|
}
|
||||||
|
|
||||||
Widget _buildMap() {
|
Widget _buildMap() {
|
||||||
@@ -78,10 +111,17 @@ class VibroNavView extends GetView<VibroNavController> {
|
|||||||
final points = controller.sourcePoints;
|
final points = controller.sourcePoints;
|
||||||
final nearest = controller.nearestPoint.value;
|
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(
|
return FlutterMap(
|
||||||
mapController: controller.mapController,
|
mapController: controller.mapController,
|
||||||
options: const MapOptions(
|
options: MapOptions(
|
||||||
initialCenter: LatLng(47.5, 19.05),
|
initialCenter: center,
|
||||||
initialZoom: 15,
|
initialZoom: 15,
|
||||||
),
|
),
|
||||||
children: [
|
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 ────
|
// ── SPS S-fájl (forráspont) import — kompakt, egyetlen dialógus ────
|
||||||
|
|
||||||
Future<void> _importSourcePoints() async {
|
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)),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|||||||
@@ -113,6 +113,9 @@ class AppPages {
|
|||||||
name: Routes.VIBRONAV,
|
name: Routes.VIBRONAV,
|
||||||
page: () => const VibroNavView(),
|
page: () => const VibroNavView(),
|
||||||
binding: BindingsBuilder(() {
|
binding: BindingsBuilder(() {
|
||||||
|
if (Get.isRegistered<VibroNavController>()) {
|
||||||
|
Get.delete<VibroNavController>(force: true);
|
||||||
|
}
|
||||||
Get.put(VibroNavController());
|
Get.put(VibroNavController());
|
||||||
}))
|
}))
|
||||||
];
|
];
|
||||||
|
|||||||
@@ -10,7 +10,9 @@ import 'dart:io';
|
|||||||
import 'dart:typed_data';
|
import 'dart:typed_data';
|
||||||
import 'package:file_picker/file_picker.dart';
|
import 'package:file_picker/file_picker.dart';
|
||||||
import 'package:flutter/material.dart';
|
import 'package:flutter/material.dart';
|
||||||
|
import 'package:flutter_map/flutter_map.dart';
|
||||||
import 'package:get/get.dart';
|
import 'package:get/get.dart';
|
||||||
|
import 'package:latlong2/latlong.dart';
|
||||||
import 'package:path/path.dart' as p;
|
import 'package:path/path.dart' as p;
|
||||||
import 'package:path_provider/path_provider.dart';
|
import 'package:path_provider/path_provider.dart';
|
||||||
import 'package:terepi_seged/enums/layer_import_source_type.dart';
|
import 'package:terepi_seged/enums/layer_import_source_type.dart';
|
||||||
@@ -300,10 +302,11 @@ class LayerImportService extends GetxService {
|
|||||||
defaultPolygonBorderColor: const Color(0xCC1565C0),
|
defaultPolygonBorderColor: const Color(0xCC1565C0),
|
||||||
defaultPolygonBorderStroke: 1.5,
|
defaultPolygonBorderStroke: 1.5,
|
||||||
defaultPolygonIsFilled: true,
|
defaultPolygonIsFilled: true,
|
||||||
|
markerCreationCallback: _pointMarkerWithLabel,
|
||||||
onMarkerTapCallback: (props) {
|
onMarkerTapCallback: (props) {
|
||||||
final label = props['name'] ?? props['title'] ?? '';
|
final label = _extractFeatureName(props);
|
||||||
if (label.toString().isNotEmpty) {
|
if (label != null) {
|
||||||
Get.snackbar(label.toString(), props['description']?.toString() ?? '',
|
Get.snackbar(label, props['description']?.toString() ?? '',
|
||||||
snackPosition: SnackPosition.BOTTOM,
|
snackPosition: SnackPosition.BOTTOM,
|
||||||
duration: const Duration(seconds: 3));
|
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()) {
|
LayerImportSourceType _sourceType(String ext) => switch (ext.toLowerCase()) {
|
||||||
'kml' => LayerImportSourceType.kml,
|
'kml' => LayerImportSourceType.kml,
|
||||||
'kmz' => LayerImportSourceType.kmz,
|
'kmz' => LayerImportSourceType.kmz,
|
||||||
|
|||||||
Reference in New Issue
Block a user