MobilApp/lib/pages/map_survey/presentations/controllers/map_survey_controller.dart

714 lines
27 KiB
Dart
Raw Normal View History

import 'dart:async';
import 'dart:convert';
import 'dart:io';
import 'dart:math';
import 'package:file_picker/file_picker.dart';
import 'package:flutter/material.dart';
import 'package:flutter/services.dart';
import 'package:flutter_map/flutter_map.dart';
// import 'package:flutter_map_geojson/flutter_map_geojson.dart';
import 'package:flutter_map_polywidget/flutter_map_polywidget.dart';
import 'package:geolocator/geolocator.dart';
import 'package:get/get.dart';
import 'package:intl/intl.dart';
// import 'package:location/location.dart';
import 'package:latlong2/latlong.dart';
import 'package:path_provider/path_provider.dart';
import 'package:share_plus/share_plus.dart';
import 'package:supabase_flutter/supabase_flutter.dart';
import 'package:terepi_seged/controls/geoid_grid.dart';
import 'package:terepi_seged/eov/convert_coordinate.dart';
import 'package:terepi_seged/eov/eov.dart';
import 'package:terepi_seged/models/point_to_measure.dart';
import 'package:terepi_seged/models/point_with_description_model.dart';
import 'package:shared_preferences/shared_preferences.dart';
import 'package:terepi_seged/pages/map_survey/presentations/views/measured_points_table_dialog.dart';
import 'package:terepi_seged/services/coord_converter_service.dart';
import 'package:terepi_seged/services/gnss/gnss_connection.dart';
import 'package:terepi_seged/services/gnss/gnss_device_service.dart';
import 'package:terepi_seged/services/gnss/gnss_service.dart';
import 'package:terepi_seged/services/ntrip_service.dart';
enum MapSurveyMode {
measure, // Bemérés — ahol vagyok, azt rögzítem
stakeout, // Kitűzés — adott ponthoz navigálok, majd rögzítem
}
class MapSurveyController extends GetxController {
static MapSurveyController get to => Get.find();
// ── Függőségek (service-ek) ───────────────────────────────────────
GnssService get _gnss => GnssService.to;
NtripService get _ntrip => NtripService.to;
final mode = MapSurveyMode.measure.obs;
final targetName = ''.obs;
final targetEovX = 0.0.obs;
final targetEovY = 0.0.obs;
double get deltaY => eov.value.Y - targetEovY.value;
double get deltaX => eov.value.X - targetEovX.value;
double get distanceToTarget => sqrt(deltaX * deltaX + deltaY * deltaY);
bool get isOnTarget => distanceToTarget < 0.05;
// ── GPS állapot — rövidítések a GnssService-hez ──────────────────
// Ezeket a view-ban közvetlenül a service-ből is lehetne olvasni,
// de a controller-en keresztül is hozzáférhetők a meglévő view kódhoz.
RxDouble get gpsLatitude => _gnss.latitude;
RxDouble get gpsLongitude => _gnss.longitude;
RxDouble get gpsAltitude => _gnss.altitude;
RxDouble get gpsGeoidSeparation => _gnss.geoidSeparation;
RxInt get gpsQuality => _gnss.gpsQuality;
RxDouble get gpsLatitudeError => _gnss.latitudeError;
RxDouble get gpsLongitudeError => _gnss.longitudeError;
RxDouble get gpsAltitudeError => _gnss.altitudeError;
Rx<DateTime> get gpsDateTime => _gnss.gpsDateTime;
RxBool get gpsIsConnected =>
(_gnss.connectionState.value == GnssConnectionState.connected).obs;
// NTRIP állapot
RxBool get ntripIsConnected => _ntrip.isConnected;
RxInt get ntripDataPacketNumbers => _ntrip.packetCount;
RxInt get ggaSenDataPacketNumber => _ntrip.ggaSentCount;
RxString get ggaSendLastTimeStr => _ntrip.ggaLastSentTime;
RxInt get ntripReceivedData => _ntrip.receivedBytes;
// ── Számformátumok ────────────────────────────────────────────────
final formatEov = NumberFormat('##0,000.0', 'hu-HU');
final formatEovZ = NumberFormat('###0.0', 'hu-HU');
final formatAltitudeError = NumberFormat('####0.000', 'hu-HU');
final formatEovForFile = NumberFormat('#####0.0', 'hu-HU');
final formatWgs84Sec = NumberFormat('00.000', 'hu-HU');
// ── EOV koordináták (számítottak) ─────────────────────────────────
Rx<Eov> eov = Eov(0, 0).obs;
Rx<double?> eovHeight = (0.0 as double?).obs;
// DMS formátum
RxInt latDegree = 0.obs;
RxInt latMin = 0.obs;
RxDouble latSec = 0.0.obs;
RxInt longDegree = 0.obs;
RxInt longMin = 0.obs;
RxDouble longSec = 0.0.obs;
// ── Térkép állapot ────────────────────────────────────────────────
static const double maxZoomValue = 25.0;
RxDouble currentLongitude = 0.0.obs;
RxDouble currentLatitude = 0.0.obs;
RxDouble currentZoom = 12.0.obs;
RxBool isMapMoveToCenter = true.obs;
RxBool mapIsInitialized = false.obs;
late final MapController mapController;
final currentLocationMarker = <Marker>[];
final pointNotesMarker = <Marker>[];
final pointsToMeasureMarker = <Marker>[];
final pointsToMeasureLabel = <PolyWidget>[];
final pointsToMeasureDropDownMenuItem = <DropdownMenuItem<int>>[];
// ── Pont adatok ───────────────────────────────────────────────────
List<PointToMeasure> pointsToMeasure = [];
List<PointWithDescription> pointWithDescriptionList = [];
RxInt pointsToMeasureSelectedValue = (-1).obs;
RxDouble distance = 0.0.obs;
int pointId = 1;
String pointIdPrefix = '';
String pointIdPostfix = '';
Rx<bool> pointMeasuringDirectionForward = true.obs;
// ── Geoid grid ────────────────────────────────────────────────────
late GeoidGrid geoidGrid;
// ── Fájlok ────────────────────────────────────────────────────────
late Directory? directory;
late File dataFile;
// ── Telefon GPS fallback ──────────────────────────────────────────
StreamSubscription<Position>? _phoneLocationSub;
2026-05-19 00:10:41 +02:00
StreamSubscription<void>? _gnssUpdateSub;
// ── UI controllerek ───────────────────────────────────────────────
final pointIdController = TextEditingController();
final pointDescriptionController = TextEditingController();
final gpsHeightController = TextEditingController();
final pointPrefixController = TextEditingController();
final pointPostfixController = TextEditingController();
late SharedPreferences prefs;
Rx<bool> isShowPassword = false.obs;
// ── Supabase ──────────────────────────────────────────────────────
RealtimeChannel? _supaChannel;
// ─────────────────────────────────────────────────────────────────
// Lifecycle
// ─────────────────────────────────────────────────────────────────
@override
void onInit() async {
super.onInit();
prefs = await SharedPreferences.getInstance();
geoidGrid = await GeoidGrid.load('assets/Grids/geoid_eht2014.gtx');
mapController = MapController();
// ── NTRIP RTCM adat → GNSS vevő ──────────────────────────────
NtripService.to.onRtcmData = (data) {
GnssService.to.sendToReceiver(data);
};
// ── GnssService pozíció változás → EOV, marker, NTRIP GGA ────
2026-05-19 00:10:41 +02:00
_gnssUpdateSub = _gnss.onDataUpdated.listen((_) {
_onGnssUpdate();
});
// ── Supabase realtime ─────────────────────────────────────────
_supaChannel = Supabase.instance.client
.channel('public:TerepiSeged_Receiver')
.onPostgresChanges(
event: PostgresChangeEvent.update,
schema: 'public',
table: 'TerepiSeged_Receiver',
callback: (payload) {
final id = payload.newRecord['pointNumber'] as int?;
if (id != null) updatePointStatus(id);
},
)
.subscribe();
mapIsInitialized.value = true;
}
@override
void onReady() async {
super.onReady();
await _initStorage();
gpsHeightController.text = '1.8';
}
@override
void onClose() async {
super.onClose();
_phoneLocationSub?.cancel();
2026-05-19 00:10:41 +02:00
_gnssUpdateSub?.cancel();
await _supaChannel?.unsubscribe();
pointIdController.dispose();
pointDescriptionController.dispose();
gpsHeightController.dispose();
pointPrefixController.dispose();
pointPostfixController.dispose();
}
// ─────────────────────────────────────────────────────────────────
// GnssService frissítés kezelése
// ─────────────────────────────────────────────────────────────────
void _onGnssUpdate() {
if (!_gnss.hasValidData) return;
final lat = _gnss.latitude.value;
final lon = _gnss.longitude.value;
final alt = _gnss.altitude.value;
final sep = _gnss.geoidSeparation.value;
// EOV konverzió
eov.value = ConvertCoordinate.ConvertWgsToEov(lat, lon);
eovHeight.value = geoidGrid.toEovHeight(lat, lon, alt, sep);
// DMS
latDegree.value = ConvertCoordinate.toDegree(lat);
latMin.value = ConvertCoordinate.toMinute(lat);
latSec.value = ConvertCoordinate.toSecond(lat);
longDegree.value = ConvertCoordinate.toDegree(lon);
longMin.value = ConvertCoordinate.toMinute(lon);
longSec.value = ConvertCoordinate.toSecond(lon);
// Távolság a kiválasztott ponthoz
if (pointsToMeasureSelectedValue.value >= 0 &&
pointsToMeasureSelectedValue.value < pointsToMeasure.length) {
final pt = pointsToMeasure[pointsToMeasureSelectedValue.value];
final wgs = CoordConverterService.to.eovToWgsPoint(pt.coordX, pt.coordY);
distance.value = calculateDistance(
LatLng(lat, lon),
LatLng(wgs.y, wgs.x),
);
}
// Térkép pozíció
currentLatitude.value = lat;
currentLongitude.value = lon;
_updateCurrentLocationMarker();
// NTRIP GGA küldés
NtripService.to.onGgaReceived(
_gnss.lastGgaLine.value,
_gnss.utcFix.value,
);
}
// ─────────────────────────────────────────────────────────────────
// Telefon GPS fallback
// ─────────────────────────────────────────────────────────────────
// ─────────────────────────────────────────────────────────────────
// Térkép vezérlők
// ─────────────────────────────────────────────────────────────────
void mapZoomIn() {
if (currentZoom.value >= maxZoomValue) return;
currentZoom.value++;
_moveMap();
}
void mapZoomOut() {
if (currentZoom.value <= 0) return;
currentZoom.value--;
_moveMap();
}
void setIsMapMoveToCenter() =>
isMapMoveToCenter.value = !isMapMoveToCenter.value;
void _moveMap() {
final lat = isMapMoveToCenter.value
? currentLatitude.value
: mapController.camera.center.latitude;
final lon = isMapMoveToCenter.value
? currentLongitude.value
: mapController.camera.center.longitude;
mapController.move(LatLng(lat, lon), currentZoom.value);
}
void _updateCurrentLocationMarker() {
currentLocationMarker.clear();
currentLocationMarker.add(Marker(
point: LatLng(currentLatitude.value, currentLongitude.value),
width: 15.0,
height: 15.0,
child: Container(
width: 15.0,
height: 15.0,
decoration: BoxDecoration(
color: getCurrentLocationMarkerColor(_gnss.gpsQuality.value),
shape: BoxShape.circle,
border: Border.all(width: 1.5, color: Colors.white),
),
),
));
if (isMapMoveToCenter.value) {
mapController.move(
LatLng(currentLatitude.value, currentLongitude.value),
currentZoom.value,
);
}
}
Color getCurrentLocationMarkerColor(int quality) => switch (quality) {
0 => Colors.black,
1 => Colors.red,
2 => Colors.blue,
4 => Colors.green,
5 => Colors.orange,
6 => Colors.yellow,
_ => Colors.white,
};
String getGpsQualityIndicator({required int quality}) => switch (quality) {
0 => 'Invalid',
1 => 'Standard GPS',
2 => 'Differential GPS',
4 => 'RTK Fix',
5 => 'RTK Float',
6 => 'Estimated (DR)',
_ => 'Invalid',
};
// ─────────────────────────────────────────────────────────────────
// Pont mentés dialóg
// ─────────────────────────────────────────────────────────────────
void showAddPointDialog() => onBottomNavigationBarTap(0);
void onBottomNavigationBarTap(int index) async {
if (index != 0) return;
if (pointsToMeasureSelectedValue.value >= 0 &&
pointsToMeasureSelectedValue.value < pointsToMeasure.length) {
pointIdController.text =
'${pointsToMeasure[pointsToMeasureSelectedValue.value].id}';
} else {
pointIdController.text = pointId.toString();
}
pointDescriptionController.text = '';
Get.dialog(
AlertDialog(
title: const Text('Pont rögzítése'),
content: Column(
mainAxisSize: MainAxisSize.min,
children: [
TextField(
controller: pointIdController,
autofocus: true,
keyboardType: TextInputType.number,
decoration: const InputDecoration(
border: OutlineInputBorder(), labelText: 'Azonosító'),
),
const SizedBox(height: 20),
TextField(
controller: pointDescriptionController,
decoration: const InputDecoration(
border: OutlineInputBorder(), labelText: 'Leírás'),
),
const SizedBox(height: 20),
TextField(
controller: gpsHeightController,
keyboardType: TextInputType.number,
decoration: const InputDecoration(
border: OutlineInputBorder(), labelText: 'GPS magasság'),
),
],
),
actions: [
Row(
mainAxisAlignment: MainAxisAlignment.spaceEvenly,
children: [
OutlinedButton(
style:
OutlinedButton.styleFrom(minimumSize: const Size(120, 40)),
onPressed: () => Get.back(),
child:
const Text('Mégsem', style: TextStyle(color: Colors.red)),
),
OutlinedButton(
style:
OutlinedButton.styleFrom(minimumSize: const Size(120, 40)),
onPressed: _saveCurrentPoint,
child: const Text('Ment',
style: TextStyle(
color: Colors.green, fontWeight: FontWeight.bold)),
),
],
),
],
),
barrierDismissible: false,
);
}
Future<void> _saveCurrentPoint() async {
pointId = int.tryParse(pointIdController.text) ?? pointId;
// Helyi lista
pointWithDescriptionList.add(PointWithDescription(
pointId,
_gnss.gpsDateTime.value,
pointDescriptionController.text,
eov.value.Y,
eov.value.X,
_gnss.latitude.value,
_gnss.longitude.value,
max(_gnss.latitudeError.value, _gnss.longitudeError.value),
_gnss.altitudeError.value,
));
// Marker hozzáadása
pointNotesMarker.add(Marker(
point: LatLng(_gnss.latitude.value, _gnss.longitude.value),
width: 15.0,
height: 15.0,
child: Container(
width: 15.0,
height: 15.0,
decoration: BoxDecoration(
color: Colors.amber[700],
shape: BoxShape.circle,
border: Border.all(width: 1.0, color: Colors.black),
),
),
));
// Fájl mentés
await dataFile.writeAsString(
'$pointId;${_gnss.gpsDateTime.value};'
'${pointDescriptionController.text};'
'${formatEovForFile.format(eov.value.Y)};'
'${formatEovForFile.format(eov.value.X)};'
'${_gnss.latitude.value};${_gnss.longitude.value};'
'${_gnss.altitude.value};'
'${max(_gnss.latitudeError.value, _gnss.longitudeError.value)};'
'${_gnss.altitudeError.value};'
'${gpsHeightController.text}\r\n',
mode: FileMode.append,
);
// Supabase mentés
await Supabase.instance.client.from('TerepiSeged_MeasuredPoints').insert({
'pointNumber': pointId,
'gnssNumber': GnssDeviceService.to.selectedDevice.value?.name ?? '',
'latitude': _gnss.latitude.value,
'longitude': _gnss.longitude.value,
'altitude': _gnss.altitude.value,
'heightOfGeoid': _gnss.geoidSeparation.value,
'eovX': eov.value.X,
'eovY': eov.value.Y,
'poleHeight': double.tryParse(gpsHeightController.text),
'horizontalError':
max(_gnss.latitudeError.value, _gnss.longitudeError.value),
'verticalError': _gnss.altitudeError.value,
'description': pointDescriptionController.text,
'isDeleted': false,
'projectId': 2,
});
await Supabase.instance.client
.from('TerepiSeged_Receiver')
.update({'isMeasured': true}).eq('pointNumber', pointId);
// Következő pont léptetése
_advancePointSelection();
Get.back();
}
void _advancePointSelection() {
final len = pointsToMeasure.length;
if (len == 0) return;
if (pointMeasuringDirectionForward.isTrue) {
if (pointsToMeasureSelectedValue.value < len - 1) {
pointId++;
pointsToMeasureSelectedValue.value++;
} else {
_showNoMorePoints();
}
} else {
if (pointsToMeasureSelectedValue.value > 0) {
pointId--;
pointsToMeasureSelectedValue.value--;
} else {
_showNoMorePoints();
}
}
}
void _showNoMorePoints() {
ScaffoldMessenger.of(Get.context!).showSnackBar(
const SnackBar(content: Text('Nincs több bemérendő pont.')),
);
}
// ─────────────────────────────────────────────────────────────────
// Pont betöltés
// ─────────────────────────────────────────────────────────────────
void ReadPointsFromFile() async {
final result = await FilePicker.platform.pickFiles();
if (result == null) return;
final file = File(result.files.single.path!);
if (!await file.exists()) return;
_clearPoints();
final content = await file.readAsLines();
for (final (index, item) in content.indexed) {
if (index == 0) continue;
_addPointFromCsv(item, index - 1);
}
if (pointsToMeasure.isNotEmpty) {
pointsToMeasureSelectedValue.value = 0;
}
}
void readPointsFromSupa() async {
final data =
await Supabase.instance.client.from('TerepiSeged_Receiver').select();
_clearPoints();
for (int i = 0; i < data.length; i++) {
final item = data[i];
final id = item['pointNumber'];
final coordX = (item['eovX'] as num?)?.toDouble();
final coordY = (item['eovY'] as num?)?.toDouble();
if (id == null || coordX == null || coordY == null) continue;
_addPoint(id: id, coordX: coordX, coordY: coordY, listIndex: i);
}
if (pointsToMeasure.isNotEmpty) {
pointsToMeasureSelectedValue.value = 0;
}
}
void _clearPoints() {
pointsToMeasure.clear();
pointsToMeasureMarker.clear();
pointsToMeasureLabel.clear();
pointsToMeasureDropDownMenuItem.clear();
}
void _addPointFromCsv(String line, int listIndex) {
final parts = line.split(';');
if (parts.length < 3) return;
final id = int.tryParse(parts[0]);
final coordX = double.tryParse(parts[1].replaceAll(',', '.'));
final coordY = double.tryParse(parts[2].replaceAll(',', '.'));
if (id == null || coordX == null || coordY == null) return;
_addPoint(id: id, coordX: coordX, coordY: coordY, listIndex: listIndex);
}
void _addPoint({
required int id,
required double coordX,
required double coordY,
required int listIndex,
}) {
pointsToMeasure.add(PointToMeasure(id: id, coordX: coordX, coordY: coordY));
final wgs = CoordConverterService.to.eovToWgsPoint(coordX, coordY);
pointsToMeasureMarker.add(Marker(
point: LatLng(wgs.y, wgs.x),
width: 15.0,
height: 15.0,
child: Container(
width: 15.0,
height: 15.0,
decoration: BoxDecoration(
color: Colors.purple,
shape: BoxShape.circle,
border: Border.all(width: 1.0, color: Colors.black),
),
),
));
pointsToMeasureLabel.add(PolyWidget(
center: LatLng(wgs.y + 0.0000075, wgs.x + 0.0000075),
widthInMeters: 3,
heightInMeters: 3,
child: FittedBox(
child: Padding(
padding: const EdgeInsets.all(4),
child: Text(
' $id ',
style: const TextStyle(
fontWeight: FontWeight.bold,
color: Colors.yellow,
fontSize: 12,
),
),
),
),
));
pointsToMeasureDropDownMenuItem.add(DropdownMenuItem<int>(
value: listIndex,
child: Text('$id'),
));
}
void pointsToMeasureSelectedValueChanged(int value) =>
pointsToMeasureSelectedValue.value = value;
// ─────────────────────────────────────────────────────────────────
// Segédmetódusok
// ─────────────────────────────────────────────────────────────────
double calculateDistance(LatLng start, LatLng end) {
const r = 6371.0;
final lat1 = start.latitude * (pi / 180);
final lon1 = start.longitude * (pi / 180);
final lat2 = end.latitude * (pi / 180);
final lon2 = end.longitude * (pi / 180);
final dLat = lat2 - lat1;
final dLon = lon2 - lon1;
final a = sin(dLat / 2) * sin(dLat / 2) +
cos(lat1) * cos(lat2) * sin(dLon / 2) * sin(dLon / 2);
return r * 2 * atan2(sqrt(a), sqrt(1 - a));
}
void updatePointStatus(int pointId) {}
void showMeasuredPointsTableDialog() =>
Get.to(() => MeasuredPointsTableDialog(), transition: Transition.fadeIn);
// ─────────────────────────────────────────────────────────────────
// Tárhely inicializálás
// ─────────────────────────────────────────────────────────────────
Future<void> _initStorage() async {
directory = await getExternalStorageDirectory();
if (directory != null && !await directory!.exists()) {
await directory!.create(recursive: true);
}
dataFile = File('${directory!.path}/data.txt');
if (!await dataFile.exists()) {
await dataFile.writeAsString(
'Id;DateTime;Description;EovX;EovY;Latitude;Longitude;Altitude;Hor.Err;Vert.Err\r\n',
);
}
}
// ─────────────────────────────────────────────────────────────────
// Export
// ─────────────────────────────────────────────────────────────────
Future<List> readMeasuredPoints() async => Supabase.instance.client
.from('TerepiSeged_MeasuredPoints')
.select()
.eq('projectId', 2)
.order('created_at');
void SaveMeasuredPointsToFile() async {
final dir = await getApplicationDocumentsDirectory();
final file = File('${dir.path}/measuredsPoints.csv');
if (await file.exists()) await file.delete();
await file.create();
await file.writeAsString(
'Id;DateTime;Description;EovX;EovY;Altitude;Hor.Err;Vert.Err\r\n');
final data = await readMeasuredPoints();
for (final d in data) {
file.writeAsStringSync(
'${d['id']};${d['created_at']};${d['description']};'
'${formatEov.format(d['eovY'])};${formatEov.format(d['eovX'])};'
'${formatEovZ.format((d['altitude'] as num) - (d['poleHeight'] as num))};'
'${formatAltitudeError.format(d['horizontalError'])};'
'${formatAltitudeError.format(d['verticalError'])}\r\n',
mode: FileMode.append,
encoding: utf8,
);
}
await SharePlus.instance.share(ShareParams(
text: 'Mérési eredmények',
files: [XFile('${dir.path}/measuredsPoints.csv')],
subject: 'Mérési eredmények',
));
}
void switchMode(MapSurveyMode newMode) {
mode.value = newMode;
if (newMode == MapSurveyMode.measure) {
targetName.value = '';
targetEovY.value = 0;
targetEovX.value = 0;
}
}
void setTarget(String name, double y, double x) {
targetName.value = name;
targetEovY.value = y;
targetEovX.value = x;
mode.value = MapSurveyMode.stakeout;
}
}