Jármű navigáció @3h
This commit is contained in:
@@ -0,0 +1,216 @@
|
||||
import 'dart:async';
|
||||
import 'dart:math' as math;
|
||||
|
||||
import 'package:get/get.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/device_identity_service.dart';
|
||||
import 'package:terepi_seged/services/gnss/gnss_service.dart';
|
||||
import 'package:terepi_seged/services/project_service.dart';
|
||||
import 'package:terepi_seged/services/vechicle_identity_service.dart';
|
||||
|
||||
/// Egyszerű navigációs panel a jelgerjesztő (vibrátor) járműhöz.
|
||||
///
|
||||
/// A pozíció a KITŰZÉSSEL AZONOS forrásból jön (GnssService — külső
|
||||
/// BT/BLE GNSS-egység a járműben), semmilyen új eszköz-integráció nem
|
||||
/// kell hozzá. A forráspontok tisztán referencia-réteg (nincs mutálható
|
||||
/// "meglőve" állapotuk); az ellenőrzés a periodikus pozíciónapló és a
|
||||
/// terv UTÓLAGOS összevetéséből adódik (lásd isVerified).
|
||||
class VibroNavController extends GetxController {
|
||||
AppDatabase get _db => AppDatabase.instance;
|
||||
|
||||
// ── Forráspontok ─────────────────────────────────────────────────
|
||||
final sourcePoints = <SourcePoint>[].obs;
|
||||
|
||||
/// Egyezés-tűrés (m) a naplózott pozíció és a terv-forráspont között.
|
||||
final tolerance = 15.0.obs;
|
||||
|
||||
// ── Élő pozíció (a GnssService-ből, EOV-ra konvertálva) ───────────
|
||||
final hasPosition = false.obs;
|
||||
final curEovY = 0.0.obs;
|
||||
final curEovX = 0.0.obs;
|
||||
final speedKmh = 0.0.obs;
|
||||
final sessionDistanceM = 0.0.obs;
|
||||
|
||||
/// Haladási irány (fok) — pozíció-előzményből számolva, ALACSONY
|
||||
/// SEBESSÉGNÉL BEFAGYASZTVA (a nyers GPS-heading álló/lassú helyzetben
|
||||
/// zajos/értelmetlen — ugyanez a minta, mint a Kitűzésnél).
|
||||
final travelHeading = Rxn<double>();
|
||||
static const _headingFreezeSpeedKmh = 5.0;
|
||||
|
||||
double? _histY, _histX;
|
||||
DateTime? _histTime;
|
||||
|
||||
// ── Legközelebbi forráspont ───────────────────────────────────────
|
||||
final nearestPoint = Rxn<SourcePoint>();
|
||||
final nearestDistance = 0.0.obs;
|
||||
|
||||
// ── Periodikus napló ───────────────────────────────────────────────
|
||||
final isLogging = false.obs;
|
||||
final logIntervalSec = 60.obs; // felhasználó által állítható
|
||||
Timer? _logTimer;
|
||||
|
||||
@override
|
||||
void onInit() {
|
||||
super.onInit();
|
||||
load();
|
||||
if (Get.isRegistered<GnssService>()) {
|
||||
ever(GnssService.to.lastGgaLine, (_) => _onPosition());
|
||||
}
|
||||
}
|
||||
|
||||
@override
|
||||
void onClose() {
|
||||
_logTimer?.cancel();
|
||||
super.onClose();
|
||||
}
|
||||
|
||||
Future<void> load() async {
|
||||
final projectId = ProjectService.to.activeProjectId;
|
||||
if (projectId == null) {
|
||||
sourcePoints.clear();
|
||||
return;
|
||||
}
|
||||
sourcePoints.value = await _db.listSourcePoints(projectId);
|
||||
}
|
||||
|
||||
// ═════════════════════════════════════════════════════════════════
|
||||
// Pozíció-feldolgozás
|
||||
// ═════════════════════════════════════════════════════════════════
|
||||
|
||||
void _onPosition() {
|
||||
final gnss = GnssService.to;
|
||||
if (gnss.gpsQuality.value <= 0 ||
|
||||
gnss.latitude.value == 0 ||
|
||||
!Get.isRegistered<CoordConverterService>()) {
|
||||
return;
|
||||
}
|
||||
|
||||
final p = CoordConverterService.to
|
||||
.wgsToEovPoint(gnss.longitude.value, gnss.latitude.value);
|
||||
final now = DateTime.now();
|
||||
|
||||
if (_histY != null && _histTime != null) {
|
||||
final dy = p.x - _histY!;
|
||||
final dx = p.y - _histX!;
|
||||
final dist = math.sqrt(dy * dy + dx * dx);
|
||||
final dtSec = now.difference(_histTime!).inMilliseconds / 1000.0;
|
||||
|
||||
if (dtSec > 0) {
|
||||
final v = dist / dtSec; // m/s
|
||||
speedKmh.value = v * 3.6;
|
||||
sessionDistanceM.value += dist;
|
||||
|
||||
// Irány csak akkor frissül, ha a sebesség a fagyasztási küszöb
|
||||
// felett van — álló/nagyon lassú helyzetben a nyers irány zajos.
|
||||
if (speedKmh.value >= _headingFreezeSpeedKmh && dist > 0.3) {
|
||||
travelHeading.value =
|
||||
(math.atan2(dy, dx) * 180 / math.pi + 360) % 360;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
_histY = p.x;
|
||||
_histX = p.y;
|
||||
_histTime = now;
|
||||
|
||||
curEovY.value = p.x;
|
||||
curEovX.value = p.y;
|
||||
hasPosition.value = true;
|
||||
|
||||
_updateNearest();
|
||||
}
|
||||
|
||||
void _updateNearest() {
|
||||
if (sourcePoints.isEmpty) {
|
||||
nearestPoint.value = null;
|
||||
return;
|
||||
}
|
||||
SourcePoint? best;
|
||||
var bestDist = double.infinity;
|
||||
for (final sp in sourcePoints) {
|
||||
final dy = sp.planEovY - curEovY.value;
|
||||
final dx = sp.planEovX - curEovX.value;
|
||||
final d = math.sqrt(dy * dy + dx * dx);
|
||||
if (d < bestDist) {
|
||||
bestDist = d;
|
||||
best = sp;
|
||||
}
|
||||
}
|
||||
nearestPoint.value = best;
|
||||
nearestDistance.value = bestDist;
|
||||
}
|
||||
|
||||
// ═════════════════════════════════════════════════════════════════
|
||||
// Terv/napló összevetés — melyik forráspont "igazolt"
|
||||
// ═════════════════════════════════════════════════════════════════
|
||||
|
||||
/// Egyszerű, szinkron ellenőrzés a már betöltött [logs] lista alapján
|
||||
/// (a nézet előre lekéri, hogy ne fusson adatbázis-lekérdezés minden
|
||||
/// egyes marker kirajzolásakor).
|
||||
bool isVerified(SourcePoint sp, List<VehiclePositionLog> logs) {
|
||||
for (final log in logs) {
|
||||
final dy = sp.planEovY - log.eovY;
|
||||
final dx = sp.planEovX - log.eovX;
|
||||
if (math.sqrt(dy * dy + dx * dx) <= tolerance.value) return true;
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
Future<List<VehiclePositionLog>> loadLogs() async {
|
||||
final projectId = ProjectService.to.activeProjectId;
|
||||
if (projectId == null) return [];
|
||||
return _db.listVehiclePositionLogs(projectId);
|
||||
}
|
||||
|
||||
// ═════════════════════════════════════════════════════════════════
|
||||
// Periodikus rögzítés
|
||||
// ═════════════════════════════════════════════════════════════════
|
||||
|
||||
void startLogging() {
|
||||
if (isLogging.value) return;
|
||||
isLogging.value = true;
|
||||
_logTick(); // azonnali első pont
|
||||
_logTimer = Timer.periodic(
|
||||
Duration(seconds: logIntervalSec.value), (_) => _logTick());
|
||||
}
|
||||
|
||||
void stopLogging() {
|
||||
isLogging.value = false;
|
||||
_logTimer?.cancel();
|
||||
_logTimer = null;
|
||||
}
|
||||
|
||||
Future<void> _logTick() async {
|
||||
if (!hasPosition.value) return;
|
||||
final projectId = ProjectService.to.activeProjectId;
|
||||
final vehicleId = VehicleIdentityService.to.selectedVehicle.value;
|
||||
if (projectId == null || vehicleId == null) return;
|
||||
|
||||
final gnss = GnssService.to;
|
||||
final entry = VehiclePositionLog(
|
||||
projectId: projectId,
|
||||
vehicleId: vehicleId,
|
||||
eovY: curEovY.value,
|
||||
eovX: curEovX.value,
|
||||
lat: gnss.latitude.value,
|
||||
lon: gnss.longitude.value,
|
||||
altitude: gnss.altitude.value,
|
||||
speedKmh: speedKmh.value,
|
||||
heading: travelHeading.value,
|
||||
fixQuality: gnss.gpsQuality.value,
|
||||
accuracy: gnss.horizontalAccuracy,
|
||||
deviceId: Get.isRegistered<DeviceIdentityService>() &&
|
||||
DeviceIdentityService.to.isReady
|
||||
? DeviceIdentityService.to.deviceId
|
||||
: null,
|
||||
appInstanceId: Get.isRegistered<DeviceIdentityService>() &&
|
||||
DeviceIdentityService.to.isReady
|
||||
? DeviceIdentityService.to.appInstanceId
|
||||
: null,
|
||||
);
|
||||
await _db.insertVehiclePositionLog(entry);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,456 @@
|
||||
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/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 StatefulWidget {
|
||||
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
|
||||
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(),
|
||||
Positioned(
|
||||
left: 8,
|
||||
right: 8,
|
||||
top: 8,
|
||||
child: _VehicleBadge(),
|
||||
),
|
||||
Positioned(
|
||||
left: 8,
|
||||
right: 8,
|
||||
bottom: 8,
|
||||
child: _NavPanel(
|
||||
controller: controller, onLoggingToggled: _refreshLogs),
|
||||
),
|
||||
],
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
Widget _buildMap() {
|
||||
return Obx(() {
|
||||
final points = controller.sourcePoints;
|
||||
final nearest = controller.nearestPoint.value;
|
||||
|
||||
return FlutterMap(
|
||||
mapController: _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',
|
||||
),
|
||||
// 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, _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();
|
||||
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;
|
||||
final VoidCallback onLoggingToggled;
|
||||
const _NavPanel({required this.controller, required this.onLoggingToggled});
|
||||
|
||||
@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();
|
||||
onLoggingToggled();
|
||||
},
|
||||
)
|
||||
: 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();
|
||||
onLoggingToggled();
|
||||
},
|
||||
),
|
||||
),
|
||||
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),
|
||||
);
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user