Files
MobilApp/lib/pages/vibro_nav/presentation/views/vibro_nav_view.dart
T

422 lines
15 KiB
Dart

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_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<VibroNavController> {
const VibroNavView({super.key});
@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>()
? 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),
),
],
),
);
}
Widget _buildMap() {
return Obx(() {
final points = controller.sourcePoints;
final nearest = controller.nearestPoint.value;
return FlutterMap(
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: '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<void> _pickVehicle() async {
final chosen = await Get.dialog<String>(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<String>(
title: Text(v),
value: v,
groupValue: VehicleIdentityService.to.selectedVehicle.value,
onChanged: (val) => Get.back(result: val),
),
],
),
));
if (chosen != null) {
await VehicleIdentityService.to.setVehicle(chosen);
}
}
// ── SPS S-fájl (forráspont) import — kompakt, egyetlen dialógus ────
Future<void> _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<bool>(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 = <SourcePoint>[];
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<void> _pickInterval(VibroNavController c) async {
final opts = [15, 30, 60, 120, 300];
final chosen = await Get.dialog<int>(AlertDialog(
title: const Text('Rögzítési időköz'),
content: Column(
mainAxisSize: MainAxisSize.min,
children: [
for (final s in opts)
RadioListTile<int>(
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),
);
}
}