Navigációs képernyőn forráspontok megjelenítése
Gitea Actions Demo / Explore-Gitea-Actions (push) Successful in 9s
Gitea Actions Demo / Explore-Gitea-Actions (push) Successful in 9s
This commit is contained in:
@@ -38,7 +38,7 @@ android {
|
|||||||
// You can update the following values to match your application needs.
|
// You can update the following values to match your application needs.
|
||||||
// For more information, see: https://docs.flutter.dev/deployment/android#reviewing-the-build-configuration.
|
// For more information, see: https://docs.flutter.dev/deployment/android#reviewing-the-build-configuration.
|
||||||
minSdkVersion 26
|
minSdkVersion 26
|
||||||
targetSdkVersion 35
|
targetSdkVersion 36
|
||||||
versionCode flutter.versionCode
|
versionCode flutter.versionCode
|
||||||
versionName flutter.versionName
|
versionName flutter.versionName
|
||||||
|
|
||||||
|
|||||||
@@ -21,6 +21,7 @@ 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/layer_import_service.dart';
|
||||||
import 'package:terepi_seged/services/layer_sync_service.dart';
|
import 'package:terepi_seged/services/layer_sync_service.dart';
|
||||||
|
import 'package:terepi_seged/services/measure_source_point_service.dart';
|
||||||
import 'package:terepi_seged/services/measured_data_service.dart';
|
import 'package:terepi_seged/services/measured_data_service.dart';
|
||||||
import 'package:terepi_seged/services/note_audio_service.dart';
|
import 'package:terepi_seged/services/note_audio_service.dart';
|
||||||
import 'package:terepi_seged/services/note_photo_service.dart';
|
import 'package:terepi_seged/services/note_photo_service.dart';
|
||||||
@@ -119,6 +120,7 @@ Future<void> main() async {
|
|||||||
Get.put(FieldPropertyService());
|
Get.put(FieldPropertyService());
|
||||||
Get.put(ParcelGeometryService());
|
Get.put(ParcelGeometryService());
|
||||||
Get.put(MeasuredDataService());
|
Get.put(MeasuredDataService());
|
||||||
|
Get.put(MeasureSourcePointService());
|
||||||
|
|
||||||
runApp(const MyApp());
|
runApp(const MyApp());
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -0,0 +1,70 @@
|
|||||||
|
/// Egy jelgerjesztési/tartalék forráspont a `terepi_seged_measure_source_points`
|
||||||
|
/// táblából. Tisztán online — nincs helyi tükör.
|
||||||
|
class MeasureSourcePoint {
|
||||||
|
final String id;
|
||||||
|
final String projectId;
|
||||||
|
final double lineNumber;
|
||||||
|
final double pointNumber;
|
||||||
|
final int sequenceIndex;
|
||||||
|
final double eovX;
|
||||||
|
final double eovY;
|
||||||
|
final double? elevation;
|
||||||
|
final double? linkedReceiverLineNumber;
|
||||||
|
|
||||||
|
/// 0 = tartalék pont, 1 = jelgerjesztés helye.
|
||||||
|
final int planState;
|
||||||
|
final DateTime? plannedAcquisitionDate;
|
||||||
|
final int? fieldSequence;
|
||||||
|
final String? sourceType;
|
||||||
|
final bool isActive;
|
||||||
|
|
||||||
|
const MeasureSourcePoint({
|
||||||
|
required this.id,
|
||||||
|
required this.projectId,
|
||||||
|
required this.lineNumber,
|
||||||
|
required this.pointNumber,
|
||||||
|
required this.sequenceIndex,
|
||||||
|
required this.eovX,
|
||||||
|
required this.eovY,
|
||||||
|
this.elevation,
|
||||||
|
this.linkedReceiverLineNumber,
|
||||||
|
required this.planState,
|
||||||
|
this.plannedAcquisitionDate,
|
||||||
|
this.fieldSequence,
|
||||||
|
this.sourceType,
|
||||||
|
required this.isActive,
|
||||||
|
});
|
||||||
|
|
||||||
|
bool get isReserve => planState == 0;
|
||||||
|
bool get isShotLocation => planState == 1;
|
||||||
|
|
||||||
|
factory MeasureSourcePoint.fromMap(Map<String, dynamic> m) =>
|
||||||
|
MeasureSourcePoint(
|
||||||
|
id: m['id'] as String,
|
||||||
|
projectId: m['project_id'] as String,
|
||||||
|
lineNumber: (m['line_number'] as num).toDouble(),
|
||||||
|
pointNumber: (m['point_number'] as num).toDouble(),
|
||||||
|
sequenceIndex: m['sequence_index'] as int,
|
||||||
|
|
||||||
|
// FIGYELEM: a terepi_seged_measure_source_points táblában az
|
||||||
|
// eov_x/eov_y oszlopnevek FEL VANNAK CSERÉLVE a valódi magyar
|
||||||
|
// EOV-konvencióhoz képest (az "eov_x" oszlop tartalmazza a
|
||||||
|
// ténylegesen Y-nak/keletinek számító, ~640 000 körüli
|
||||||
|
// értéket, az "eov_y" a ténylegesen X-nek/északinak számítót,
|
||||||
|
// ~185 000 körülit) — itt kompenzáljuk, hogy a modell mezői
|
||||||
|
// (eovX/eovY) a kód többi részével KONZISZTENS, valódi EOV-
|
||||||
|
// jelentést hordozzanak.
|
||||||
|
eovX: (m['eov_y'] as num).toDouble(),
|
||||||
|
eovY: (m['eov_x'] as num).toDouble(),
|
||||||
|
elevation: (m['elevation'] as num?)?.toDouble(),
|
||||||
|
linkedReceiverLineNumber:
|
||||||
|
(m['linked_receiver_line_number'] as num?)?.toDouble(),
|
||||||
|
planState: m['plan_state'] as int,
|
||||||
|
plannedAcquisitionDate: m['planned_acquisition_date'] != null
|
||||||
|
? DateTime.parse(m['planned_acquisition_date'] as String)
|
||||||
|
: null,
|
||||||
|
fieldSequence: m['field_sequence'] as int?,
|
||||||
|
sourceType: m['source_type'] as String?,
|
||||||
|
isActive: m['is_active'] as bool,
|
||||||
|
);
|
||||||
|
}
|
||||||
@@ -1,119 +1,155 @@
|
|||||||
import 'dart:async';
|
import 'dart:async';
|
||||||
import 'dart:math' as math;
|
import 'dart:math' as math;
|
||||||
|
|
||||||
|
import 'package:flutter/foundation.dart';
|
||||||
import 'package:flutter_map/flutter_map.dart';
|
import 'package:flutter_map/flutter_map.dart';
|
||||||
|
import 'package:geolocator/geolocator.dart' as geo;
|
||||||
import 'package:get/get.dart';
|
import 'package:get/get.dart';
|
||||||
import 'package:latlong2/latlong.dart';
|
import 'package:latlong2/latlong.dart';
|
||||||
import 'package:terepi_seged/models/source_point.dart';
|
import 'package:supabase_flutter/supabase_flutter.dart';
|
||||||
import 'package:terepi_seged/models/vechicle_position_log.dart';
|
import 'package:terepi_seged/models/measure_source_point.dart';
|
||||||
import 'package:terepi_seged/services/app_database.dart';
|
import 'package:terepi_seged/services/app_logger.dart';
|
||||||
import 'package:terepi_seged/services/coord_converter_service.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_connection.dart';
|
||||||
import 'package:terepi_seged/services/gnss/gnss_service.dart';
|
import 'package:terepi_seged/services/gnss/gnss_service.dart';
|
||||||
|
import 'package:terepi_seged/services/gnss/phone_gps_connection.dart';
|
||||||
|
import 'package:terepi_seged/services/measure_source_point_service.dart';
|
||||||
import 'package:terepi_seged/services/project_service.dart';
|
import 'package:terepi_seged/services/project_service.dart';
|
||||||
import 'package:terepi_seged/services/vechicle_identity_service.dart';
|
import 'package:terepi_seged/services/vechicle_identity_service.dart';
|
||||||
|
|
||||||
/// Egyszerű navigációs panel a jelgerjesztő (vibrátor) járműhöz.
|
/// Forráspont + egyszer kiszámolt WGS84 pozíció. A térkép ebből rajzol,
|
||||||
|
/// így GNSS-frissítéskor nincs újraszámolás.
|
||||||
|
class GeoSourcePoint {
|
||||||
|
final MeasureSourcePoint point;
|
||||||
|
final LatLng latLng;
|
||||||
|
const GeoSourcePoint(this.point, this.latLng);
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Navigációs képernyő — a KÜLSŐ (Bluetooth/BLE RTK-vevő, vagy a telefon
|
||||||
|
/// GPS-e, amit a GnssService-eszközválasztóval választasz ki) a FŐ
|
||||||
|
/// pozíció. EMELLETT, attól teljesen FÜGGETLENÜL, mindig fut egy saját,
|
||||||
|
/// belső telefon-GPS kapcsolat is — akkor is naplózódik, ha épp külső
|
||||||
|
/// vevő az aktív forrás.
|
||||||
///
|
///
|
||||||
/// A pozíció a KITŰZÉSSEL AZONOS forrásból jön (GnssService — külső
|
/// Mindkét adatfolyam a KÖZÖS, PC-szoftverrel is megosztott
|
||||||
/// BT/BLE GNSS-egység a járműben), semmilyen új eszköz-integráció nem
|
/// `TerepiSeged_VibratorTracker` táblába ír, a `gpsType` mezővel
|
||||||
/// kell hozzá. A forráspontok tisztán referencia-réteg (nincs mutálható
|
/// megkülönböztetve (1 = külső/fő forrás, 2 = belső telefon-GPS).
|
||||||
/// "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 {
|
class VibroNavController extends GetxController {
|
||||||
AppDatabase get _db => AppDatabase.instance;
|
static VibroNavController get to => Get.find();
|
||||||
|
|
||||||
// ── Forráspontok ─────────────────────────────────────────────────
|
static const _gpsTypeExternal = 1;
|
||||||
final sourcePoints = <SourcePoint>[].obs;
|
static const _gpsTypeInternal = 2;
|
||||||
|
|
||||||
/// Egyezés-tűrés (m) a naplózott pozíció és a terv-forráspont között.
|
static const _trailMaxPoints = 300;
|
||||||
final tolerance = 15.0.obs;
|
static const _trailMinDistM = 3.0;
|
||||||
|
|
||||||
// ── Élő pozíció (a GnssService-ből, EOV-ra konvertálva) ───────────
|
final mapController = MapController();
|
||||||
|
final mapReady = false.obs;
|
||||||
final hasPosition = false.obs;
|
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>();
|
final travelHeading = Rxn<double>();
|
||||||
static const _headingFreezeSpeedKmh = 5.0;
|
final speedKmh = 0.0.obs;
|
||||||
|
|
||||||
|
/// Ha true, a térkép automatikusan követi a pozíciót. Amint a
|
||||||
|
/// felhasználó kézzel elhúzza/nagyítja a térképet, false-ra vált — a
|
||||||
|
/// nézet ilyenkor "ott marad", és egy gomb hozza vissza a követést.
|
||||||
|
final followMode = true.obs;
|
||||||
|
|
||||||
|
/// Track-up: a térkép a haladási irány szerint forogjon, ne mindig
|
||||||
|
/// észak legyen fent. Kikapcsolható, ha inkább a hagyományos,
|
||||||
|
/// észak-fent nézetet szeretnéd.
|
||||||
|
final trackUpEnabled = true.obs;
|
||||||
|
|
||||||
|
final trail = <LatLng>[].obs;
|
||||||
|
|
||||||
|
final sourcePoints = <MeasureSourcePoint>[].obs;
|
||||||
|
final geoSourcePoints = <GeoSourcePoint>[].obs;
|
||||||
|
final Map<String, LatLng?> _geoCache = {};
|
||||||
|
final isLoadingSourcePoints = false.obs;
|
||||||
|
|
||||||
|
/// Ha false, a tartalék (plan_state = 0) pontok nem jelennek meg a
|
||||||
|
/// térképen — csak a jelgerjesztés helyei (plan_state = 1) látszanak.
|
||||||
|
final showReservePoints = false.obs;
|
||||||
|
|
||||||
|
final lastExternalUploadAt = Rxn<DateTime>();
|
||||||
|
final lastInternalUploadAt = Rxn<DateTime>();
|
||||||
|
final externalUploadPulse = false.obs;
|
||||||
|
final internalUploadPulse = false.obs;
|
||||||
|
|
||||||
|
StreamSubscription? _gnssUpdateSub;
|
||||||
|
Timer? _externalUploadTimer;
|
||||||
|
|
||||||
|
final _internalGps = PhoneGpsConnection();
|
||||||
|
StreamSubscription<geo.Position>? _internalPositionSub;
|
||||||
|
StreamSubscription<SourcePointFetchResult>? _sourcePointsSub;
|
||||||
|
geo.Position? _lastInternalPosition;
|
||||||
|
Timer? _internalUploadTimer;
|
||||||
|
|
||||||
double? _histY, _histX;
|
double? _histY, _histX;
|
||||||
DateTime? _histTime;
|
DateTime? _histTime;
|
||||||
|
static const _headingFreezeSpeedKmh = 5.0;
|
||||||
// ── Legközelebbi forráspont ───────────────────────────────────────
|
|
||||||
final nearestPoint = Rxn<SourcePoint>();
|
|
||||||
final nearestDistance = 0.0.obs;
|
|
||||||
|
|
||||||
// ── Periodikus napló ───────────────────────────────────────────────
|
|
||||||
final isLogging = false.obs;
|
|
||||||
final logIntervalSec = 5.obs; // felhasználó által állítható
|
|
||||||
final logs = <VehiclePositionLog>[].obs;
|
|
||||||
Timer? _logTimer;
|
|
||||||
|
|
||||||
// ── Térkép — a MapController itt él, nem a View State-jében ────────
|
|
||||||
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();
|
|
||||||
refreshLogs();
|
watchSourcePoints();
|
||||||
|
ever(ProjectService.to.activeProject, (_) => watchSourcePoints());
|
||||||
|
|
||||||
if (Get.isRegistered<GnssService>()) {
|
if (Get.isRegistered<GnssService>()) {
|
||||||
// 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 =
|
_gnssUpdateSub =
|
||||||
GnssService.to.onDataUpdated.listen((_) => _onPosition());
|
GnssService.to.onDataUpdated.listen((_) => _onExternalPosition());
|
||||||
GnssService.to.determineInitialPosition().whenComplete(() {
|
GnssService.to.determineInitialPosition().whenComplete(() {
|
||||||
|
// A kezdő pozíció lekérése önmagában NEM vált ki onDataUpdated-et
|
||||||
|
// — ha lett érvényes koordináta, itt kell külön jeleznünk, hogy a
|
||||||
|
// térkép ne maradjon a "nincs pozíció" alapértéken.
|
||||||
|
final gnss = GnssService.to;
|
||||||
|
if (gnss.latitude.value != 0) {
|
||||||
|
hasPosition.value = true;
|
||||||
|
_addTrailPoint(LatLng(gnss.latitude.value, gnss.longitude.value));
|
||||||
|
}
|
||||||
mapReady.value = true;
|
mapReady.value = true;
|
||||||
});
|
});
|
||||||
} else {
|
} else {
|
||||||
mapReady.value = true;
|
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
|
// A kézi térkép-húzás felismerése mostantól a MapOptions
|
||||||
// _tryAutoStart() metódust semmi nem hívta meg.
|
// onPositionChanged (hasGesture) callback-jén megy, közvetlenül a
|
||||||
ever(hasPosition, (_) => _tryAutoStart());
|
// View-ban — az megbízhatóbb, mint a nyers mapEventStream-forrás
|
||||||
ever(VehicleIdentityService.to.selectedVehicle, (_) => _tryAutoStart());
|
// találgatása volt.
|
||||||
|
|
||||||
|
_externalUploadTimer =
|
||||||
|
Timer.periodic(const Duration(seconds: 5), (_) => _uploadExternal());
|
||||||
|
|
||||||
|
_startInternalGps();
|
||||||
|
_internalUploadTimer =
|
||||||
|
Timer.periodic(const Duration(seconds: 10), (_) => _uploadInternal());
|
||||||
}
|
}
|
||||||
|
|
||||||
@override
|
@override
|
||||||
void onClose() {
|
void onClose() {
|
||||||
_logTimer?.cancel();
|
|
||||||
_gnssUpdateSub?.cancel();
|
_gnssUpdateSub?.cancel();
|
||||||
|
_sourcePointsSub?.cancel();
|
||||||
|
_externalUploadTimer?.cancel();
|
||||||
|
_internalUploadTimer?.cancel();
|
||||||
|
_internalPositionSub?.cancel();
|
||||||
|
_internalGps.disconnect().whenComplete(_internalGps.dispose);
|
||||||
super.onClose();
|
super.onClose();
|
||||||
}
|
}
|
||||||
|
|
||||||
Future<void> refreshLogs() async {
|
void resumeFollowing() {
|
||||||
logs.value = await loadLogs();
|
followMode.value = true;
|
||||||
}
|
}
|
||||||
|
|
||||||
Future<void> load() async {
|
void toggleTrackUp() {
|
||||||
final projectId = ProjectService.to.activeProjectId;
|
trackUpEnabled.value = !trackUpEnabled.value;
|
||||||
if (projectId == null) {
|
|
||||||
sourcePoints.clear();
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
sourcePoints.value = await _db.listSourcePoints(projectId);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// ═════════════════════════════════════════════════════════════════
|
// ═════════════════════════════════════════════════════════════════
|
||||||
// Pozíció-feldolgozás
|
// Külső (fő) pozíció
|
||||||
// ═════════════════════════════════════════════════════════════════
|
// ═════════════════════════════════════════════════════════════════
|
||||||
|
|
||||||
void _onPosition() {
|
void _onExternalPosition() {
|
||||||
final gnss = GnssService.to;
|
final gnss = GnssService.to;
|
||||||
if (gnss.gpsQuality.value <= 0 ||
|
if (gnss.gpsQuality.value <= 0 ||
|
||||||
gnss.latitude.value == 0 ||
|
gnss.latitude.value == 0 ||
|
||||||
@@ -123,6 +159,7 @@ class VibroNavController extends GetxController {
|
|||||||
|
|
||||||
final p = CoordConverterService.to
|
final p = CoordConverterService.to
|
||||||
.wgsToEovPoint(gnss.longitude.value, gnss.latitude.value);
|
.wgsToEovPoint(gnss.longitude.value, gnss.latitude.value);
|
||||||
|
if (!p.x.isFinite || !p.y.isFinite) return;
|
||||||
final now = DateTime.now();
|
final now = DateTime.now();
|
||||||
|
|
||||||
if (_histY != null && _histTime != null) {
|
if (_histY != null && _histTime != null) {
|
||||||
@@ -130,128 +167,194 @@ class VibroNavController extends GetxController {
|
|||||||
final dx = p.y - _histX!;
|
final dx = p.y - _histX!;
|
||||||
final dist = math.sqrt(dy * dy + dx * dx);
|
final dist = math.sqrt(dy * dy + dx * dx);
|
||||||
final dtSec = now.difference(_histTime!).inMilliseconds / 1000.0;
|
final dtSec = now.difference(_histTime!).inMilliseconds / 1000.0;
|
||||||
|
|
||||||
if (dtSec > 0) {
|
if (dtSec > 0) {
|
||||||
final v = dist / dtSec; // m/s
|
final v = (dist / dtSec) * 3.6;
|
||||||
speedKmh.value = v * 3.6;
|
speedKmh.value = v;
|
||||||
sessionDistanceM.value += dist;
|
if (v >= _headingFreezeSpeedKmh && dist > 0.3) {
|
||||||
|
|
||||||
// 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 =
|
travelHeading.value =
|
||||||
(math.atan2(dy, dx) * 180 / math.pi + 360) % 360;
|
(math.atan2(dy, dx) * 180 / math.pi + 360) % 360;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
_histY = p.x;
|
_histY = p.x;
|
||||||
_histX = p.y;
|
_histX = p.y;
|
||||||
_histTime = now;
|
_histTime = now;
|
||||||
|
|
||||||
curEovY.value = p.x;
|
|
||||||
curEovX.value = p.y;
|
|
||||||
hasPosition.value = true;
|
hasPosition.value = true;
|
||||||
|
_addTrailPoint(LatLng(gnss.latitude.value, gnss.longitude.value));
|
||||||
_updateNearest();
|
|
||||||
}
|
}
|
||||||
|
|
||||||
void _updateNearest() {
|
void _addTrailPoint(LatLng p) {
|
||||||
if (sourcePoints.isEmpty) {
|
if (trail.isNotEmpty) {
|
||||||
nearestPoint.value = null;
|
final d = const Distance().as(LengthUnit.Meter, trail.last, p);
|
||||||
|
if (d < _trailMinDistM) return;
|
||||||
|
}
|
||||||
|
trail.add(p);
|
||||||
|
if (trail.length > _trailMaxPoints) trail.removeAt(0);
|
||||||
|
}
|
||||||
|
|
||||||
|
Future<void> _uploadExternal() async {
|
||||||
|
final gnss = GnssService.to;
|
||||||
|
if (!hasPosition.value) return;
|
||||||
|
final vehicleNumber =
|
||||||
|
_vehicleNumberFrom(VehicleIdentityService.to.selectedVehicle.value);
|
||||||
|
if (vehicleNumber == null) return;
|
||||||
|
|
||||||
|
try {
|
||||||
|
final p = Get.isRegistered<CoordConverterService>()
|
||||||
|
? CoordConverterService.to
|
||||||
|
.wgsToEovPoint(gnss.longitude.value, gnss.latitude.value)
|
||||||
|
: null;
|
||||||
|
|
||||||
|
await Supabase.instance.client
|
||||||
|
.from('TerepiSeged_VibratorTracker')
|
||||||
|
.insert({
|
||||||
|
'gpsType': _gpsTypeExternal,
|
||||||
|
'vibratorNumber': vehicleNumber,
|
||||||
|
'gpsQuality': gnss.gpsQuality.value,
|
||||||
|
'gpsDateTime': gnss.gpsDateTime.value.toUtc().toIso8601String(),
|
||||||
|
'latitude': gnss.latitude.value,
|
||||||
|
'longitude': gnss.longitude.value,
|
||||||
|
'altitude': gnss.altitude.value,
|
||||||
|
'eovY': p?.x,
|
||||||
|
'eovX': p?.y,
|
||||||
|
'horizontalError':
|
||||||
|
math.max(gnss.latitudeError.value, gnss.longitudeError.value),
|
||||||
|
'verticalError': gnss.altitudeError.value,
|
||||||
|
});
|
||||||
|
lastExternalUploadAt.value = DateTime.now();
|
||||||
|
_pulse(externalUploadPulse);
|
||||||
|
} catch (e) {
|
||||||
|
AppLogger.e(
|
||||||
|
'NavigationController', 'Külső GPS-pozíció feltöltési hiba: $e');
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// ═════════════════════════════════════════════════════════════════
|
||||||
|
// Belső (telefon) GPS
|
||||||
|
// ═════════════════════════════════════════════════════════════════
|
||||||
|
|
||||||
|
Future<void> _startInternalGps() async {
|
||||||
|
_internalPositionSub = _internalGps.positionStream.listen((pos) {
|
||||||
|
_lastInternalPosition = pos;
|
||||||
|
});
|
||||||
|
await _internalGps.connect('internal');
|
||||||
|
}
|
||||||
|
|
||||||
|
Future<void> _uploadInternal() async {
|
||||||
|
final pos = _lastInternalPosition;
|
||||||
|
if (pos == null) return;
|
||||||
|
final vehicleNumber =
|
||||||
|
_vehicleNumberFrom(VehicleIdentityService.to.selectedVehicle.value);
|
||||||
|
if (vehicleNumber == null) return;
|
||||||
|
|
||||||
|
try {
|
||||||
|
final p = Get.isRegistered<CoordConverterService>()
|
||||||
|
? CoordConverterService.to.wgsToEovPoint(pos.longitude, pos.latitude)
|
||||||
|
: null;
|
||||||
|
|
||||||
|
await Supabase.instance.client
|
||||||
|
.from('TerepiSeged_VibratorTracker')
|
||||||
|
.insert({
|
||||||
|
'gpsType': _gpsTypeInternal,
|
||||||
|
'vibratorNumber': vehicleNumber,
|
||||||
|
'gpsQuality': 1,
|
||||||
|
'gpsDateTime': pos.timestamp.toUtc().toIso8601String(),
|
||||||
|
'latitude': pos.latitude,
|
||||||
|
'longitude': pos.longitude,
|
||||||
|
'altitude': pos.altitude,
|
||||||
|
'eovY': p?.x,
|
||||||
|
'eovX': p?.y,
|
||||||
|
'horizontalError': pos.accuracy,
|
||||||
|
'verticalError': pos.altitudeAccuracy,
|
||||||
|
});
|
||||||
|
lastInternalUploadAt.value = DateTime.now();
|
||||||
|
_pulse(internalUploadPulse);
|
||||||
|
} catch (e) {
|
||||||
|
AppLogger.e('NavigationController',
|
||||||
|
'Belső (telefon) GPS-pozíció feltöltési hiba: $e');
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// ═════════════════════════════════════════════════════════════════
|
||||||
|
// Segéd
|
||||||
|
// ═════════════════════════════════════════════════════════════════
|
||||||
|
|
||||||
|
void _pulse(RxBool flag) {
|
||||||
|
flag.value = true;
|
||||||
|
Future.delayed(const Duration(milliseconds: 900), () => flag.value = false);
|
||||||
|
}
|
||||||
|
|
||||||
|
/// A "Teszt" egy jól elkülöníthető, a valódi 1-4-es járművekkel SOHA
|
||||||
|
/// nem ütköző azonosítót kap (99) — így tesztelés közben is végigfut
|
||||||
|
/// a teljes feltöltési lánc, csak a PC-szoftver oldalán egyértelműen
|
||||||
|
/// megkülönböztethető a valódi terepi adatoktól.
|
||||||
|
static const testVehicleNumber = 99;
|
||||||
|
|
||||||
|
int? _vehicleNumberFrom(String? vehicleId) {
|
||||||
|
if (vehicleId == null) return null;
|
||||||
|
if (vehicleId == 'Teszt') return testVehicleNumber;
|
||||||
|
final digits = vehicleId.replaceAll(RegExp(r'[^0-9]'), '');
|
||||||
|
return int.tryParse(digits);
|
||||||
|
}
|
||||||
|
|
||||||
|
void watchSourcePoints() {
|
||||||
|
_sourcePointsSub?.cancel();
|
||||||
|
final projectId = ProjectService.to.activeProject.value?.uuid;
|
||||||
|
if (projectId == null) {
|
||||||
|
sourcePoints.clear();
|
||||||
|
geoSourcePoints.clear();
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
SourcePoint? best;
|
isLoadingSourcePoints.value = true;
|
||||||
var bestDist = double.infinity;
|
_sourcePointsSub =
|
||||||
for (final sp in sourcePoints) {
|
MeasureSourcePointService.to.watchTodayOrUndated(projectId).listen(
|
||||||
final dy = sp.planEovY - curEovY.value;
|
(result) {
|
||||||
final dx = sp.planEovX - curEovX.value;
|
sourcePoints.value = result.points;
|
||||||
final d = math.sqrt(dy * dy + dx * dx);
|
geoSourcePoints.value = _toGeo(result.points);
|
||||||
if (d < bestDist) {
|
isLoadingSourcePoints.value = false;
|
||||||
bestDist = d;
|
if (result.capped) {
|
||||||
best = sp;
|
Get.snackbar(
|
||||||
}
|
'Sok forráspont',
|
||||||
}
|
'A megjelenített pontok száma korlátozva lett — érdemes '
|
||||||
nearestPoint.value = best;
|
'kitölteni a tervezett dátumot a projektben.',
|
||||||
nearestDistance.value = bestDist;
|
snackPosition: SnackPosition.BOTTOM,
|
||||||
}
|
duration: const Duration(seconds: 5),
|
||||||
|
|
||||||
// ═════════════════════════════════════════════════════════════════
|
|
||||||
// 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;
|
|
||||||
}
|
|
||||||
|
|
||||||
void _tryAutoStart() {
|
|
||||||
if (isLogging.value) return;
|
|
||||||
if (!hasPosition.value) return;
|
|
||||||
if (VehicleIdentityService.to.selectedVehicle.value == null) return;
|
|
||||||
startLogging();
|
|
||||||
}
|
|
||||||
|
|
||||||
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);
|
|
||||||
logs.add(entry);
|
|
||||||
}
|
}
|
||||||
|
},
|
||||||
|
onError: (e) {
|
||||||
|
isLoadingSourcePoints.value = false;
|
||||||
|
AppLogger.e('NavigationController', 'Forráspont-stream hiba: $e');
|
||||||
|
},
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
void toggleReservePoints() {
|
||||||
|
showReservePoints.value = !showReservePoints.value;
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Átváltás pontonként EGYSZER. A nem véges eredményeket eldobjuk,
|
||||||
|
/// mert a LatLng NaN összeomlasztja a kamerát.
|
||||||
|
List<GeoSourcePoint> _toGeo(List<MeasureSourcePoint> pts) {
|
||||||
|
if (!Get.isRegistered<CoordConverterService>()) return const [];
|
||||||
|
final conv = CoordConverterService.to;
|
||||||
|
final out = <GeoSourcePoint>[];
|
||||||
|
for (final p in pts) {
|
||||||
|
final key = '${p.eovX}_${p.eovY}';
|
||||||
|
final ll = _geoCache.putIfAbsent(key, () {
|
||||||
|
try {
|
||||||
|
final w = conv.eovToWgsPoint(p.eovY, p.eovX);
|
||||||
|
if (w.x.isFinite && w.y.isFinite) return LatLng(w.y, w.x);
|
||||||
|
} catch (_) {}
|
||||||
|
return null;
|
||||||
|
});
|
||||||
|
if (ll != null) out.add(GeoSourcePoint(p, ll));
|
||||||
|
}
|
||||||
|
return out;
|
||||||
|
}
|
||||||
|
|
||||||
|
List<GeoSourcePoint> get visibleGeoSourcePoints => showReservePoints.value
|
||||||
|
? geoSourcePoints
|
||||||
|
: geoSourcePoints.where((g) => g.point.isShotLocation).toList();
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,57 +1,59 @@
|
|||||||
import 'dart:io';
|
import 'dart:math' as math;
|
||||||
|
|
||||||
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:flutter_map/flutter_map.dart';
|
||||||
import 'package:get/get.dart';
|
import 'package:get/get.dart';
|
||||||
import 'package:latlong2/latlong.dart';
|
import 'package:latlong2/latlong.dart';
|
||||||
import 'package:terepi_seged/models/source_point.dart';
|
import 'package:terepi_seged/pages/vibro_nav/presentation/controllers/vibro_nav_controller.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_connection.dart';
|
||||||
import 'package:terepi_seged/services/gnss/gnss_device_service.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/sps_import_service.dart';
|
|
||||||
import 'package:terepi_seged/services/vechicle_identity_service.dart';
|
import 'package:terepi_seged/services/vechicle_identity_service.dart';
|
||||||
|
import 'package:terepi_seged/widgets/gnss_device_picker_dialog.dart';
|
||||||
import 'package:terepi_seged/widgets/map/animated_map_follow.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> {
|
class VibroNavView extends GetView<VibroNavController> {
|
||||||
const VibroNavView({super.key});
|
const VibroNavView({super.key});
|
||||||
|
|
||||||
@override
|
@override
|
||||||
Widget build(BuildContext context) {
|
Widget build(BuildContext context) {
|
||||||
return Scaffold(
|
return Scaffold(
|
||||||
|
// 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(
|
appBar: AppBar(
|
||||||
title: const Text('Navigáció'),
|
// title: const Text('Navigáció'),
|
||||||
|
title: Obx(() => Text(
|
||||||
|
'N(${controller.sourcePoints.length}/${controller.visibleGeoSourcePoints.length})',
|
||||||
|
style: const TextStyle(fontSize: 11),
|
||||||
|
)),
|
||||||
actions: [
|
actions: [
|
||||||
|
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(
|
IconButton(
|
||||||
icon: const Icon(Icons.bluetooth_connected),
|
icon: const Icon(Icons.bluetooth_connected),
|
||||||
tooltip: "Gyors újracsatlakozás a GNSS vevőhöz",
|
tooltip: 'Gyors újracsatlakozás az utolsó GNSS-vevőhöz',
|
||||||
onPressed: _quickReconnectGnss,
|
onPressed: _quickReconnect,
|
||||||
),
|
),
|
||||||
IconButton(
|
IconButton(
|
||||||
icon: const Icon(Icons.layers_outlined),
|
icon: const Icon(Icons.settings_input_antenna),
|
||||||
tooltip: 'Réteg importja (GeoJSON/KML/KMZ)',
|
tooltip: 'GNSS-eszköz kiválasztása',
|
||||||
onPressed: _importLayer,
|
onPressed: () => GnssDevicePickerDialog.show(),
|
||||||
),
|
|
||||||
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,
|
|
||||||
),
|
),
|
||||||
],
|
],
|
||||||
),
|
),
|
||||||
@@ -70,425 +72,334 @@ class VibroNavView extends GetView<VibroNavController> {
|
|||||||
}
|
}
|
||||||
return Stack(
|
return Stack(
|
||||||
children: [
|
children: [
|
||||||
_buildMap(),
|
const _NavMap(),
|
||||||
Builder(builder: (context) {
|
Obx(() {
|
||||||
final target = controller.hasPosition.value &&
|
final gnss = GnssService.to;
|
||||||
Get.isRegistered<GnssService>()
|
final target =
|
||||||
? LatLng(GnssService.to.latitude.value,
|
controller.followMode.value && controller.hasPosition.value
|
||||||
GnssService.to.longitude.value)
|
? LatLng(gnss.latitude.value, gnss.longitude.value)
|
||||||
|
: null;
|
||||||
|
final heading = controller.trackUpEnabled.value
|
||||||
|
? controller.travelHeading.value
|
||||||
: null;
|
: null;
|
||||||
return AnimatedMapFollow(
|
return AnimatedMapFollow(
|
||||||
mapController: controller.mapController,
|
mapController: controller.mapController,
|
||||||
target: target,
|
target: target,
|
||||||
heading: controller.travelHeading.value,
|
heading: heading,
|
||||||
);
|
);
|
||||||
}),
|
}),
|
||||||
|
// EGYETLEN sor, egymás mellett — nincs két, egymást átfedő
|
||||||
|
// Positioned widget, tehát fizikailag nem takarhatják egymást.
|
||||||
Positioned(
|
Positioned(
|
||||||
left: 8,
|
left: 8,
|
||||||
right: 8,
|
right: 8,
|
||||||
top: 8,
|
top: 8,
|
||||||
child: Row(
|
child: Row(
|
||||||
children: [
|
children: [
|
||||||
Expanded(child: _VehicleBadge()),
|
const Expanded(child: _VehicleBadge()),
|
||||||
const SizedBox(width: 8),
|
const SizedBox(width: 8),
|
||||||
const _GnssStatusBadge(),
|
const _GnssStatusBadge(),
|
||||||
|
const SizedBox(width: 8),
|
||||||
|
const _CompassWidget(),
|
||||||
],
|
],
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
Positioned(
|
Positioned(
|
||||||
left: 8,
|
left: 12,
|
||||||
right: 8,
|
right: 12,
|
||||||
bottom: 8,
|
bottom: 16,
|
||||||
child: _NavPanel(controller: controller),
|
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),
|
||||||
|
),
|
||||||
|
)),
|
||||||
],
|
],
|
||||||
);
|
);
|
||||||
}));
|
}),
|
||||||
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
Widget _buildMap() {
|
void _quickReconnect() {
|
||||||
return Obx(() {
|
final device = GnssDeviceService.to.selectedDevice.value;
|
||||||
final points = controller.sourcePoints;
|
if (device == null) {
|
||||||
final nearest = controller.nearestPoint.value;
|
Get.snackbar('Nincs korábbi GNSS-eszköz',
|
||||||
|
'Előbb válassz ki egyet a jobb felső ikonnal.',
|
||||||
|
snackPosition: SnackPosition.BOTTOM);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
Get.snackbar('Újracsatlakozás', '„${device.name}" — folyamatban…',
|
||||||
|
snackPosition: SnackPosition.BOTTOM,
|
||||||
|
duration: const Duration(seconds: 2));
|
||||||
|
GnssService.to.reconnect();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
final gnssPos =
|
Color _qualityColor(int quality) =>
|
||||||
Get.isRegistered<GnssService>() && GnssService.to.latitude.value != 0
|
quality >= 4 ? Colors.green : (quality > 0 ? Colors.orange : Colors.grey);
|
||||||
? LatLng(
|
|
||||||
GnssService.to.latitude.value, GnssService.to.longitude.value)
|
/// A térkép ITT, önálló widgetként él: a build-je Obx-en kívül fut, így
|
||||||
: null;
|
/// GNSS-frissítéskor a FlutterMap és a TileLayer NEM épül újra. Csak az
|
||||||
final center = gnssPos ?? const LatLng(47.5, 19.05);
|
/// alábbi, saját Obx-ű rétegek frissülnek.
|
||||||
|
class _NavMap extends StatelessWidget {
|
||||||
|
const _NavMap();
|
||||||
|
|
||||||
|
@override
|
||||||
|
Widget build(BuildContext context) {
|
||||||
|
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);
|
||||||
|
|
||||||
return FlutterMap(
|
return FlutterMap(
|
||||||
mapController: controller.mapController,
|
mapController: controller.mapController,
|
||||||
options: MapOptions(
|
options: MapOptions(
|
||||||
initialCenter: center,
|
initialCenter: center,
|
||||||
initialZoom: 15,
|
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: [
|
children: [
|
||||||
// TileLayer(
|
|
||||||
// urlTemplate: 'https://tile.openstreetmap.org/{z}/{x}/{y}.png',
|
|
||||||
// userAgentPackageName: 'hu.app_dev.terepi_seged',
|
|
||||||
// ),
|
|
||||||
TileLayer(
|
TileLayer(
|
||||||
urlTemplate: 'http://{s}.google.com/vt/lyrs=s,h&x={x}&y={y}&z={z}',
|
urlTemplate: 'http://{s}.google.com/vt/lyrs=s,h&x={x}&y={y}&z={z}',
|
||||||
subdomains: const ['mt0', 'mt1', 'mt2', 'mt3'],
|
subdomains: const ['mt0', 'mt1', 'mt2', 'mt3'],
|
||||||
maxNativeZoom: 18,
|
maxNativeZoom: 18),
|
||||||
|
const _SourcePointsLayer(),
|
||||||
|
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),
|
||||||
|
),
|
||||||
|
]);
|
||||||
|
}),
|
||||||
|
],
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
// Tervezett útvonal + minden más importált háttérréteg — a
|
/// A forráspontok rétege — csak a `geoSourcePoints` és a
|
||||||
// MEGLÉVŐ rétegimport-mechanizmus, nincs hozzá új kód.
|
/// `showReservePoints` változására épül újra, a GNSS-frissítésekre nem.
|
||||||
const ImportedLayerOverlay(),
|
/// A koordináták a controllerben már átváltva vannak.
|
||||||
MarkerLayer(markers: [
|
class _SourcePointsLayer extends StatelessWidget {
|
||||||
for (final sp in points)
|
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(
|
Marker(
|
||||||
point: LatLng(sp.planLat, sp.planLon),
|
point: g.latLng,
|
||||||
width: 40,
|
width: 74,
|
||||||
height: 40,
|
height: 26,
|
||||||
|
alignment: Alignment.centerLeft,
|
||||||
child: _SourcePointMarker(
|
child: _SourcePointMarker(
|
||||||
point: sp,
|
isShot: g.point.isShotLocation,
|
||||||
isNearest: sp.uuid == nearest?.uuid,
|
label: _fmtPointNumber(g.point.pointNumber),
|
||||||
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 ────────────────────────────────────────────────
|
class _PulsingPositionMarker extends StatefulWidget {
|
||||||
|
final Color color;
|
||||||
|
const _PulsingPositionMarker({required this.color});
|
||||||
|
|
||||||
Future<void> _pickVehicle() async {
|
@override
|
||||||
final chosen = await Get.dialog<String>(AlertDialog(
|
State<_PulsingPositionMarker> createState() => _PulsingPositionMarkerState();
|
||||||
title: const Text('Melyik járműben van ez a tablet?'),
|
}
|
||||||
content: Column(
|
|
||||||
mainAxisSize: MainAxisSize.min,
|
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();
|
||||||
|
}
|
||||||
|
|
||||||
|
@override
|
||||||
|
Widget build(BuildContext context) {
|
||||||
|
return AnimatedBuilder(
|
||||||
|
animation: _ctrl,
|
||||||
|
builder: (context, _) {
|
||||||
|
final t = _ctrl.value;
|
||||||
|
return Stack(
|
||||||
|
alignment: Alignment.center,
|
||||||
children: [
|
children: [
|
||||||
for (final v in VehicleIdentityService.availableVehicles)
|
Opacity(
|
||||||
RadioListTile<String>(
|
opacity: (1 - t).clamp(0.0, 1.0),
|
||||||
title: Text(v),
|
child: Container(
|
||||||
value: v,
|
width: 18 + t * 34,
|
||||||
groupValue: VehicleIdentityService.to.selectedVehicle.value,
|
height: 18 + t * 34,
|
||||||
onChanged: (val) => Get.back(result: val),
|
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),
|
||||||
],
|
],
|
||||||
),
|
),
|
||||||
));
|
),
|
||||||
if (chosen != null) {
|
|
||||||
await VehicleIdentityService.to.setVehicle(chosen);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
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 ────
|
|
||||||
|
|
||||||
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);
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// ═══════════════════════════════════════════════════════════════════
|
class _CompassWidget extends StatelessWidget {
|
||||||
// HUD panel: sebesség, táv, legközelebbi pont, rögzítés vezérlés
|
const _CompassWidget();
|
||||||
// ═══════════════════════════════════════════════════════════════════
|
|
||||||
|
|
||||||
class _NavPanel extends StatelessWidget {
|
@override
|
||||||
final VibroNavController controller;
|
Widget build(BuildContext context) {
|
||||||
const _NavPanel({required this.controller});
|
final controller = VibroNavController.to;
|
||||||
|
return Obx(() {
|
||||||
|
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),
|
||||||
|
),
|
||||||
|
);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
class _VehicleBadge extends StatelessWidget {
|
||||||
|
const _VehicleBadge();
|
||||||
|
|
||||||
@override
|
@override
|
||||||
Widget build(BuildContext context) {
|
Widget build(BuildContext context) {
|
||||||
return Obx(() {
|
return Obx(() {
|
||||||
final nearest = controller.nearestPoint.value;
|
final vehicle = VehicleIdentityService.to.selectedVehicle.value;
|
||||||
return Card(
|
return Material(
|
||||||
elevation: 6,
|
elevation: 2,
|
||||||
|
borderRadius: BorderRadius.circular(8),
|
||||||
|
color: Colors.white,
|
||||||
|
child: InkWell(
|
||||||
|
borderRadius: BorderRadius.circular(8),
|
||||||
|
onTap: () => _pickVehicle(context),
|
||||||
child: Padding(
|
child: Padding(
|
||||||
padding: const EdgeInsets.all(12),
|
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)),
|
||||||
|
),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
void _pickVehicle(BuildContext context) {
|
||||||
|
Get.bottomSheet(
|
||||||
|
Material(
|
||||||
|
color: Colors.white,
|
||||||
child: Column(
|
child: Column(
|
||||||
mainAxisSize: MainAxisSize.min,
|
mainAxisSize: MainAxisSize.min,
|
||||||
children: [
|
children: [
|
||||||
Row(
|
for (final v in VehicleIdentityService.availableVehicles)
|
||||||
mainAxisAlignment: MainAxisAlignment.spaceAround,
|
ListTile(
|
||||||
children: [
|
title: Text(v),
|
||||||
_Stat(
|
onTap: () {
|
||||||
label: 'Sebesség',
|
VehicleIdentityService.to.setVehicle(v);
|
||||||
value:
|
Get.back();
|
||||||
'${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),
|
|
||||||
);
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -527,23 +438,213 @@ class _GnssStatusBadge extends StatelessWidget {
|
|||||||
}
|
}
|
||||||
|
|
||||||
return Container(
|
return Container(
|
||||||
padding: const EdgeInsets.symmetric(horizontal: 8, vertical: 4),
|
padding: const EdgeInsets.symmetric(horizontal: 10, vertical: 8),
|
||||||
decoration: BoxDecoration(
|
decoration: BoxDecoration(
|
||||||
color: color.withOpacity(0.15),
|
color: Colors.white,
|
||||||
borderRadius: BorderRadius.circular(6),
|
borderRadius: BorderRadius.circular(8),
|
||||||
border: Border.all(color: color, width: 1),
|
border: Border.all(color: color, width: 1.5),
|
||||||
|
boxShadow: const [BoxShadow(color: Colors.black12, blurRadius: 3)],
|
||||||
),
|
),
|
||||||
child: Row(
|
child: Row(
|
||||||
mainAxisSize: MainAxisSize.min,
|
mainAxisSize: MainAxisSize.min,
|
||||||
children: [
|
children: [
|
||||||
Icon(icon, size: 14, color: color),
|
Icon(icon, size: 16, color: color),
|
||||||
const SizedBox(width: 4),
|
const SizedBox(width: 4),
|
||||||
Text(label,
|
Text(label,
|
||||||
style: TextStyle(
|
style: TextStyle(
|
||||||
fontSize: 11, fontWeight: FontWeight.w600, color: color)),
|
fontSize: 12, fontWeight: FontWeight.w700, color: color)),
|
||||||
],
|
],
|
||||||
),
|
),
|
||||||
);
|
);
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
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,
|
||||||
|
),
|
||||||
|
),
|
||||||
|
],
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|||||||
@@ -85,8 +85,10 @@ class PhoneGpsConnection implements GnssConnection {
|
|||||||
Future<void> disconnect() async {
|
Future<void> disconnect() async {
|
||||||
_retryTimer?.cancel();
|
_retryTimer?.cancel();
|
||||||
await _positionSub?.cancel();
|
await _positionSub?.cancel();
|
||||||
|
if (!_stateController.isClosed) {
|
||||||
_stateController.add(GnssConnectionState.disconnected);
|
_stateController.add(GnssConnectionState.disconnected);
|
||||||
}
|
}
|
||||||
|
}
|
||||||
|
|
||||||
@override
|
@override
|
||||||
void sendData(Uint8List data) {
|
void sendData(Uint8List data) {
|
||||||
|
|||||||
@@ -0,0 +1,108 @@
|
|||||||
|
import 'dart:async';
|
||||||
|
|
||||||
|
import 'package:get/get.dart';
|
||||||
|
import 'package:supabase_flutter/supabase_flutter.dart';
|
||||||
|
|
||||||
|
import '../models/measure_source_point.dart';
|
||||||
|
|
||||||
|
class SourcePointFetchResult {
|
||||||
|
final List<MeasureSourcePoint> points;
|
||||||
|
final bool capped;
|
||||||
|
const SourcePointFetchResult({required this.points, required this.capped});
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Forráspontok (jelgerjesztés/tartalék) lekérdezése a navigációs
|
||||||
|
/// térképhez. SZÁNDÉKOSAN tisztán online — nincs helyi tükör.
|
||||||
|
class MeasureSourcePointService extends GetxService {
|
||||||
|
static MeasureSourcePointService get to => Get.find();
|
||||||
|
|
||||||
|
SupabaseClient get _client => Supabase.instance.client;
|
||||||
|
|
||||||
|
/// Ha egy pontnak VAN megadott tervezett dátuma, csak a mait mutatjuk;
|
||||||
|
/// aminek NINCS dátuma, azt mindig — plusz egy kemény felső korlát,
|
||||||
|
/// nehogy egy dátum nélküli projektnél ezerszámra terhelje a térképet.
|
||||||
|
static const _safetyLimit = 6000;
|
||||||
|
|
||||||
|
/// Élő frissítés a stream() helyett: a tényleges adatot SZERVER-oldali
|
||||||
|
/// szűréssel, lapozva kérjük le (a PostgREST 1000 soros felső
|
||||||
|
/// korlátja miatt a .stream() csonkított, rendezetlen részhalmazt
|
||||||
|
/// adott), a realtime csatorna pedig csak jelez, hogy változott
|
||||||
|
/// valami — ilyenkor újra lekérünk.
|
||||||
|
Stream<SourcePointFetchResult> watchTodayOrUndated(String projectId) {
|
||||||
|
late StreamController<SourcePointFetchResult> controller;
|
||||||
|
RealtimeChannel? channel;
|
||||||
|
Timer? debounce;
|
||||||
|
var disposed = false;
|
||||||
|
|
||||||
|
Future<void> refetch() async {
|
||||||
|
if (disposed) return;
|
||||||
|
try {
|
||||||
|
final result = await _fetchAll(projectId);
|
||||||
|
if (!disposed) controller.add(result);
|
||||||
|
} catch (e, s) {
|
||||||
|
if (!disposed) controller.addError(e, s);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
controller = StreamController<SourcePointFetchResult>(
|
||||||
|
onListen: () {
|
||||||
|
refetch();
|
||||||
|
channel = _client
|
||||||
|
.channel('source_points_$projectId')
|
||||||
|
.onPostgresChanges(
|
||||||
|
event: PostgresChangeEvent.all,
|
||||||
|
schema: 'public',
|
||||||
|
table: 'terepi_seged_measure_source_points',
|
||||||
|
filter: PostgresChangeFilter(
|
||||||
|
type: PostgresChangeFilterType.eq,
|
||||||
|
column: 'project_id',
|
||||||
|
value: projectId,
|
||||||
|
),
|
||||||
|
callback: (_) {
|
||||||
|
// Sok egyidejű változásnál (pl. tömeges import) ne
|
||||||
|
// kérjünk le minden egyes sorra újra.
|
||||||
|
debounce?.cancel();
|
||||||
|
debounce = Timer(const Duration(milliseconds: 600), refetch);
|
||||||
|
},
|
||||||
|
)
|
||||||
|
.subscribe();
|
||||||
|
},
|
||||||
|
onCancel: () async {
|
||||||
|
disposed = true;
|
||||||
|
debounce?.cancel();
|
||||||
|
if (channel != null) await _client.removeChannel(channel!);
|
||||||
|
},
|
||||||
|
);
|
||||||
|
return controller.stream;
|
||||||
|
}
|
||||||
|
|
||||||
|
Future<SourcePointFetchResult> _fetchAll(String projectId) async {
|
||||||
|
final now = DateTime.now();
|
||||||
|
final todayStr = '${now.year.toString().padLeft(4, '0')}-'
|
||||||
|
'${now.month.toString().padLeft(2, '0')}-'
|
||||||
|
'${now.day.toString().padLeft(2, '0')}';
|
||||||
|
|
||||||
|
const pageSize = 1000;
|
||||||
|
final all = <MeasureSourcePoint>[];
|
||||||
|
var from = 0;
|
||||||
|
while (all.length <= _safetyLimit) {
|
||||||
|
final rows = await _client
|
||||||
|
.from('terepi_seged_measure_source_points')
|
||||||
|
.select()
|
||||||
|
.eq('project_id', projectId)
|
||||||
|
.eq('is_active', true)
|
||||||
|
.or('planned_acquisition_date.eq.$todayStr,'
|
||||||
|
'planned_acquisition_date.is.null')
|
||||||
|
.order('line_number', ascending: true)
|
||||||
|
.order('point_number', ascending: true)
|
||||||
|
.range(from, from + pageSize - 1);
|
||||||
|
all.addAll(rows.map((r) => MeasureSourcePoint.fromMap(r)));
|
||||||
|
if (rows.length < pageSize) break;
|
||||||
|
from += pageSize;
|
||||||
|
}
|
||||||
|
|
||||||
|
final capped = all.length > _safetyLimit;
|
||||||
|
if (capped) all.removeRange(_safetyLimit, all.length);
|
||||||
|
return SourcePointFetchResult(points: all, capped: capped);
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -1,3 +1,4 @@
|
|||||||
|
import 'dart:async';
|
||||||
import 'dart:typed_data';
|
import 'dart:typed_data';
|
||||||
|
|
||||||
import 'package:get/get.dart';
|
import 'package:get/get.dart';
|
||||||
@@ -17,16 +18,92 @@ class MeasuredDataService extends GetxService {
|
|||||||
/// TODO: erősítsd meg a valós bucket-nevet, ha eltér ettől.
|
/// TODO: erősítsd meg a valós bucket-nevet, ha eltér ettől.
|
||||||
static const _bucket = 'terepi_seged-measure-images';
|
static const _bucket = 'terepi_seged-measure-images';
|
||||||
|
|
||||||
/// Élő, folyamatosan frissülő stream — a Supabase Realtime-on keresztül,
|
static const _pageSize = 1000;
|
||||||
/// pontosan úgy, ahogy korábban a Firestore .snapshots() működött. Új
|
|
||||||
/// felvétel a PC-szoftverből AZONNAL megjelenik, frissítés nélkül.
|
/// A legfrissebb ennyi felvételt töltjük be (a PostgREST 1000-es
|
||||||
|
/// max-rows miatt lapozva).
|
||||||
|
static const _maxRecords = 3000;
|
||||||
|
|
||||||
|
/// SZÁNDÉKOSAN nem `.stream()`: az rendezetlen, 1000 sorra vágott
|
||||||
|
/// lekérdezést futtat, ezért sorok tűnnek el. Helyette szerveroldalon
|
||||||
|
/// szűrt, lapozott lekérdezés + Realtime csatorna, ami csak jelzés
|
||||||
|
/// (debounce-olt újratöltés).
|
||||||
Stream<List<MeasuredRecord>> watchRecords(String projectId) {
|
Stream<List<MeasuredRecord>> watchRecords(String projectId) {
|
||||||
return _client
|
late StreamController<List<MeasuredRecord>> controller;
|
||||||
|
RealtimeChannel? channel;
|
||||||
|
Timer? debounce;
|
||||||
|
var disposed = false;
|
||||||
|
var fetching = false;
|
||||||
|
var again = false;
|
||||||
|
|
||||||
|
Future<void> refetch() async {
|
||||||
|
if (disposed) return;
|
||||||
|
if (fetching) {
|
||||||
|
again = true;
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
fetching = true;
|
||||||
|
try {
|
||||||
|
do {
|
||||||
|
again = false;
|
||||||
|
final list = await _fetchAll(projectId);
|
||||||
|
if (!disposed) controller.add(list);
|
||||||
|
} while (again && !disposed);
|
||||||
|
} catch (e, s) {
|
||||||
|
if (!disposed) controller.addError(e, s);
|
||||||
|
} finally {
|
||||||
|
fetching = false;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
controller = StreamController<List<MeasuredRecord>>(
|
||||||
|
onListen: () {
|
||||||
|
refetch();
|
||||||
|
channel = _client
|
||||||
|
.channel('measure_records_$projectId')
|
||||||
|
.onPostgresChanges(
|
||||||
|
event: PostgresChangeEvent.all,
|
||||||
|
schema: 'public',
|
||||||
|
table: 'terepi_seged_measure_records',
|
||||||
|
filter: PostgresChangeFilter(
|
||||||
|
type: PostgresChangeFilterType.eq,
|
||||||
|
column: 'project_id',
|
||||||
|
value: projectId,
|
||||||
|
),
|
||||||
|
callback: (_) {
|
||||||
|
debounce?.cancel();
|
||||||
|
debounce = Timer(const Duration(milliseconds: 800), refetch);
|
||||||
|
},
|
||||||
|
)
|
||||||
|
.subscribe();
|
||||||
|
},
|
||||||
|
onCancel: () async {
|
||||||
|
disposed = true;
|
||||||
|
debounce?.cancel();
|
||||||
|
if (channel != null) await _client.removeChannel(channel!);
|
||||||
|
},
|
||||||
|
);
|
||||||
|
return controller.stream;
|
||||||
|
}
|
||||||
|
|
||||||
|
Future<List<MeasuredRecord>> _fetchAll(String projectId) async {
|
||||||
|
final out = <MeasuredRecord>[];
|
||||||
|
var from = 0;
|
||||||
|
while (out.length < _maxRecords) {
|
||||||
|
final rows = await _client
|
||||||
.from('terepi_seged_measure_records')
|
.from('terepi_seged_measure_records')
|
||||||
.stream(primaryKey: ['id'])
|
.select()
|
||||||
.eq('project_id', projectId)
|
.eq('project_id', projectId)
|
||||||
.order('measured_at', ascending: false)
|
.order('measured_at', ascending: false)
|
||||||
.map((rows) => rows.map((r) => MeasuredRecord.fromMap(r)).toList());
|
.order('id', ascending: false)
|
||||||
|
.range(from, from + _pageSize - 1);
|
||||||
|
for (final r in rows) {
|
||||||
|
out.add(MeasuredRecord.fromMap(r));
|
||||||
|
}
|
||||||
|
if (rows.length < _pageSize) break;
|
||||||
|
from += _pageSize;
|
||||||
|
}
|
||||||
|
return out;
|
||||||
}
|
}
|
||||||
|
|
||||||
/// A felvételhez tartozó kép betöltése — elsőbbséget adva az image_url-nek
|
/// A felvételhez tartozó kép betöltése — elsőbbséget adva az image_url-nek
|
||||||
|
|||||||
@@ -10,7 +10,7 @@ class VehicleIdentityService extends GetxService {
|
|||||||
static const _key = 'vehicle_id';
|
static const _key = 'vehicle_id';
|
||||||
|
|
||||||
/// Az alapértelmezett választható lista — igény szerint bővíthető.
|
/// Az alapértelmezett választható lista — igény szerint bővíthető.
|
||||||
static const availableVehicles = ['V1', 'V2', 'V3'];
|
static const availableVehicles = ['V1', 'V2', 'V3', 'V4', 'Teszt'];
|
||||||
|
|
||||||
final selectedVehicle = Rxn<String>();
|
final selectedVehicle = Rxn<String>();
|
||||||
|
|
||||||
|
|||||||
@@ -42,9 +42,18 @@ class AnimatedMapFollow extends StatelessWidget {
|
|||||||
duration: duration,
|
duration: duration,
|
||||||
curve: Curves.easeOut,
|
curve: Curves.easeOut,
|
||||||
builder: (context, animatedPos, _) {
|
builder: (context, animatedPos, _) {
|
||||||
|
// NE hívjuk a .move()-ot közvetlenül itt — ez a builder BUILD
|
||||||
|
// közben fut, a mapController.move() pedig a flutter_map
|
||||||
|
// belső gesztus-kezelő widgetjén (MapInteractiveViewer) vált
|
||||||
|
// ki setState()-et, ami "setState() called during build"
|
||||||
|
// hibát ad. A jelenlegi képkocka VÉGÉRE halasztjuk, ahol ez
|
||||||
|
// már biztonságos — ugyanaz az elv, mint az
|
||||||
|
// onPositionChanged-nél.
|
||||||
|
WidgetsBinding.instance.addPostFrameCallback((_) {
|
||||||
try {
|
try {
|
||||||
mapController.move(animatedPos, mapController.camera.zoom);
|
mapController.move(animatedPos, mapController.camera.zoom);
|
||||||
} catch (_) {}
|
} catch (_) {}
|
||||||
|
});
|
||||||
return const SizedBox.shrink();
|
return const SizedBox.shrink();
|
||||||
},
|
},
|
||||||
),
|
),
|
||||||
@@ -55,9 +64,11 @@ class AnimatedMapFollow extends StatelessWidget {
|
|||||||
duration: duration,
|
duration: duration,
|
||||||
curve: Curves.easeOut,
|
curve: Curves.easeOut,
|
||||||
builder: (context, animatedRotation, _) {
|
builder: (context, animatedRotation, _) {
|
||||||
|
WidgetsBinding.instance.addPostFrameCallback((_) {
|
||||||
try {
|
try {
|
||||||
mapController.rotate(animatedRotation);
|
mapController.rotate(animatedRotation);
|
||||||
} catch (_) {}
|
} catch (_) {}
|
||||||
|
});
|
||||||
return const SizedBox.shrink();
|
return const SizedBox.shrink();
|
||||||
},
|
},
|
||||||
),
|
),
|
||||||
|
|||||||
Reference in New Issue
Block a user