2026-10-04 20:18:08 +02:00
|
|
|
import 'dart:math' as math;
|
2026-07-13 07:58:51 +02:00
|
|
|
|
|
|
|
|
import 'package:flutter/material.dart';
|
|
|
|
|
import 'package:flutter_map/flutter_map.dart';
|
|
|
|
|
import 'package:get/get.dart';
|
|
|
|
|
import 'package:latlong2/latlong.dart';
|
2026-10-04 22:24:31 +02:00
|
|
|
import 'package:terepi_seged/models/vibro_nav_route.dart';
|
2026-10-04 20:18:08 +02:00
|
|
|
import 'package:terepi_seged/pages/vibro_nav/presentation/controllers/vibro_nav_controller.dart';
|
2026-10-04 22:24:31 +02:00
|
|
|
import 'package:terepi_seged/pages/vibro_nav/presentation/views/vibro_nav_routes_view.dart';
|
2026-07-15 04:21:30 +02:00
|
|
|
import 'package:terepi_seged/services/gnss/gnss_connection.dart';
|
|
|
|
|
import 'package:terepi_seged/services/gnss/gnss_device_service.dart';
|
2026-07-13 07:58:51 +02:00
|
|
|
import 'package:terepi_seged/services/gnss/gnss_service.dart';
|
|
|
|
|
import 'package:terepi_seged/services/vechicle_identity_service.dart';
|
2026-10-04 20:18:08 +02:00
|
|
|
import 'package:terepi_seged/widgets/gnss_device_picker_dialog.dart';
|
2026-07-15 03:13:52 +02:00
|
|
|
import 'package:terepi_seged/widgets/map/animated_map_follow.dart';
|
2026-07-13 07:58:51 +02:00
|
|
|
|
2026-07-15 03:13:52 +02:00
|
|
|
class VibroNavView extends GetView<VibroNavController> {
|
2026-07-13 07:58:51 +02:00
|
|
|
const VibroNavView({super.key});
|
|
|
|
|
|
|
|
|
|
@override
|
|
|
|
|
Widget build(BuildContext context) {
|
|
|
|
|
return Scaffold(
|
2026-10-04 20:18:08 +02:00
|
|
|
// Tömör AppBar, NEM lóg a térkép fölé félig átlátszóan — az előző
|
|
|
|
|
// verzióban ez tette rosszul láthatóvá az eszköz-választó gombot.
|
|
|
|
|
appBar: AppBar(
|
|
|
|
|
// title: const Text('Navigáció'),
|
|
|
|
|
title: Obx(() => Text(
|
|
|
|
|
'N(${controller.sourcePoints.length}/${controller.visibleGeoSourcePoints.length})',
|
|
|
|
|
style: const TextStyle(fontSize: 11),
|
|
|
|
|
)),
|
|
|
|
|
actions: [
|
2026-10-04 22:24:31 +02:00
|
|
|
IconButton(
|
|
|
|
|
icon: const Icon(Icons.alt_route),
|
|
|
|
|
tooltip: 'Útvonalak kezelése',
|
|
|
|
|
onPressed: VibroNavRoutesView.open,
|
|
|
|
|
),
|
2026-10-04 20:18:08 +02:00
|
|
|
Obx(() => IconButton(
|
|
|
|
|
icon: Icon(controller.showReservePoints.value
|
|
|
|
|
? Icons.circle_outlined
|
|
|
|
|
: Icons.circle),
|
|
|
|
|
tooltip: controller.showReservePoints.value
|
|
|
|
|
? 'Tartalék pontok elrejtése'
|
|
|
|
|
: 'Tartalék pontok megjelenítése',
|
|
|
|
|
onPressed: controller.toggleReservePoints,
|
|
|
|
|
)),
|
|
|
|
|
Obx(() => IconButton(
|
|
|
|
|
icon: Icon(controller.trackUpEnabled.value
|
|
|
|
|
? Icons.explore
|
|
|
|
|
: Icons.explore_off_outlined),
|
|
|
|
|
tooltip: controller.trackUpEnabled.value
|
|
|
|
|
? 'Forgó térkép (track-up) — kikapcsolás'
|
|
|
|
|
: 'Észak-fent nézet — track-up bekapcsolása',
|
|
|
|
|
onPressed: controller.toggleTrackUp,
|
|
|
|
|
)),
|
|
|
|
|
IconButton(
|
|
|
|
|
icon: const Icon(Icons.bluetooth_connected),
|
|
|
|
|
tooltip: 'Gyors újracsatlakozás az utolsó GNSS-vevőhöz',
|
|
|
|
|
onPressed: _quickReconnect,
|
|
|
|
|
),
|
|
|
|
|
IconButton(
|
|
|
|
|
icon: const Icon(Icons.settings_input_antenna),
|
|
|
|
|
tooltip: 'GNSS-eszköz kiválasztása',
|
|
|
|
|
onPressed: () => GnssDevicePickerDialog.show(),
|
2026-07-13 07:58:51 +02:00
|
|
|
),
|
|
|
|
|
],
|
|
|
|
|
),
|
2026-10-04 20:18:08 +02:00
|
|
|
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: [
|
|
|
|
|
const _NavMap(),
|
|
|
|
|
Obx(() {
|
|
|
|
|
final gnss = GnssService.to;
|
|
|
|
|
final target =
|
|
|
|
|
controller.followMode.value && controller.hasPosition.value
|
|
|
|
|
? LatLng(gnss.latitude.value, gnss.longitude.value)
|
|
|
|
|
: null;
|
|
|
|
|
final heading = controller.trackUpEnabled.value
|
|
|
|
|
? controller.travelHeading.value
|
|
|
|
|
: null;
|
|
|
|
|
return AnimatedMapFollow(
|
|
|
|
|
mapController: controller.mapController,
|
|
|
|
|
target: target,
|
|
|
|
|
heading: heading,
|
|
|
|
|
);
|
|
|
|
|
}),
|
|
|
|
|
// EGYETLEN sor, egymás mellett — nincs két, egymást átfedő
|
|
|
|
|
// Positioned widget, tehát fizikailag nem takarhatják egymást.
|
|
|
|
|
Positioned(
|
|
|
|
|
left: 8,
|
|
|
|
|
right: 8,
|
|
|
|
|
top: 8,
|
|
|
|
|
child: Row(
|
|
|
|
|
children: [
|
|
|
|
|
const Expanded(child: _VehicleBadge()),
|
|
|
|
|
const SizedBox(width: 8),
|
|
|
|
|
const _GnssStatusBadge(),
|
|
|
|
|
const SizedBox(width: 8),
|
|
|
|
|
const _CompassWidget(),
|
|
|
|
|
],
|
|
|
|
|
),
|
|
|
|
|
),
|
|
|
|
|
Positioned(
|
|
|
|
|
left: 12,
|
|
|
|
|
right: 12,
|
|
|
|
|
bottom: 16,
|
|
|
|
|
child: const _PositionHud(),
|
|
|
|
|
),
|
|
|
|
|
Obx(() => controller.followMode.value
|
|
|
|
|
? const SizedBox.shrink()
|
|
|
|
|
: Positioned(
|
|
|
|
|
right: 16,
|
|
|
|
|
bottom: 170,
|
|
|
|
|
child: FloatingActionButton(
|
|
|
|
|
heroTag: 'recenter',
|
|
|
|
|
onPressed: controller.resumeFollowing,
|
|
|
|
|
child: const Icon(Icons.my_location),
|
|
|
|
|
),
|
|
|
|
|
)),
|
|
|
|
|
],
|
|
|
|
|
);
|
|
|
|
|
}),
|
|
|
|
|
);
|
2026-07-13 07:58:51 +02:00
|
|
|
}
|
|
|
|
|
|
2026-10-04 20:18:08 +02:00
|
|
|
void _quickReconnect() {
|
2026-07-15 04:21:30 +02:00
|
|
|
final device = GnssDeviceService.to.selectedDevice.value;
|
|
|
|
|
if (device == null) {
|
2026-10-04 20:18:08 +02:00
|
|
|
Get.snackbar('Nincs korábbi GNSS-eszköz',
|
|
|
|
|
'Előbb válassz ki egyet a jobb felső ikonnal.',
|
|
|
|
|
snackPosition: SnackPosition.BOTTOM);
|
2026-07-15 04:21:30 +02:00
|
|
|
return;
|
|
|
|
|
}
|
2026-10-04 20:18:08 +02:00
|
|
|
Get.snackbar('Újracsatlakozás', '„${device.name}" — folyamatban…',
|
|
|
|
|
snackPosition: SnackPosition.BOTTOM,
|
|
|
|
|
duration: const Duration(seconds: 2));
|
|
|
|
|
GnssService.to.reconnect();
|
2026-07-13 07:58:51 +02:00
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
2026-10-04 20:18:08 +02:00
|
|
|
Color _qualityColor(int quality) =>
|
|
|
|
|
quality >= 4 ? Colors.green : (quality > 0 ? Colors.orange : Colors.grey);
|
2026-07-13 07:58:51 +02:00
|
|
|
|
2026-10-04 20:18:08 +02:00
|
|
|
/// A térkép ITT, önálló widgetként él: a build-je Obx-en kívül fut, így
|
|
|
|
|
/// GNSS-frissítéskor a FlutterMap és a TileLayer NEM épül újra. Csak az
|
|
|
|
|
/// alábbi, saját Obx-ű rétegek frissülnek.
|
|
|
|
|
class _NavMap extends StatelessWidget {
|
|
|
|
|
const _NavMap();
|
2026-07-13 07:58:51 +02:00
|
|
|
|
|
|
|
|
@override
|
|
|
|
|
Widget build(BuildContext context) {
|
2026-10-04 20:18:08 +02:00
|
|
|
final controller = VibroNavController.to;
|
|
|
|
|
final gnss = GnssService.to;
|
|
|
|
|
// Csak a KEZDŐ középpont — a követést az AnimatedMapFollow végzi.
|
|
|
|
|
final center = controller.hasPosition.value
|
|
|
|
|
? LatLng(gnss.latitude.value, gnss.longitude.value)
|
|
|
|
|
: const LatLng(47.5, 19.05);
|
2026-07-13 07:58:51 +02:00
|
|
|
|
2026-10-04 20:18:08 +02:00
|
|
|
return FlutterMap(
|
|
|
|
|
mapController: controller.mapController,
|
|
|
|
|
options: MapOptions(
|
|
|
|
|
initialCenter: center,
|
|
|
|
|
initialZoom: 17,
|
|
|
|
|
maxZoom: 22,
|
|
|
|
|
onPositionChanged: (position, hasGesture) {
|
|
|
|
|
if (hasGesture && controller.followMode.value) {
|
|
|
|
|
// Build közben ne írjunk Rx-et — képkocka végére ütemezzük.
|
|
|
|
|
WidgetsBinding.instance.addPostFrameCallback((_) {
|
|
|
|
|
controller.followMode.value = false;
|
|
|
|
|
});
|
|
|
|
|
}
|
|
|
|
|
},
|
|
|
|
|
),
|
|
|
|
|
children: [
|
|
|
|
|
TileLayer(
|
|
|
|
|
urlTemplate: 'http://{s}.google.com/vt/lyrs=s,h&x={x}&y={y}&z={z}',
|
|
|
|
|
subdomains: const ['mt0', 'mt1', 'mt2', 'mt3'],
|
|
|
|
|
maxNativeZoom: 18),
|
|
|
|
|
const _SourcePointsLayer(),
|
2026-10-04 22:24:31 +02:00
|
|
|
const _RoutesLayer(),
|
2026-10-04 20:18:08 +02:00
|
|
|
Obx(() => controller.trail.length > 1
|
|
|
|
|
? PolylineLayer(polylines: [
|
|
|
|
|
Polyline(
|
|
|
|
|
points: controller.trail.toList(),
|
|
|
|
|
color: Colors.blueAccent.withOpacity(0.75),
|
|
|
|
|
strokeWidth: 4,
|
|
|
|
|
),
|
|
|
|
|
])
|
|
|
|
|
: const SizedBox.shrink()),
|
|
|
|
|
Obx(() {
|
|
|
|
|
if (!controller.hasPosition.value) return const SizedBox.shrink();
|
|
|
|
|
final color = _qualityColor(gnss.gpsQuality.value);
|
|
|
|
|
return CircleLayer(circles: [
|
|
|
|
|
CircleMarker(
|
|
|
|
|
point: LatLng(gnss.latitude.value, gnss.longitude.value),
|
|
|
|
|
radius:
|
|
|
|
|
math.max(gnss.latitudeError.value, gnss.longitudeError.value),
|
|
|
|
|
useRadiusInMeter: true,
|
|
|
|
|
color: color.withOpacity(0.12),
|
|
|
|
|
borderColor: color.withOpacity(0.4),
|
|
|
|
|
borderStrokeWidth: 1,
|
|
|
|
|
),
|
|
|
|
|
]);
|
|
|
|
|
}),
|
|
|
|
|
Obx(() {
|
|
|
|
|
if (!controller.hasPosition.value) return const SizedBox.shrink();
|
|
|
|
|
final color = _qualityColor(gnss.gpsQuality.value);
|
|
|
|
|
return MarkerLayer(markers: [
|
|
|
|
|
Marker(
|
|
|
|
|
point: LatLng(gnss.latitude.value, gnss.longitude.value),
|
|
|
|
|
width: 60,
|
|
|
|
|
height: 60,
|
|
|
|
|
child: _PulsingPositionMarker(color: color),
|
|
|
|
|
),
|
|
|
|
|
]);
|
|
|
|
|
}),
|
|
|
|
|
],
|
|
|
|
|
);
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
/// A forráspontok rétege — csak a `geoSourcePoints` és a
|
|
|
|
|
/// `showReservePoints` változására épül újra, a GNSS-frissítésekre nem.
|
|
|
|
|
/// A koordináták a controllerben már átváltva vannak.
|
|
|
|
|
class _SourcePointsLayer extends StatelessWidget {
|
|
|
|
|
const _SourcePointsLayer();
|
|
|
|
|
|
|
|
|
|
@override
|
|
|
|
|
Widget build(BuildContext context) {
|
|
|
|
|
final c = VibroNavController.to;
|
|
|
|
|
return Obx(() {
|
|
|
|
|
final list = c.visibleGeoSourcePoints;
|
|
|
|
|
if (list.isEmpty) return const SizedBox.shrink();
|
|
|
|
|
return MarkerLayer(
|
|
|
|
|
markers: [
|
|
|
|
|
for (final g in list)
|
|
|
|
|
Marker(
|
|
|
|
|
point: g.latLng,
|
|
|
|
|
width: 74,
|
|
|
|
|
height: 26,
|
|
|
|
|
alignment: Alignment.centerLeft,
|
|
|
|
|
child: _SourcePointMarker(
|
|
|
|
|
isShot: g.point.isShotLocation,
|
|
|
|
|
label: _fmtPointNumber(g.point.pointNumber),
|
|
|
|
|
),
|
2026-07-13 07:58:51 +02:00
|
|
|
),
|
|
|
|
|
],
|
2026-10-04 20:18:08 +02:00
|
|
|
);
|
|
|
|
|
});
|
2026-07-13 07:58:51 +02:00
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
2026-10-04 20:18:08 +02:00
|
|
|
class _PulsingPositionMarker extends StatefulWidget {
|
|
|
|
|
final Color color;
|
|
|
|
|
const _PulsingPositionMarker({required this.color});
|
|
|
|
|
|
|
|
|
|
@override
|
|
|
|
|
State<_PulsingPositionMarker> createState() => _PulsingPositionMarkerState();
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
class _PulsingPositionMarkerState extends State<_PulsingPositionMarker>
|
|
|
|
|
with SingleTickerProviderStateMixin {
|
|
|
|
|
late final AnimationController _ctrl;
|
|
|
|
|
|
|
|
|
|
@override
|
|
|
|
|
void initState() {
|
|
|
|
|
super.initState();
|
|
|
|
|
_ctrl =
|
|
|
|
|
AnimationController(vsync: this, duration: const Duration(seconds: 2))
|
|
|
|
|
..repeat();
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
@override
|
|
|
|
|
void dispose() {
|
|
|
|
|
_ctrl.dispose();
|
|
|
|
|
super.dispose();
|
|
|
|
|
}
|
2026-07-13 07:58:51 +02:00
|
|
|
|
|
|
|
|
@override
|
|
|
|
|
Widget build(BuildContext context) {
|
2026-10-04 20:18:08 +02:00
|
|
|
return AnimatedBuilder(
|
|
|
|
|
animation: _ctrl,
|
|
|
|
|
builder: (context, _) {
|
|
|
|
|
final t = _ctrl.value;
|
|
|
|
|
return Stack(
|
|
|
|
|
alignment: Alignment.center,
|
|
|
|
|
children: [
|
|
|
|
|
Opacity(
|
|
|
|
|
opacity: (1 - t).clamp(0.0, 1.0),
|
|
|
|
|
child: Container(
|
|
|
|
|
width: 18 + t * 34,
|
|
|
|
|
height: 18 + t * 34,
|
|
|
|
|
decoration: BoxDecoration(
|
|
|
|
|
shape: BoxShape.circle,
|
|
|
|
|
color: widget.color.withOpacity(0.35),
|
|
|
|
|
),
|
|
|
|
|
),
|
|
|
|
|
),
|
|
|
|
|
Container(
|
|
|
|
|
width: 20,
|
|
|
|
|
height: 20,
|
|
|
|
|
decoration: BoxDecoration(
|
|
|
|
|
color: widget.color,
|
|
|
|
|
shape: BoxShape.circle,
|
|
|
|
|
border: Border.all(color: Colors.white, width: 2.5),
|
|
|
|
|
boxShadow: const [
|
|
|
|
|
BoxShadow(color: Colors.black38, blurRadius: 4),
|
|
|
|
|
],
|
|
|
|
|
),
|
|
|
|
|
),
|
|
|
|
|
],
|
|
|
|
|
);
|
|
|
|
|
},
|
2026-07-13 07:58:51 +02:00
|
|
|
);
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
2026-10-04 20:18:08 +02:00
|
|
|
class _CompassWidget extends StatelessWidget {
|
|
|
|
|
const _CompassWidget();
|
2026-07-13 07:58:51 +02:00
|
|
|
|
|
|
|
|
@override
|
|
|
|
|
Widget build(BuildContext context) {
|
2026-10-04 20:18:08 +02:00
|
|
|
final controller = VibroNavController.to;
|
2026-07-13 07:58:51 +02:00
|
|
|
return Obx(() {
|
2026-10-04 20:18:08 +02:00
|
|
|
final heading = controller.travelHeading.value ?? 0;
|
|
|
|
|
final rotation = controller.trackUpEnabled.value ? -heading : heading;
|
|
|
|
|
return Container(
|
|
|
|
|
width: 40,
|
|
|
|
|
height: 40,
|
|
|
|
|
decoration: BoxDecoration(
|
|
|
|
|
color: Colors.white,
|
|
|
|
|
shape: BoxShape.circle,
|
|
|
|
|
boxShadow: const [BoxShadow(color: Colors.black26, blurRadius: 3)],
|
|
|
|
|
),
|
|
|
|
|
child: Transform.rotate(
|
|
|
|
|
angle: rotation * math.pi / 180,
|
|
|
|
|
child:
|
|
|
|
|
const Icon(Icons.navigation, color: Colors.redAccent, size: 22),
|
2026-07-13 07:58:51 +02:00
|
|
|
),
|
|
|
|
|
);
|
|
|
|
|
});
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
2026-10-04 20:18:08 +02:00
|
|
|
class _VehicleBadge extends StatelessWidget {
|
|
|
|
|
const _VehicleBadge();
|
2026-07-13 07:58:51 +02:00
|
|
|
|
|
|
|
|
@override
|
|
|
|
|
Widget build(BuildContext context) {
|
2026-10-04 20:18:08 +02:00
|
|
|
return Obx(() {
|
|
|
|
|
final vehicle = VehicleIdentityService.to.selectedVehicle.value;
|
|
|
|
|
return Material(
|
|
|
|
|
elevation: 2,
|
|
|
|
|
borderRadius: BorderRadius.circular(8),
|
|
|
|
|
color: Colors.white,
|
|
|
|
|
child: InkWell(
|
|
|
|
|
borderRadius: BorderRadius.circular(8),
|
|
|
|
|
onTap: () => _pickVehicle(context),
|
|
|
|
|
child: Padding(
|
|
|
|
|
padding: const EdgeInsets.symmetric(horizontal: 10, vertical: 8),
|
|
|
|
|
child: Row(
|
|
|
|
|
mainAxisSize: MainAxisSize.min,
|
|
|
|
|
children: [
|
|
|
|
|
const Icon(Icons.local_shipping_outlined, size: 18),
|
|
|
|
|
const SizedBox(width: 6),
|
|
|
|
|
Flexible(
|
|
|
|
|
child: Text(vehicle ?? 'Jármű kiválasztása',
|
|
|
|
|
overflow: TextOverflow.ellipsis,
|
|
|
|
|
style: TextStyle(
|
|
|
|
|
fontWeight: FontWeight.w600,
|
|
|
|
|
color: vehicle == null ? Colors.orange : null)),
|
|
|
|
|
),
|
|
|
|
|
],
|
|
|
|
|
),
|
|
|
|
|
),
|
2026-07-13 07:58:51 +02:00
|
|
|
),
|
2026-10-04 20:18:08 +02:00
|
|
|
);
|
|
|
|
|
});
|
2026-07-13 07:58:51 +02:00
|
|
|
}
|
|
|
|
|
|
2026-10-04 20:18:08 +02:00
|
|
|
void _pickVehicle(BuildContext context) {
|
|
|
|
|
Get.bottomSheet(
|
|
|
|
|
Material(
|
|
|
|
|
color: Colors.white,
|
|
|
|
|
child: Column(
|
|
|
|
|
mainAxisSize: MainAxisSize.min,
|
|
|
|
|
children: [
|
|
|
|
|
for (final v in VehicleIdentityService.availableVehicles)
|
|
|
|
|
ListTile(
|
|
|
|
|
title: Text(v),
|
|
|
|
|
onTap: () {
|
|
|
|
|
VehicleIdentityService.to.setVehicle(v);
|
|
|
|
|
Get.back();
|
|
|
|
|
},
|
|
|
|
|
),
|
|
|
|
|
],
|
|
|
|
|
),
|
2026-07-13 07:58:51 +02:00
|
|
|
),
|
|
|
|
|
);
|
|
|
|
|
}
|
|
|
|
|
}
|
2026-07-15 04:21:30 +02:00
|
|
|
|
|
|
|
|
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(
|
2026-10-04 20:18:08 +02:00
|
|
|
padding: const EdgeInsets.symmetric(horizontal: 10, vertical: 8),
|
2026-07-15 04:21:30 +02:00
|
|
|
decoration: BoxDecoration(
|
2026-10-04 20:18:08 +02:00
|
|
|
color: Colors.white,
|
|
|
|
|
borderRadius: BorderRadius.circular(8),
|
|
|
|
|
border: Border.all(color: color, width: 1.5),
|
|
|
|
|
boxShadow: const [BoxShadow(color: Colors.black12, blurRadius: 3)],
|
2026-07-15 04:21:30 +02:00
|
|
|
),
|
|
|
|
|
child: Row(
|
|
|
|
|
mainAxisSize: MainAxisSize.min,
|
|
|
|
|
children: [
|
2026-10-04 20:18:08 +02:00
|
|
|
Icon(icon, size: 16, color: color),
|
2026-07-15 04:21:30 +02:00
|
|
|
const SizedBox(width: 4),
|
|
|
|
|
Text(label,
|
|
|
|
|
style: TextStyle(
|
2026-10-04 20:18:08 +02:00
|
|
|
fontSize: 12, fontWeight: FontWeight.w700, color: color)),
|
2026-07-15 04:21:30 +02:00
|
|
|
],
|
|
|
|
|
),
|
|
|
|
|
);
|
|
|
|
|
});
|
|
|
|
|
}
|
|
|
|
|
}
|
2026-10-04 20:18:08 +02:00
|
|
|
|
|
|
|
|
class _PositionHud extends StatelessWidget {
|
|
|
|
|
const _PositionHud();
|
|
|
|
|
|
|
|
|
|
@override
|
|
|
|
|
Widget build(BuildContext context) {
|
|
|
|
|
final controller = VibroNavController.to;
|
|
|
|
|
return Obx(() {
|
|
|
|
|
final gnss = GnssService.to;
|
|
|
|
|
if (!controller.hasPosition.value) return const SizedBox.shrink();
|
|
|
|
|
|
|
|
|
|
return Container(
|
|
|
|
|
padding: const EdgeInsets.all(14),
|
|
|
|
|
decoration: BoxDecoration(
|
|
|
|
|
color: Colors.black.withOpacity(0.72),
|
|
|
|
|
borderRadius: BorderRadius.circular(16),
|
|
|
|
|
boxShadow: const [BoxShadow(color: Colors.black45, blurRadius: 10)],
|
|
|
|
|
),
|
|
|
|
|
child: Column(
|
|
|
|
|
crossAxisAlignment: CrossAxisAlignment.start,
|
|
|
|
|
mainAxisSize: MainAxisSize.min,
|
|
|
|
|
children: [
|
|
|
|
|
Row(
|
|
|
|
|
crossAxisAlignment: CrossAxisAlignment.end,
|
|
|
|
|
children: [
|
|
|
|
|
Text(
|
|
|
|
|
controller.speedKmh.value.toStringAsFixed(0),
|
|
|
|
|
style: const TextStyle(
|
|
|
|
|
fontSize: 44,
|
|
|
|
|
fontWeight: FontWeight.bold,
|
|
|
|
|
color: Colors.white,
|
|
|
|
|
height: 1,
|
|
|
|
|
fontFeatures: [FontFeature.tabularFigures()]),
|
|
|
|
|
),
|
|
|
|
|
const Padding(
|
|
|
|
|
padding: EdgeInsets.only(left: 4, bottom: 6),
|
|
|
|
|
child: Text('km/h',
|
|
|
|
|
style: TextStyle(color: Colors.white70, fontSize: 13)),
|
|
|
|
|
),
|
|
|
|
|
const Spacer(),
|
|
|
|
|
_UploadPulseDot(
|
|
|
|
|
active: controller.externalUploadPulse.value,
|
|
|
|
|
label: 'Külső'),
|
|
|
|
|
const SizedBox(width: 10),
|
|
|
|
|
_UploadPulseDot(
|
|
|
|
|
active: controller.internalUploadPulse.value,
|
|
|
|
|
label: 'Belső'),
|
|
|
|
|
],
|
|
|
|
|
),
|
|
|
|
|
const SizedBox(height: 6),
|
|
|
|
|
Text(
|
|
|
|
|
'${gnss.latitude.value.toStringAsFixed(6)}, '
|
|
|
|
|
'${gnss.longitude.value.toStringAsFixed(6)}',
|
|
|
|
|
style: const TextStyle(
|
|
|
|
|
fontSize: 13,
|
|
|
|
|
color: Colors.white70,
|
|
|
|
|
fontFeatures: [FontFeature.tabularFigures()]),
|
|
|
|
|
),
|
|
|
|
|
const SizedBox(height: 6),
|
|
|
|
|
Row(
|
|
|
|
|
children: [
|
|
|
|
|
_hudChip(Icons.satellite_alt, '${gnss.satelliteCount.value}'),
|
|
|
|
|
const SizedBox(width: 12),
|
|
|
|
|
_hudChip(Icons.height,
|
|
|
|
|
'${gnss.altitude.value.toStringAsFixed(1)} m'),
|
|
|
|
|
const SizedBox(width: 12),
|
|
|
|
|
_hudChip(Icons.adjust,
|
|
|
|
|
'±${math.max(gnss.latitudeError.value, gnss.longitudeError.value).toStringAsFixed(2)} m'),
|
|
|
|
|
],
|
|
|
|
|
),
|
|
|
|
|
],
|
|
|
|
|
),
|
|
|
|
|
);
|
|
|
|
|
});
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
Widget _hudChip(IconData icon, String value) => Row(
|
|
|
|
|
mainAxisSize: MainAxisSize.min,
|
|
|
|
|
children: [
|
|
|
|
|
Icon(icon, size: 14, color: Colors.white54),
|
|
|
|
|
const SizedBox(width: 3),
|
|
|
|
|
Text(value,
|
|
|
|
|
style: const TextStyle(fontSize: 12, color: Colors.white70)),
|
|
|
|
|
],
|
|
|
|
|
);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
class _UploadPulseDot extends StatelessWidget {
|
|
|
|
|
final bool active;
|
|
|
|
|
final String label;
|
|
|
|
|
const _UploadPulseDot({required this.active, required this.label});
|
|
|
|
|
|
|
|
|
|
@override
|
|
|
|
|
Widget build(BuildContext context) {
|
|
|
|
|
return Column(
|
|
|
|
|
children: [
|
|
|
|
|
AnimatedContainer(
|
|
|
|
|
duration: const Duration(milliseconds: 250),
|
|
|
|
|
width: 10,
|
|
|
|
|
height: 10,
|
|
|
|
|
decoration: BoxDecoration(
|
|
|
|
|
shape: BoxShape.circle,
|
|
|
|
|
color: active ? Colors.greenAccent : Colors.white24,
|
|
|
|
|
boxShadow: active
|
|
|
|
|
? [
|
|
|
|
|
BoxShadow(
|
|
|
|
|
color: Colors.greenAccent.withOpacity(0.7),
|
|
|
|
|
blurRadius: 6)
|
|
|
|
|
]
|
|
|
|
|
: null,
|
|
|
|
|
),
|
|
|
|
|
),
|
|
|
|
|
const SizedBox(height: 2),
|
|
|
|
|
Text(label, style: const TextStyle(fontSize: 9, color: Colors.white54)),
|
|
|
|
|
],
|
|
|
|
|
);
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
String _fmtPointNumber(double v) =>
|
|
|
|
|
v == v.roundToDouble() ? v.toInt().toString() : v.toStringAsFixed(1);
|
|
|
|
|
|
|
|
|
|
class _SourcePointMarker extends StatelessWidget {
|
|
|
|
|
final bool isShot;
|
|
|
|
|
final String label;
|
|
|
|
|
const _SourcePointMarker({required this.isShot, required this.label});
|
|
|
|
|
|
|
|
|
|
@override
|
|
|
|
|
Widget build(BuildContext context) {
|
|
|
|
|
// Jól látható, telített színek napfényben — a jelgerjesztés helye
|
|
|
|
|
// élénk piros-narancs, a tartalék pont cián; mindkettő fekete
|
|
|
|
|
// körvonallal, hogy világos térkép-háttéren se vesszen el.
|
|
|
|
|
final dotColor = isShot ? const Color(0xFFFF1744) : const Color(0xFF00E5FF);
|
|
|
|
|
|
|
|
|
|
return Row(
|
|
|
|
|
mainAxisSize: MainAxisSize.min,
|
|
|
|
|
crossAxisAlignment: CrossAxisAlignment.center,
|
|
|
|
|
children: [
|
|
|
|
|
Container(
|
|
|
|
|
width: isShot ? 11 : 8,
|
|
|
|
|
height: isShot ? 11 : 8,
|
|
|
|
|
decoration: BoxDecoration(
|
|
|
|
|
shape: BoxShape.circle,
|
|
|
|
|
color: dotColor,
|
|
|
|
|
border: Border.all(color: Colors.black87, width: 1),
|
|
|
|
|
),
|
|
|
|
|
),
|
|
|
|
|
const SizedBox(width: 3),
|
|
|
|
|
_OutlinedLabel(text: label, color: dotColor),
|
|
|
|
|
],
|
|
|
|
|
);
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
/// Felirat HÁTTÉR NÉLKÜL, de fehér körvonallal a betűk körül — így
|
|
|
|
|
/// bármilyen térkép-háttéren olvasható marad, nincs szükség kitöltött
|
|
|
|
|
/// buborékra.
|
|
|
|
|
class _OutlinedLabel extends StatelessWidget {
|
|
|
|
|
final String text;
|
|
|
|
|
final Color color;
|
|
|
|
|
const _OutlinedLabel({required this.text, required this.color});
|
|
|
|
|
|
|
|
|
|
@override
|
|
|
|
|
Widget build(BuildContext context) {
|
|
|
|
|
return Stack(
|
|
|
|
|
children: [
|
|
|
|
|
Text(
|
|
|
|
|
text,
|
|
|
|
|
style: TextStyle(
|
|
|
|
|
fontSize: 14,
|
|
|
|
|
fontWeight: FontWeight.w800,
|
|
|
|
|
foreground: Paint()
|
|
|
|
|
..style = PaintingStyle.stroke
|
|
|
|
|
..strokeWidth = 3
|
|
|
|
|
..color = Colors.white,
|
|
|
|
|
),
|
|
|
|
|
),
|
|
|
|
|
Text(
|
|
|
|
|
text,
|
|
|
|
|
style: TextStyle(
|
|
|
|
|
fontSize: 14,
|
|
|
|
|
fontWeight: FontWeight.w800,
|
|
|
|
|
color: color,
|
|
|
|
|
),
|
|
|
|
|
),
|
|
|
|
|
],
|
|
|
|
|
);
|
|
|
|
|
}
|
|
|
|
|
}
|
2026-10-04 22:24:31 +02:00
|
|
|
|
|
|
|
|
/// A mai napra érvényes útvonalak (kerülők). Saját Obx-ben él, ezért
|
|
|
|
|
/// GNSS-frissítésre nem rajzolódik újra.
|
|
|
|
|
class _RoutesLayer extends StatelessWidget {
|
|
|
|
|
const _RoutesLayer();
|
|
|
|
|
|
|
|
|
|
@override
|
|
|
|
|
Widget build(BuildContext context) {
|
|
|
|
|
final c = VibroNavController.to;
|
|
|
|
|
return Obx(() {
|
|
|
|
|
final list = c.visibleRoutes;
|
|
|
|
|
if (list.isEmpty) return const SizedBox.shrink();
|
|
|
|
|
return PolylineLayer(polylines: [
|
|
|
|
|
for (final r in list)
|
|
|
|
|
for (final line in r.lines)
|
|
|
|
|
Polyline(
|
|
|
|
|
points: line,
|
|
|
|
|
strokeWidth: 5,
|
|
|
|
|
color:
|
|
|
|
|
VibroNavRoute.colorFor(r.routeKind, r.color).withOpacity(0.9),
|
|
|
|
|
borderStrokeWidth: 2,
|
|
|
|
|
borderColor: Colors.white,
|
|
|
|
|
),
|
|
|
|
|
]);
|
|
|
|
|
});
|
|
|
|
|
}
|
|
|
|
|
}
|