diff --git a/android/app/build.gradle b/android/app/build.gradle
index 124771f..34abfae 100644
--- a/android/app/build.gradle
+++ b/android/app/build.gradle
@@ -36,7 +36,7 @@ android {
applicationId "hu.app_dev.terepi_seged"
// 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.
- minSdkVersion 23
+ minSdkVersion 26
targetSdkVersion 35
versionCode flutter.versionCode
versionName flutter.versionName
diff --git a/android/app/src/main/AndroidManifest.xml b/android/app/src/main/AndroidManifest.xml
index 4a96329..0378876 100644
--- a/android/app/src/main/AndroidManifest.xml
+++ b/android/app/src/main/AndroidManifest.xml
@@ -5,6 +5,11 @@
android:name="${applicationName}"
android:requestLegacyExternalStorage="true"
android:icon="@mipmap/ic_launcher">
+
+
$(FLUTTER_BUILD_NUMBER)
LSRequiresIPhoneOS
+ NSLocationWhenInUseUsageDescription
+ A nyomvonal rögzítéséhez folyamatos helymeghatározás szükséges.
+ NSLocationAlwaysAndWhenInUseUsageDescription
+ Háttérben futó track rögzítéséhez szükséges.
+ UIBackgroundModes
+
+ location
+ fetch
+
UILaunchStoryboardName
LaunchScreen
UIMainStoryboardFile
diff --git a/lib/models/track.dart b/lib/models/track.dart
new file mode 100644
index 0000000..2636a98
--- /dev/null
+++ b/lib/models/track.dart
@@ -0,0 +1,160 @@
+import 'dart:math' as math;
+
+// ─── TrackPoint ───────────────────────────────────────────────────────────────
+
+class TrackPoint {
+ final int? id;
+ final int trackId;
+ final double latitude;
+ final double longitude;
+ final double? altitude;
+ final double? accuracy;
+ final double? speed; // m/s
+ final double? heading;
+ final DateTime timestamp;
+
+ const TrackPoint({
+ this.id,
+ required this.trackId,
+ required this.latitude,
+ required this.longitude,
+ this.altitude,
+ this.accuracy,
+ this.speed,
+ this.heading,
+ required this.timestamp,
+ });
+
+ Map toMap() => {
+ if (id != null) 'id': id,
+ 'track_id': trackId,
+ 'latitude': latitude,
+ 'longitude': longitude,
+ 'altitude': altitude,
+ 'accuracy': accuracy,
+ 'speed': speed,
+ 'heading': heading,
+ 'timestamp': timestamp.toIso8601String(),
+ };
+
+ factory TrackPoint.fromMap(Map m) => TrackPoint(
+ id: m['id'] as int?,
+ trackId: m['track_id'] as int,
+ latitude: m['latitude'] as double,
+ longitude: m['longitude'] as double,
+ altitude: m['altitude'] as double?,
+ accuracy: m['accuracy'] as double?,
+ speed: m['speed'] as double?,
+ heading: m['heading'] as double?,
+ timestamp: DateTime.parse(m['timestamp'] as String),
+ );
+}
+
+// ─── Track státusz ────────────────────────────────────────────────────────────
+
+enum TrackStatus { recording, paused, finished }
+
+// ─── Track ───────────────────────────────────────────────────────────────────
+
+class Track {
+ final int? id;
+ final String name;
+ final DateTime startTime;
+ final DateTime? endTime;
+ final TrackStatus status;
+ final String source; // pl. "Telefon GPS", "BLE GNSS"
+
+ // Statisztikák — ezeket a DB is tárolja a gyors listázáshoz
+ final double distanceMeters;
+ final int pointCount;
+
+ const Track({
+ this.id,
+ required this.name,
+ required this.startTime,
+ this.endTime,
+ this.status = TrackStatus.recording,
+ this.source = 'Telefon GPS',
+ this.distanceMeters = 0,
+ this.pointCount = 0,
+ });
+
+ Track copyWith({
+ int? id,
+ String? name,
+ DateTime? startTime,
+ DateTime? endTime,
+ TrackStatus? status,
+ String? source,
+ double? distanceMeters,
+ int? pointCount,
+ }) =>
+ Track(
+ id: id ?? this.id,
+ name: name ?? this.name,
+ startTime: startTime ?? this.startTime,
+ endTime: endTime ?? this.endTime,
+ status: status ?? this.status,
+ source: source ?? this.source,
+ distanceMeters: distanceMeters ?? this.distanceMeters,
+ pointCount: pointCount ?? this.pointCount,
+ );
+
+ /// Formázott időtartam (óó:pp:mm)
+ String get durationFormatted {
+ final end = endTime ?? DateTime.now();
+ final d = end.difference(startTime);
+ final h = d.inHours.toString().padLeft(2, '0');
+ final m = (d.inMinutes % 60).toString().padLeft(2, '0');
+ final s = (d.inSeconds % 60).toString().padLeft(2, '0');
+ return '$h:$m:$s';
+ }
+
+ /// Formázott távolság
+ String get distanceFormatted {
+ if (distanceMeters < 1000) {
+ return '${distanceMeters.toStringAsFixed(0)} m';
+ }
+ return '${(distanceMeters / 1000).toStringAsFixed(2)} km';
+ }
+
+ Map toMap() => {
+ if (id != null) 'id': id,
+ 'name': name,
+ 'start_time': startTime.toIso8601String(),
+ 'end_time': endTime?.toIso8601String(),
+ 'status': status.name,
+ 'source': source,
+ 'distance_meters': distanceMeters,
+ 'point_count': pointCount,
+ };
+
+ factory Track.fromMap(Map m) => Track(
+ id: m['id'] as int?,
+ name: m['name'] as String,
+ startTime: DateTime.parse(m['start_time'] as String),
+ endTime: m['end_time'] != null
+ ? DateTime.parse(m['end_time'] as String)
+ : null,
+ status: TrackStatus.values.byName(m['status'] as String),
+ source: m['source'] as String? ?? 'Telefon GPS',
+ distanceMeters: (m['distance_meters'] as num?)?.toDouble() ?? 0,
+ pointCount: m['point_count'] as int? ?? 0,
+ );
+}
+
+// ─── Segédszámítás: Haversine távolság ───────────────────────────────────────
+
+double haversineMeters(double lat1, double lon1, double lat2, double lon2) {
+ const r = 6371000.0;
+ final dLat = _toRad(lat2 - lat1);
+ final dLon = _toRad(lon2 - lon1);
+ final a = math.sin(dLat / 2) * math.sin(dLat / 2) +
+ math.cos(_toRad(lat1)) *
+ math.cos(_toRad(lat2)) *
+ math.sin(dLon / 2) *
+ math.sin(dLon / 2);
+ return r * 2 * math.atan2(math.sqrt(a), math.sqrt(1 - a));
+}
+
+double _toRad(double deg) => deg * math.pi / 180;
diff --git a/lib/pages/home/presentation/views/home_view.dart b/lib/pages/home/presentation/views/home_view.dart
index 7f8cbb6..a3ebd13 100644
--- a/lib/pages/home/presentation/views/home_view.dart
+++ b/lib/pages/home/presentation/views/home_view.dart
@@ -175,15 +175,15 @@ class HomeView extends GetView {
iconData: Icons.edit_road,
label: "Track",
onPressed: () async {
- // Get.toNamed("/navigation");
- ScaffoldMessenger.of(context)
- .showSnackBar(const SnackBar(
- content: Text(
- "Fejlesztlés alatt",
- style: TextStyle(fontWeight: FontWeight.bold),
- ),
- backgroundColor: Colors.black54,
- ));
+ Get.toNamed("/tracking");
+ // ScaffoldMessenger.of(context)
+ // .showSnackBar(const SnackBar(
+ // content: Text(
+ // "Fejlesztlés alatt",
+ // style: TextStyle(fontWeight: FontWeight.bold),
+ // ),
+ // backgroundColor: Colors.black54,
+ // ));
},
),
],
diff --git a/lib/pages/map/presentation/controllers/map_controller.dart b/lib/pages/map/presentation/controllers/map_controller.dart
index 8885a68..50d1b0e 100644
--- a/lib/pages/map/presentation/controllers/map_controller.dart
+++ b/lib/pages/map/presentation/controllers/map_controller.dart
@@ -104,6 +104,7 @@ class MapViewController extends GetxController {
late GeoidGrid geoidGrid;
StreamSubscription? _phoneLocationSub;
+ final _phoneLocation = Location();
TextEditingController pointIdController = TextEditingController();
TextEditingController pointDescriptionController = TextEditingController();
@@ -325,30 +326,48 @@ class MapViewController extends GetxController {
_updateCurrentLocationMarker();
if (!gpsIsConnected.value) {
- _startPhoneGps();
+ await _startPhoneGps();
}
}
- void _startPhoneGps() async {
- // Ha már fut, nem indítjuk újra
+ Future _startPhoneGps() async {
if (_phoneLocationSub != null) return;
- final location = Location();
+ // Frissítési beállítások — ezt a location csomag megköveteli
+ await _phoneLocation.changeSettings(
+ accuracy: LocationAccuracy.high,
+ interval: 1000, // ms — másodpercenkénti frissítés
+ distanceFilter: 0, // méter — minden frissítés jöjjön
+ );
- // Engedélyek — már megvan a _getInitialLocation()-ból,
- // de biztonságos újra ellenőrizni
- final permission = await location.hasPermission();
- if (permission == PermissionStatus.denied) return;
+ // Engedély teljes körű ellenőrzése
+ var permission = await _phoneLocation.hasPermission();
+ if (permission == PermissionStatus.denied) {
+ permission = await _phoneLocation.requestPermission();
+ }
+ if (permission != PermissionStatus.granted &&
+ permission != PermissionStatus.grantedLimited) {
+ return;
+ }
- // Folyamatos frissítés indítása
- _phoneLocationSub = location.onLocationChanged.listen((LocationData data) {
+ // GPS szolgáltatás ellenőrzése
+ bool serviceEnabled = await _phoneLocation.serviceEnabled();
+ if (!serviceEnabled) {
+ serviceEnabled = await _phoneLocation.requestService();
+ if (!serviceEnabled) return;
+ }
+
+ print('Phone GPS: stream starting...');
+
+ _phoneLocationSub = _phoneLocation.onLocationChanged.listen((data) {
if (gpsIsConnected.value) {
- // Ha közben csatlakozott a külső GPS — leállítjuk magunkat
_stopPhoneGps();
return;
}
- currentLatitude.value = data.latitude ?? currentLatitude.value;
- currentLongitude.value = data.longitude ?? currentLongitude.value;
+ if (data.latitude == null || data.longitude == null) return;
+
+ currentLatitude.value = data.latitude!;
+ currentLongitude.value = data.longitude!;
_updateCurrentLocationMarker();
});
}
diff --git a/lib/pages/tracking/bindings/tracking_bindings.dart b/lib/pages/tracking/bindings/tracking_bindings.dart
index 7c544ca..5ec4a90 100644
--- a/lib/pages/tracking/bindings/tracking_bindings.dart
+++ b/lib/pages/tracking/bindings/tracking_bindings.dart
@@ -4,6 +4,8 @@ import 'package:terepi_seged/pages/tracking/presentation/controllers/tracking_co
class TrackingBinding extends Bindings {
@override
void dependencies() {
- Get.lazyPut(() => TrackingController());
+ if (!Get.isRegistered()) {
+ Get.put(TrackingController(), permanent: true);
+ }
}
}
diff --git a/lib/pages/tracking/presentation/controllers/tracking_controller.dart b/lib/pages/tracking/presentation/controllers/tracking_controller.dart
index 75a2c7d..7eabedf 100644
--- a/lib/pages/tracking/presentation/controllers/tracking_controller.dart
+++ b/lib/pages/tracking/presentation/controllers/tracking_controller.dart
@@ -1,3 +1,313 @@
+import 'dart:async';
+import 'package:flutter/material.dart';
+import 'package:flutter_foreground_task/flutter_foreground_task.dart';
import 'package:get/get.dart';
+import 'package:intl/intl.dart';
+import 'package:latlong2/latlong.dart';
+import 'package:share_plus/share_plus.dart';
-class TrackingController extends GetxController {}
+import '../../../../services/location_source.dart';
+import '../../../../services/phone_gps_source.dart';
+import '../../../../services/track_database.dart';
+import '../../../../services/gpx_exporter.dart';
+import '../../../../models/track.dart';
+
+// ─── Foreground task handler ─────────────────────────────────────────────────
+// Ez a függvény a háttér-isolate-ban fut. Csak a meglevő pozíció-streamet
+// tartja fenn, a tényleges adatfeldolgozás a főszálban történik.
+
+@pragma('vm:entry-point')
+void startTrackingCallback() {
+ FlutterForegroundTask.setTaskHandler(_TrackingTaskHandler());
+}
+
+class _TrackingTaskHandler extends TaskHandler {
+ @override
+ Future onStart(DateTime timestamp, TaskStarter starter) async {}
+
+ @override
+ void onRepeatEvent(DateTime timestamp) {
+ // A Geolocator stream a főizoláton folyik tovább, itt nincs teendő.
+ }
+
+ @override
+ Future onDestroy(DateTime timestamp, bool isTimeout) async {}
+}
+
+// ─── TrackingController ──────────────────────────────────────────────────────
+
+class TrackingController extends GetxController {
+ // ── Állapot ────────────────────────────────────────────────────────────────
+ final isRecording = false.obs;
+ final isPaused = false.obs;
+ final currentTrack = Rxn