import 'dart:io'; 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: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'; 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'; /// 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 GetView { const VibroNavView({super.key}); @override Widget build(BuildContext context) { return Scaffold( 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() ? 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: Row( children: [ Expanded(child: _VehicleBadge()), const SizedBox(width: 8), const _GnssStatusBadge(), ], ), ), Positioned( left: 8, right: 8, bottom: 8, child: _NavPanel(controller: controller), ), ], ); })); } Widget _buildMap() { return Obx(() { final points = controller.sourcePoints; final nearest = controller.nearestPoint.value; final gnssPos = Get.isRegistered() && 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: MapOptions( initialCenter: center, initialZoom: 15, ), children: [ // TileLayer( // urlTemplate: 'https://tile.openstreetmap.org/{z}/{x}/{y}.png', // userAgentPackageName: 'hu.app_dev.terepi_seged', // ), TileLayer( 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(), MarkerLayer(markers: [ for (final sp in points) Marker( point: LatLng(sp.planLat, sp.planLon), width: 40, height: 40, child: _SourcePointMarker( point: sp, isNearest: sp.uuid == nearest?.uuid, isVerified: controller.isVerified(sp, controller.logs), ), ), if (controller.hasPosition.value) Marker( point: LatLng(GnssService.to.latitude.value, GnssService.to.longitude.value), width: 34, height: 34, child: const _VehicleMarker(), ), ]), ], ); }); } // ── Jármű-választás ──────────────────────────────────────────────── Future _pickVehicle() async { final chosen = await Get.dialog(AlertDialog( title: const Text('Melyik járműben van ez a tablet?'), content: Column( mainAxisSize: MainAxisSize.min, children: [ for (final v in VehicleIdentityService.availableVehicles) RadioListTile( title: Text(v), value: v, groupValue: VehicleIdentityService.to.selectedVehicle.value, onChanged: (val) => Get.back(result: val), ), ], ), )); if (chosen != null) { await VehicleIdentityService.to.setVehicle(chosen); } } Future _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 _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 _importSourcePoints() async { final result = await FilePicker.platform.pickFiles(type: FileType.any); final path = result?.files.single.path; if (path == null) return; final projectId = ProjectService.to.activeProjectId; if (projectId == null) return; try { final content = await File(path).readAsString(); final points = SpsParser.parseSourceFile(content); if (points.isEmpty) { Get.snackbar( 'Import', 'Nem sikerült forráspontot beolvasni ebből a fájlból — ' 'ellenőrizd, hogy valódi SPS S-fájl-e.', snackPosition: SnackPosition.BOTTOM); return; } final withPos = points.where((p) => p.eovY != null && p.eovX != null); final ok = await Get.dialog(AlertDialog( title: const Text('Forráspontok importja'), content: Text('${points.length} pont az S-fájlban, ' '${withPos.length} érvényes koordinátával.\n\n' 'Első néhány: ${points.take(3).map((p) => '${p.lineId}·${p.station}').join(', ')}…'), actions: [ TextButton( onPressed: () => Get.back(result: false), child: const Text('Mégse')), FilledButton( onPressed: () => Get.back(result: true), child: const Text('Import')), ], )); if (ok != true) return; final conv = CoordConverterService.to; final batch = DateTime.now().toIso8601String(); final saved = []; for (final p in withPos) { final w = conv.eovToWgsPoint(p.eovY!, p.eovX!); saved.add(SourcePoint( projectId: projectId, lineId: p.lineId, station: p.station, planEovY: p.eovY!, planEovX: p.eovX!, planLat: w.y, planLon: w.x, importBatch: batch, )); } 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) { Get.snackbar('Import hiba', e.toString(), snackPosition: SnackPosition.BOTTOM); } } } // ═══════════════════════════════════════════════════════════════════ // HUD panel: sebesség, táv, legközelebbi pont, rögzítés vezérlés // ═══════════════════════════════════════════════════════════════════ class _NavPanel extends StatelessWidget { final VibroNavController controller; const _NavPanel({required this.controller}); @override Widget build(BuildContext context) { return Obx(() { final nearest = controller.nearestPoint.value; return Card( elevation: 6, child: Padding( padding: const EdgeInsets.all(12), child: Column( mainAxisSize: MainAxisSize.min, children: [ Row( mainAxisAlignment: MainAxisAlignment.spaceAround, children: [ _Stat( label: 'Sebesség', value: '${controller.speedKmh.value.toStringAsFixed(0)} km/h'), _Stat( label: 'Megtett táv', value: _fmtDist(controller.sessionDistanceM.value)), if (nearest != null) _Stat( label: 'Legközelebbi VP', value: nearest.displayId, highlight: true), if (nearest != null) _Stat( label: 'Táv odáig', value: _fmtDist(controller.nearestDistance.value)), ], ), const SizedBox(height: 8), Row( children: [ Expanded( child: controller.isLogging.value ? OutlinedButton.icon( icon: const Icon(Icons.stop_circle_outlined), label: Text('Rögzítés leállítása ' '(${controller.logIntervalSec.value} mp)'), onPressed: () { controller.stopLogging(); }, ) : FilledButton.icon( icon: const Icon(Icons.fiber_manual_record, color: Colors.red), label: Text('Rögzítés indítása ' '(${controller.logIntervalSec.value} mp-enként)'), onPressed: () { controller.startLogging(); }, ), ), const SizedBox(width: 8), IconButton( icon: const Icon(Icons.timer_outlined), tooltip: 'Rögzítési időköz', onPressed: controller.isLogging.value ? null : () => _pickInterval(controller), ), ], ), ], ), ), ); }); } Future _pickInterval(VibroNavController c) async { final opts = [15, 30, 60, 120, 300]; final chosen = await Get.dialog(AlertDialog( title: const Text('Rögzítési időköz'), content: Column( mainAxisSize: MainAxisSize.min, children: [ for (final s in opts) RadioListTile( title: Text(s < 60 ? '$s másodperc' : '${s ~/ 60} perc'), value: s, groupValue: c.logIntervalSec.value, onChanged: (v) => Get.back(result: v), ), ], ), )); if (chosen != null) c.logIntervalSec.value = chosen; } static String _fmtDist(double m) => m < 1000 ? '${m.toStringAsFixed(0)} m' : '${(m / 1000).toStringAsFixed(2)} km'; } class _Stat extends StatelessWidget { final String label; final String value; final bool highlight; const _Stat( {required this.label, required this.value, this.highlight = false}); @override Widget build(BuildContext context) { return Column( mainAxisSize: MainAxisSize.min, children: [ Text(value, style: TextStyle( fontSize: highlight ? 16 : 18, fontWeight: FontWeight.w700, color: highlight ? Theme.of(context).colorScheme.primary : null)), Text(label, style: TextStyle(fontSize: 10, color: Colors.grey.shade600)), ], ); } } // ═══════════════════════════════════════════════════════════════════ class _VehicleBadge extends StatelessWidget { @override Widget build(BuildContext context) { return Obx(() { final v = VehicleIdentityService.to.selectedVehicle.value; return Align( alignment: Alignment.topLeft, child: Container( padding: const EdgeInsets.symmetric(horizontal: 10, vertical: 5), decoration: BoxDecoration( color: v == null ? Colors.orange.withOpacity(0.9) : Colors.black.withOpacity(0.7), borderRadius: BorderRadius.circular(8), ), child: Text( v == null ? 'Nincs jármű kiválasztva!' : 'Jármű: $v', style: const TextStyle(color: Colors.white, fontSize: 12), ), ), ); }); } } class _SourcePointMarker extends StatelessWidget { final SourcePoint point; final bool isNearest; final bool isVerified; const _SourcePointMarker( {required this.point, required this.isNearest, required this.isVerified}); @override Widget build(BuildContext context) { final color = isVerified ? Colors.green : Colors.grey.shade600; return Column( mainAxisSize: MainAxisSize.min, children: [ Icon( isVerified ? Icons.check_circle : Icons.radio_button_unchecked, color: isNearest ? Colors.red : color, size: isNearest ? 26 : 18, ), Text(point.displayId, style: TextStyle( fontSize: 9, fontWeight: isNearest ? FontWeight.w700 : FontWeight.w400, color: isNearest ? Colors.red : Colors.black87)), ], ); } } class _VehicleMarker extends StatelessWidget { const _VehicleMarker(); @override Widget build(BuildContext context) { // Track-up módban a szimbólum mindig "felfelé" mutat — a világ // forog körülötte, nem a szimbólum a világ körül. return Container( decoration: BoxDecoration( color: Colors.blue, shape: BoxShape.circle, border: Border.all(color: Colors.white, width: 3), boxShadow: [ BoxShadow(color: Colors.blue.withOpacity(0.5), blurRadius: 8) ], ), child: const Icon(Icons.navigation, color: Colors.white, size: 18), ); } } class _GnssStatusBadge extends StatelessWidget { const _GnssStatusBadge(); @override Widget build(BuildContext context) { if (!Get.isRegistered()) 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)), ], ), ); }); } }