diff --git a/GETX_GEOLOCATION_README.md b/GETX_GEOLOCATION_README.md new file mode 100644 index 0000000..2792c25 --- /dev/null +++ b/GETX_GEOLOCATION_README.md @@ -0,0 +1,222 @@ +# GetX Geolocation Implementation + +Diese Implementation zeigt, wie die Geolocation-Funktionalität mit GetX State Management umgesetzt wird. + +## 📂 Struktur + +```struct +lib/ +├── controllers/ +│ ├── geolocation_controller.dart # Basis Controller +│ └── geolocation_advanced_controller.dart # Erweiterte Controller mit Statistiken +├── bindings/ +│ └── geolocation_binding.dart # Dependency Injection +├── routes/ +│ └── app_routes.dart # Navigation & Routen +├── pages/examples/ +│ └── geolocation_example.dart # GetX View (UI) +├── services/ +│ └── geolocation.dart # Static Service Layer +└── utils/ + └── geo_utils.dart # Geo Utility Funktionen +``` + +## 🎯 Features + +### GeolocationController (Basis) + +- ✅ Reactive State Management mit GetX +- ✅ Einmalige Positionsabfrage +- ✅ Kontinuierliches Position Tracking +- ✅ Permission Management +- ✅ Automatische Snackbar Notifications +- ✅ Loading States +- ✅ Error Handling + +### GeolocationAdvancedController (Erweitert) + +- ✅ Alle Basis-Features +- ✅ Tracking-Statistiken (Distanz, Geschwindigkeit) +- ✅ Position History +- ✅ Track Duration Timer +- ✅ Data Export Funktionalität +- ✅ Track Center & Bounds Berechnung + +## 🔧 Verwendung + +### 1. Basis Controller verwenden + +```dart +class MyPage extends StatelessWidget { + @override + Widget build(BuildContext context) { + final controller = Get.put(GeolocationController()); + + return Scaffold( + body: Obx(() => Column( + children: [ + Text(controller.statusText), + if (controller.hasPosition) + Text('Lat: ${controller.currentPosition!.latitude}'), + ElevatedButton( + onPressed: controller.getCurrentPosition, + child: Text('Position abrufen'), + ), + ], + )), + ); + } +} +``` + +### 2. Mit Bindings (Empfohlen) + +```dart +// In app_routes.dart +GetPage( + name: '/geolocation', + page: () => GeolocationExample(), + binding: GeolocationBinding(), +), + +// Navigation +Get.toNamed('/geolocation'); +``` + +### 3. Controller Methoden + +```dart +final controller = Get.find(); + +// Position abrufen +await controller.getCurrentPosition(); + +// Tracking starten/stoppen +await controller.toggleTracking(); + +// Permissions prüfen +await controller.checkPermissions(); + +// Einstellungen öffnen +await controller.openLocationSettings(); +await controller.openAppSettings(); + +// Reset +controller.reset(); +``` + +## 📱 UI Features + +### Reactive UI Components + +- Status Card mit Loading-Indikator +- Position Details Card (nur wenn Position verfügbar) +- Tracking Badge +- Icon-basierte Action Buttons +- Responsive Layout + +### Beispiel Obx() Usage + +```dart +Obx(() => controller.isLoading + ? CircularProgressIndicator() + : Text(controller.statusText) +) +``` + +## 🎨 GetX Pattern + +### Controller Pattern + +```dart +class GeolocationController extends GetxController { + // Reactive Variables + final _isTracking = false.obs; + + // Getter + bool get isTracking => _isTracking.value; + + // Methods + void startTracking() { + _isTracking.value = true; + } +} +``` + +### View Pattern + +```dart +class GeolocationView extends StatelessWidget { + @override + Widget build(BuildContext context) { + final controller = Get.put(GeolocationController()); + + return Obx(() => + // Reactive UI hier + ); + } +} +``` + +## 🚀 Migration von StatefulWidget + +### Vorher (StatefulWidget) + +```dart +class _MyPageState extends State { + bool _isTracking = false; + + void _startTracking() { + setState(() { + _isTracking = true; + }); + } +} +``` + +### Nachher (GetX) + +```dart +class MyController extends GetxController { + final _isTracking = false.obs; + bool get isTracking => _isTracking.value; + + void startTracking() { + _isTracking.value = true; // Automatisches UI Update + } +} +``` + +## 📋 Vorteile der GetX Implementation + +1. **Weniger Boilerplate**: Kein setState() nötig +2. **Automatische Lifecycle**: Controller wird automatisch disposed +3. **Reactive UI**: Obx() updated automatisch bei Änderungen +4. **Dependency Injection**: Saubere Trennung von Logic und UI +5. **Navigation**: Einfache Navigation mit Get.to() +6. **Snackbars**: Integrierte Notification System +7. **Performance**: Nur betroffene Widgets werden rebuilt + +## 🔄 Lifecycle + +```dart +onInit() → onReady() → onClose() +``` + +- `onInit()`: Initialisierung +- `onReady()`: Nach dem ersten build +- `onClose()`: Cleanup (automatisch) + +## 📝 Beispiel Integration + +```dart +// main.dart +void main() { + runApp(GetMaterialApp( + home: GeolocationExample(), + initialBinding: GeolocationBinding(), + )); +} +``` + +Die GetX Implementation bietet eine moderne, reaktive und performante Lösung für Geolocation State Management in Flutter! diff --git a/lib/bindings/geolocation_binding.dart b/lib/bindings/geolocation_binding.dart new file mode 100644 index 0000000..5b10c25 --- /dev/null +++ b/lib/bindings/geolocation_binding.dart @@ -0,0 +1,20 @@ +import 'package:get/get.dart'; +import '../controllers/geolocation_controller.dart'; + +/// GetX Binding für Geolocation Dependencies +class GeolocationBinding extends Bindings { + @override + void dependencies() { + // Lazy Loading - Controller wird erst erstellt wenn benötigt + Get.lazyPut(() => GeolocationController()); + } +} + +/// Alternative: Permanent Binding für App-weite Nutzung +class GeolocationPermanentBinding extends Bindings { + @override + void dependencies() { + // Permanent - Controller bleibt im Speicher + Get.put(GeolocationController(), permanent: true); + } +} \ No newline at end of file diff --git a/lib/controllers/geolocation_advanced_controller.dart b/lib/controllers/geolocation_advanced_controller.dart new file mode 100644 index 0000000..63d68e5 --- /dev/null +++ b/lib/controllers/geolocation_advanced_controller.dart @@ -0,0 +1,354 @@ +import 'package:get/get.dart'; +import 'package:geolocator/geolocator.dart'; +import '../services/geolocation.dart'; +import '../utils/geo_utils.dart'; + +/// Erweiterte Geolocation Controller Klasse mit zusätzlichen Features +class GeolocationAdvancedController extends GetxController { + // Reactive Variablen + final _currentPosition = Rxn(); + final _previousPosition = Rxn(); + final _statusText = 'Noch keine Position ermittelt'.obs; + final _isTracking = false.obs; + final _isLoading = false.obs; + final _totalDistance = 0.0.obs; + final _averageSpeed = 0.0.obs; + final _maxSpeed = 0.0.obs; + final _trackingDuration = 0.obs; + final _positionHistory = [].obs; + + // Timer für Tracking-Dauer + DateTime? _trackingStartTime; + + // Getter für reactive Variablen + Position? get currentPosition => _currentPosition.value; + Position? get previousPosition => _previousPosition.value; + String get statusText => _statusText.value; + bool get isTracking => _isTracking.value; + bool get isLoading => _isLoading.value; + double get totalDistance => _totalDistance.value; + double get averageSpeed => _averageSpeed.value; + double get maxSpeed => _maxSpeed.value; + int get trackingDuration => _trackingDuration.value; + List get positionHistory => _positionHistory; + + @override + void onClose() { + stopTracking(); + super.onClose(); + } + + /// Holt die aktuelle Position einmalig + Future getCurrentPosition() async { + try { + _isLoading.value = true; + _statusText.value = 'Position wird ermittelt...'; + + Position? position = await GeolocationService.getCurrentPosition(); + + if (position != null) { + _updatePosition(position); + _statusText.value = GeolocationService.positionToString(position); + + Get.snackbar( + 'Position ermittelt', + 'Genauigkeit: ${position.accuracy.toStringAsFixed(1)}m', + snackPosition: SnackPosition.BOTTOM, + ); + } else { + _statusText.value = 'Position konnte nicht ermittelt werden'; + Get.snackbar( + 'Fehler', + 'Position konnte nicht ermittelt werden', + snackPosition: SnackPosition.BOTTOM, + ); + } + } catch (e) { + _statusText.value = 'Fehler beim Abrufen der Position: $e'; + Get.snackbar( + 'Fehler', + 'Unerwarteter Fehler: $e', + snackPosition: SnackPosition.BOTTOM, + ); + } finally { + _isLoading.value = false; + } + } + + /// Startet kontinuierliches Position Tracking mit Statistiken + Future startTracking() async { + if (_isTracking.value) return; + + try { + _isLoading.value = true; + _statusText.value = 'Tracking wird gestartet...'; + + // Reset Statistiken + _resetTrackingStats(); + _trackingStartTime = DateTime.now(); + + bool success = await GeolocationService.startPositionStream( + onPositionChanged: (Position position) { + _updatePosition(position); + _updateTrackingStats(position); + _statusText.value = _buildTrackingStatusText(position); + }, + onError: (String error) { + _statusText.value = 'Tracking Fehler: $error'; + _isTracking.value = false; + Get.snackbar( + 'Tracking Fehler', + error, + snackPosition: SnackPosition.BOTTOM, + ); + }, + accuracy: LocationAccuracy.high, + distanceFilter: 3, // Updates alle 3 Meter für genauere Statistiken + ); + + if (success) { + _isTracking.value = true; + _statusText.value = 'Tracking aktiv - Warten auf erste Position...'; + + // Starte Timer für Tracking-Dauer + _startDurationTimer(); + + Get.snackbar( + 'Tracking gestartet', + 'Sammle GPS-Daten und Statistiken...', + snackPosition: SnackPosition.BOTTOM, + ); + } else { + _statusText.value = 'Tracking konnte nicht gestartet werden'; + Get.snackbar( + 'Fehler', + 'Tracking konnte nicht gestartet werden', + snackPosition: SnackPosition.BOTTOM, + ); + } + } catch (e) { + _statusText.value = 'Fehler beim Starten des Trackings: $e'; + Get.snackbar( + 'Fehler', + 'Tracking-Fehler: $e', + snackPosition: SnackPosition.BOTTOM, + ); + } finally { + _isLoading.value = false; + } + } + + /// Stoppt das Position Tracking + Future stopTracking() async { + if (!_isTracking.value) return; + + try { + await GeolocationService.stopPositionStream(); + _isTracking.value = false; + _trackingStartTime = null; + + String summary = _buildTrackingSummary(); + _statusText.value = 'Tracking gestoppt\n$summary'; + + Get.snackbar( + 'Tracking gestoppt', + summary, + snackPosition: SnackPosition.BOTTOM, + duration: const Duration(seconds: 5), + ); + } catch (e) { + Get.snackbar( + 'Fehler', + 'Fehler beim Stoppen des Trackings: $e', + snackPosition: SnackPosition.BOTTOM, + ); + } + } + + /// Prüft Permission Status + Future checkPermissions() async { + try { + _isLoading.value = true; + _statusText.value = 'Berechtigungen werden geprüft...'; + + LocationPermission permission = await GeolocationService.checkPermission(); + bool serviceEnabled = await GeolocationService.isLocationServiceEnabled(); + + _statusText.value = 'Service aktiv: $serviceEnabled\n' + 'Berechtigung: ${LocationPermissionHelper.getPermissionStatusText(permission)}'; + + String permissionStatus = LocationPermissionHelper.getPermissionStatusText(permission); + Get.snackbar( + 'Berechtigungs-Status', + 'Location Service: ${serviceEnabled ? "Aktiv" : "Inaktiv"}\n$permissionStatus', + snackPosition: SnackPosition.BOTTOM, + duration: const Duration(seconds: 4), + ); + } catch (e) { + _statusText.value = 'Fehler beim Prüfen der Berechtigungen: $e'; + Get.snackbar( + 'Fehler', + 'Berechtigungen konnten nicht geprüft werden: $e', + snackPosition: SnackPosition.BOTTOM, + ); + } finally { + _isLoading.value = false; + } + } + + /// Private Methoden + + void _updatePosition(Position position) { + _previousPosition.value = _currentPosition.value; + _currentPosition.value = position; + _positionHistory.add(position); + } + + void _updateTrackingStats(Position position) { + if (_previousPosition.value != null) { + // Berechne Distanz zur vorherigen Position + double distance = GeoUtils.calculateDistanceKm( + startLat: _previousPosition.value!.latitude, + startLng: _previousPosition.value!.longitude, + endLat: position.latitude, + endLng: position.longitude, + ); + + _totalDistance.value += distance; + + // Aktualisiere max Geschwindigkeit + double currentSpeedKmh = GeoUtils.mpsToKmh(position.speed); + if (currentSpeedKmh > _maxSpeed.value) { + _maxSpeed.value = currentSpeedKmh; + } + + // Berechne Durchschnittsgeschwindigkeit + if (_trackingStartTime != null) { + double hours = DateTime.now().difference(_trackingStartTime!).inMinutes / 60.0; + if (hours > 0) { + _averageSpeed.value = _totalDistance.value / hours; + } + } + } + } + + void _resetTrackingStats() { + _totalDistance.value = 0.0; + _averageSpeed.value = 0.0; + _maxSpeed.value = 0.0; + _trackingDuration.value = 0; + _positionHistory.clear(); + } + + String _buildTrackingStatusText(Position position) { + return 'TRACKING AKTIV\n' + 'Position: ${position.latitude.toStringAsFixed(6)}, ${position.longitude.toStringAsFixed(6)}\n' + 'Genauigkeit: ${position.accuracy.toStringAsFixed(1)}m\n' + 'Geschwindigkeit: ${GeoUtils.mpsToKmh(position.speed).toStringAsFixed(1)} km/h\n' + 'Distanz: ${(_totalDistance.value * 1000).toStringAsFixed(0)}m\n' + 'Dauer: ${_formatDuration(_trackingDuration.value)}'; + } + + String _buildTrackingSummary() { + return 'Gesamtdistanz: ${(_totalDistance.value * 1000).toStringAsFixed(0)}m\n' + 'Max. Geschwindigkeit: ${_maxSpeed.value.toStringAsFixed(1)} km/h\n' + 'Ø Geschwindigkeit: ${_averageSpeed.value.toStringAsFixed(1)} km/h\n' + 'Dauer: ${_formatDuration(_trackingDuration.value)}\n' + 'Punkte: ${_positionHistory.length}'; + } + + void _startDurationTimer() { + // Update duration every second while tracking + Future.doWhile(() async { + await Future.delayed(const Duration(seconds: 1)); + if (_isTracking.value && _trackingStartTime != null) { + _trackingDuration.value = DateTime.now().difference(_trackingStartTime!).inSeconds; + return true; + } + return false; + }); + } + + String _formatDuration(int seconds) { + int hours = seconds ~/ 3600; + int minutes = (seconds % 3600) ~/ 60; + int secs = seconds % 60; + + if (hours > 0) { + return '${hours}h ${minutes}m ${secs}s'; + } else if (minutes > 0) { + return '${minutes}m ${secs}s'; + } else { + return '${secs}s'; + } + } + + // Zusätzliche Helper-Methoden + + /// Togglet das Tracking (Start/Stop) + Future toggleTracking() async { + if (_isTracking.value) { + await stopTracking(); + } else { + await startTracking(); + } + } + + /// Öffnet die Location Einstellungen + Future openLocationSettings() async { + await GeolocationService.openLocationSettings(); + } + + /// Öffnet die App Einstellungen + Future openAppSettings() async { + await GeolocationService.openAppSettings(); + } + + /// Exportiert Tracking-Daten als Map + Map exportTrackingData() { + return { + 'totalDistance': _totalDistance.value, + 'averageSpeed': _averageSpeed.value, + 'maxSpeed': _maxSpeed.value, + 'duration': _trackingDuration.value, + 'pointCount': _positionHistory.length, + 'positions': _positionHistory.map((p) => { + 'latitude': p.latitude, + 'longitude': p.longitude, + 'accuracy': p.accuracy, + 'speed': p.speed, + 'timestamp': p.timestamp.toIso8601String(), + }).toList(), + }; + } + + /// Berechnet Mittelpunkt aller getrackten Positionen + Map? getTrackCenter() { + if (_positionHistory.isEmpty) return null; + return GeoUtils.calculateCentroid(_positionHistory); + } + + /// Berechnet Bounding Box aller getrackten Positionen + Map? getTrackBounds() { + if (_positionHistory.isEmpty) return null; + return GeoUtils.calculateBoundingBox(_positionHistory); + } + + /// Resettet alle Werte + void reset() { + stopTracking(); + _currentPosition.value = null; + _previousPosition.value = null; + _statusText.value = 'Noch keine Position ermittelt'; + _resetTrackingStats(); + } + + // Getter für UI + bool get hasPosition => _currentPosition.value != null; + double get currentAccuracy => _currentPosition.value?.accuracy ?? 0.0; + double get currentSpeedKmh => GeoUtils.mpsToKmh(_currentPosition.value?.speed ?? 0.0); + String get formattedPosition { + if (_currentPosition.value == null) return 'Keine Position verfügbar'; + return GeolocationService.positionToString(_currentPosition.value!); + } +} \ No newline at end of file diff --git a/lib/controllers/geolocation_controller.dart b/lib/controllers/geolocation_controller.dart new file mode 100644 index 0000000..cb057c8 --- /dev/null +++ b/lib/controllers/geolocation_controller.dart @@ -0,0 +1,270 @@ +import 'package:get/get.dart'; +import 'package:geolocator/geolocator.dart'; +import '../../services/geolocation.dart'; + +/// GetX Controller für Geolocation Funktionalität +class GeolocationController extends GetxController { + // Reactive Variablen + final _currentPosition = Rxn(); + final _statusText = 'Noch keine Position ermittelt'.obs; + final _isTracking = false.obs; + final _isLoading = false.obs; + + // Getter für reactive Variablen + Position? get currentPosition => _currentPosition.value; + String get statusText => _statusText.value; + bool get isTracking => _isTracking.value; + bool get isLoading => _isLoading.value; + + @override + void onClose() { + // Cleanup beim Schließen des Controllers + stopTracking(); + super.onClose(); + } + + /// Holt die aktuelle Position einmalig + Future getCurrentPosition() async { + try { + _isLoading.value = true; + _statusText.value = 'Position wird ermittelt...'; + + Position? position = await GeolocationService.getCurrentPosition(); + + if (position != null) { + _currentPosition.value = position; + _statusText.value = GeolocationService.positionToString(position); + + // Erfolgs-Snackbar anzeigen + Get.snackbar( + 'Position ermittelt', + 'Aktuelle Position wurde erfolgreich abgerufen', + snackPosition: SnackPosition.BOTTOM, + ); + } else { + _statusText.value = 'Position konnte nicht ermittelt werden'; + + // Fehler-Snackbar anzeigen + Get.snackbar( + 'Fehler', + 'Position konnte nicht ermittelt werden', + snackPosition: SnackPosition.BOTTOM, + ); + } + } catch (e) { + _statusText.value = 'Fehler beim Abrufen der Position: $e'; + + Get.snackbar( + 'Fehler', + 'Unerwarteter Fehler: $e', + snackPosition: SnackPosition.BOTTOM, + ); + } finally { + _isLoading.value = false; + } + } + + /// Startet kontinuierliches Position Tracking + Future startTracking() async { + if (_isTracking.value) return; + + try { + _isLoading.value = true; + _statusText.value = 'Tracking wird gestartet...'; + + bool success = await GeolocationService.startPositionStream( + onPositionChanged: (Position position) { + _currentPosition.value = position; + _statusText.value = GeolocationService.positionToString(position); + }, + onError: (String error) { + _statusText.value = 'Tracking Fehler: $error'; + _isTracking.value = false; + + Get.snackbar( + 'Tracking Fehler', + error, + snackPosition: SnackPosition.BOTTOM, + ); + }, + accuracy: LocationAccuracy.high, + distanceFilter: 5, // Updates alle 5 Meter + ); + + if (success) { + _isTracking.value = true; + _statusText.value = 'Tracking aktiv - Warten auf erste Position...'; + + Get.snackbar( + 'Tracking gestartet', + 'Position wird kontinuierlich überwacht', + snackPosition: SnackPosition.BOTTOM, + ); + } else { + _statusText.value = 'Tracking konnte nicht gestartet werden'; + + Get.snackbar( + 'Fehler', + 'Tracking konnte nicht gestartet werden', + snackPosition: SnackPosition.BOTTOM, + ); + } + } catch (e) { + _statusText.value = 'Fehler beim Starten des Trackings: $e'; + + Get.snackbar( + 'Fehler', + 'Tracking-Fehler: $e', + snackPosition: SnackPosition.BOTTOM, + ); + } finally { + _isLoading.value = false; + } + } + + /// Stoppt das Position Tracking + Future stopTracking() async { + if (!_isTracking.value) return; + + try { + await GeolocationService.stopPositionStream(); + _isTracking.value = false; + _statusText.value = 'Tracking gestoppt'; + + Get.snackbar( + 'Tracking gestoppt', + 'Position wird nicht mehr überwacht', + snackPosition: SnackPosition.BOTTOM, + ); + } catch (e) { + Get.snackbar( + 'Fehler', + 'Fehler beim Stoppen des Trackings: $e', + snackPosition: SnackPosition.BOTTOM, + ); + } + } + + /// Togglet das Tracking (Start/Stop) + Future toggleTracking() async { + if (_isTracking.value) { + await stopTracking(); + } else { + await startTracking(); + } + } + + /// Prüft Permission Status + Future checkPermissions() async { + try { + _isLoading.value = true; + _statusText.value = 'Berechtigungen werden geprüft...'; + + LocationPermission permission = await GeolocationService.checkPermission(); + bool serviceEnabled = await GeolocationService.isLocationServiceEnabled(); + + _statusText.value = 'Service aktiv: $serviceEnabled\n' + 'Berechtigung: ${LocationPermissionHelper.getPermissionStatusText(permission)}'; + + // Detaillierte Information in Snackbar + String permissionStatus = LocationPermissionHelper.getPermissionStatusText(permission); + Get.snackbar( + 'Berechtigungs-Status', + 'Location Service: ${serviceEnabled ? "Aktiv" : "Inaktiv"}\n$permissionStatus', + snackPosition: SnackPosition.BOTTOM, + duration: const Duration(seconds: 4), + ); + } catch (e) { + _statusText.value = 'Fehler beim Prüfen der Berechtigungen: $e'; + + Get.snackbar( + 'Fehler', + 'Berechtigungen konnten nicht geprüft werden: $e', + snackPosition: SnackPosition.BOTTOM, + ); + } finally { + _isLoading.value = false; + } + } + + /// Öffnet die Location Einstellungen + Future openLocationSettings() async { + try { + bool opened = await GeolocationService.openLocationSettings(); + + if (opened) { + Get.snackbar( + 'Einstellungen geöffnet', + 'Location-Einstellungen wurden geöffnet', + snackPosition: SnackPosition.BOTTOM, + ); + } else { + Get.snackbar( + 'Fehler', + 'Location-Einstellungen konnten nicht geöffnet werden', + snackPosition: SnackPosition.BOTTOM, + ); + } + } catch (e) { + Get.snackbar( + 'Fehler', + 'Fehler beim Öffnen der Location-Einstellungen: $e', + snackPosition: SnackPosition.BOTTOM, + ); + } + } + + /// Öffnet die App Einstellungen + Future openAppSettings() async { + try { + bool opened = await GeolocationService.openAppSettings(); + + if (opened) { + Get.snackbar( + 'Einstellungen geöffnet', + 'App-Einstellungen wurden geöffnet', + snackPosition: SnackPosition.BOTTOM, + ); + } else { + Get.snackbar( + 'Fehler', + 'App-Einstellungen konnten nicht geöffnet werden', + snackPosition: SnackPosition.BOTTOM, + ); + } + } catch (e) { + Get.snackbar( + 'Fehler', + 'Fehler beim Öffnen der App-Einstellungen: $e', + snackPosition: SnackPosition.BOTTOM, + ); + } + } + + /// Formatiert die aktuelle Position als lesbaren String + String get formattedPosition { + if (_currentPosition.value == null) return 'Keine Position verfügbar'; + return GeolocationService.positionToString(_currentPosition.value!); + } + + /// Prüft ob eine Position verfügbar ist + bool get hasPosition => _currentPosition.value != null; + + /// Holt die Genauigkeit der aktuellen Position + double get currentAccuracy => _currentPosition.value?.accuracy ?? 0.0; + + /// Holt die Geschwindigkeit der aktuellen Position in km/h + double get currentSpeedKmh { + if (_currentPosition.value?.speed == null) return 0.0; + return (_currentPosition.value!.speed * 3.6); // m/s zu km/h + } + + /// Resettet alle Werte auf ihre Anfangszustände + void reset() { + stopTracking(); + _currentPosition.value = null; + _statusText.value = 'Noch keine Position ermittelt'; + _isTracking.value = false; + _isLoading.value = false; + } +} \ No newline at end of file diff --git a/lib/main.dart b/lib/main.dart index 0c087f4..0fa59df 100644 --- a/lib/main.dart +++ b/lib/main.dart @@ -1,4 +1,8 @@ import 'package:flutter/material.dart'; +import 'package:get/get.dart'; +import 'routes/app_routes.dart'; +import 'bindings/geolocation_binding.dart'; +import 'pages/examples/geolocation_example.dart'; void main() { runApp(const MyApp()); @@ -9,59 +13,62 @@ class MyApp extends StatelessWidget { @override Widget build(BuildContext context) { - return MaterialApp( - title: 'Flutter Demo', + return GetMaterialApp( + debugShowCheckedModeBanner: false, + title: 'Web Flutter Tank Appwrite App', theme: ThemeData( colorScheme: ColorScheme.fromSeed(seedColor: Colors.deepPurple), + useMaterial3: true, ), - home: const MyHomePage(title: 'Flutter Demo Home Page'), + // GetX Konfiguration + initialRoute: AppRoutes.geolocation, + getPages: AppRoutes.pages, + initialBinding: GeolocationBinding(), // Optional: Globale Bindings + // Alternative: Direkte Navigation ohne Routen + home: const GeolocationExample(), + + // GetX Optionen + enableLog: true, // Debugging + defaultTransition: Transition.fade, + transitionDuration: const Duration(milliseconds: 300), ); } } -class MyHomePage extends StatefulWidget { - const MyHomePage({super.key, required this.title}); - - final String title; - - @override - State createState() => _MyHomePageState(); -} - -class _MyHomePageState extends State { - int _counter = 0; - - void _incrementCounter() { - setState(() { - _counter++; - }); - } +/// Alternative: Home Page mit Navigation zu Geolocation +class HomePage extends StatelessWidget { + const HomePage({super.key}); @override Widget build(BuildContext context) { return Scaffold( appBar: AppBar( + title: const Text('Tank App'), backgroundColor: Theme.of(context).colorScheme.inversePrimary, - - title: Text(widget.title), ), body: Center( child: Column( mainAxisAlignment: MainAxisAlignment.center, - children: [ - const Text('You have pushed the button this many times:'), - Text( - '$_counter', - style: Theme.of(context).textTheme.headlineMedium, + children: [ + const Text( + 'Tank Appwrite App', + style: TextStyle(fontSize: 24, fontWeight: FontWeight.bold), + ), + const SizedBox(height: 32), + ElevatedButton.icon( + onPressed: () => Get.to(() => const GeolocationExample()), + icon: const Icon(Icons.location_on), + label: const Text('Geolocation Beispiel'), + style: ElevatedButton.styleFrom( + padding: const EdgeInsets.symmetric( + horizontal: 24, + vertical: 12, + ), + ), ), ], ), ), - floatingActionButton: FloatingActionButton( - onPressed: _incrementCounter, - tooltip: 'Increment', - child: const Icon(Icons.add), - ), ); } } diff --git a/lib/main_with_getx.dart b/lib/main_with_getx.dart new file mode 100644 index 0000000..53a0582 --- /dev/null +++ b/lib/main_with_getx.dart @@ -0,0 +1,75 @@ +import 'package:flutter/material.dart'; +import 'package:get/get.dart'; +import 'routes/app_routes.dart'; +import 'bindings/geolocation_binding.dart'; +import 'pages/examples/geolocation_example.dart'; + +void main() { + runApp(const MyApp()); +} + +class MyApp extends StatelessWidget { + const MyApp({super.key}); + + @override + Widget build(BuildContext context) { + return GetMaterialApp( + debugShowCheckedModeBanner: false, + title: 'Web Flutter Tank Appwrite App', + theme: ThemeData( + colorScheme: ColorScheme.fromSeed(seedColor: Colors.deepPurple), + useMaterial3: true, + ), + // GetX Konfiguration + initialRoute: AppRoutes.geolocation, + getPages: AppRoutes.pages, + initialBinding: GeolocationBinding(), // Optional: Globale Bindings + + // Alternative: Direkte Navigation ohne Routen + //home: const GeolocationExample(), + + // GetX Optionen + enableLog: true, // Debugging + defaultTransition: Transition.fade, + transitionDuration: const Duration(milliseconds: 300), + ); + } +} + +/// Alternative: Home Page mit Navigation zu Geolocation +class HomePage extends StatelessWidget { + const HomePage({super.key}); + + @override + Widget build(BuildContext context) { + return Scaffold( + appBar: AppBar( + title: const Text('Tank App'), + backgroundColor: Theme.of(context).colorScheme.inversePrimary, + ), + body: Center( + child: Column( + mainAxisAlignment: MainAxisAlignment.center, + children: [ + const Text( + 'Tank Appwrite App', + style: TextStyle(fontSize: 24, fontWeight: FontWeight.bold), + ), + const SizedBox(height: 32), + ElevatedButton.icon( + onPressed: () => Get.to(() => const GeolocationExample()), + icon: const Icon(Icons.location_on), + label: const Text('Geolocation Beispiel'), + style: ElevatedButton.styleFrom( + padding: const EdgeInsets.symmetric( + horizontal: 24, + vertical: 12, + ), + ), + ), + ], + ), + ), + ); + } +} \ No newline at end of file diff --git a/lib/pages/examples/geolocation_example.dart b/lib/pages/examples/geolocation_example.dart new file mode 100644 index 0000000..1d2148e --- /dev/null +++ b/lib/pages/examples/geolocation_example.dart @@ -0,0 +1,254 @@ +import 'package:flutter/material.dart'; +import 'package:get/get.dart'; +import '../../controllers/geolocation_controller.dart'; + +/// GetX View für Geolocation Funktionalität +class GeolocationExample extends StatelessWidget { + const GeolocationExample({super.key}); + + @override + Widget build(BuildContext context) { + // Controller wird automatisch erstellt und verwaltet + final GeolocationController controller = Get.put(GeolocationController()); + + return Scaffold( + appBar: AppBar( + title: const Text('Geolocation Beispiel'), + backgroundColor: Theme.of(context).colorScheme.inversePrimary, + actions: [ + // Reset Button in der AppBar + IconButton( + onPressed: controller.reset, + icon: const Icon(Icons.refresh), + tooltip: 'Zurücksetzen', + ), + ], + ), + body: Padding( + padding: const EdgeInsets.all(16.0), + child: Column( + crossAxisAlignment: CrossAxisAlignment.stretch, + children: [ + // Status Card + Obx(() => Card( + child: Padding( + padding: const EdgeInsets.all(16.0), + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Row( + children: [ + const Text( + 'Status:', + style: TextStyle( + fontSize: 18, + fontWeight: FontWeight.bold, + ), + ), + const Spacer(), + if (controller.isLoading) + const SizedBox( + width: 20, + height: 20, + child: CircularProgressIndicator(strokeWidth: 2), + ), + if (controller.isTracking) + Container( + padding: const EdgeInsets.symmetric( + horizontal: 8, + vertical: 4, + ), + decoration: BoxDecoration( + color: Colors.green, + borderRadius: BorderRadius.circular(12), + ), + child: const Text( + 'TRACKING', + style: TextStyle( + color: Colors.white, + fontSize: 12, + fontWeight: FontWeight.bold, + ), + ), + ), + ], + ), + const SizedBox(height: 8), + Text(controller.statusText), + ], + ), + ), + )), + + const SizedBox(height: 16), + + // Position Details Card + Obx(() => controller.hasPosition + ? Card( + child: Padding( + padding: const EdgeInsets.all(16.0), + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + const Text( + 'Aktuelle Position:', + style: TextStyle( + fontSize: 18, + fontWeight: FontWeight.bold, + ), + ), + const SizedBox(height: 8), + _buildPositionDetail( + 'Breitengrad:', + controller.currentPosition!.latitude.toStringAsFixed(6), + Icons.location_on, + ), + _buildPositionDetail( + 'Längengrad:', + controller.currentPosition!.longitude.toStringAsFixed(6), + Icons.location_on, + ), + _buildPositionDetail( + 'Genauigkeit:', + '${controller.currentPosition!.accuracy.toStringAsFixed(1)} m', + Icons.gps_fixed, + ), + _buildPositionDetail( + 'Höhe:', + '${controller.currentPosition!.altitude.toStringAsFixed(1)} m', + Icons.height, + ), + _buildPositionDetail( + 'Geschwindigkeit:', + '${controller.currentSpeedKmh.toStringAsFixed(1)} km/h', + Icons.speed, + ), + _buildPositionDetail( + 'Zeit:', + controller.currentPosition!.timestamp.toString(), + Icons.access_time, + ), + ], + ), + ), + ) + : const SizedBox.shrink()), + + const SizedBox(height: 16), + + // Action Buttons + Expanded( + child: Column( + children: [ + // Aktuelle Position Button + Obx(() => SizedBox( + width: double.infinity, + child: ElevatedButton.icon( + onPressed: controller.isLoading ? null : controller.getCurrentPosition, + icon: controller.isLoading + ? const SizedBox( + width: 20, + height: 20, + child: CircularProgressIndicator(strokeWidth: 2), + ) + : const Icon(Icons.my_location), + label: const Text('Aktuelle Position ermitteln'), + style: ElevatedButton.styleFrom( + padding: const EdgeInsets.symmetric(vertical: 12), + ), + ), + )), + + const SizedBox(height: 12), + + // Tracking Toggle Button + Obx(() => SizedBox( + width: double.infinity, + child: ElevatedButton.icon( + onPressed: controller.isLoading ? null : controller.toggleTracking, + icon: Icon(controller.isTracking ? Icons.stop : Icons.play_arrow), + label: Text(controller.isTracking ? 'Tracking stoppen' : 'Tracking starten'), + style: ElevatedButton.styleFrom( + backgroundColor: controller.isTracking ? Colors.red : Colors.green, + foregroundColor: Colors.white, + padding: const EdgeInsets.symmetric(vertical: 12), + ), + ), + )), + + const SizedBox(height: 12), + + // Permissions Button + Obx(() => SizedBox( + width: double.infinity, + child: ElevatedButton.icon( + onPressed: controller.isLoading ? null : controller.checkPermissions, + icon: const Icon(Icons.security), + label: const Text('Berechtigungen prüfen'), + style: ElevatedButton.styleFrom( + padding: const EdgeInsets.symmetric(vertical: 12), + ), + ), + )), + + const SizedBox(height: 12), + + // Settings Buttons Row + Row( + children: [ + Expanded( + child: ElevatedButton.icon( + onPressed: controller.openLocationSettings, + icon: const Icon(Icons.location_city), + label: const Text('Location'), + style: ElevatedButton.styleFrom( + padding: const EdgeInsets.symmetric(vertical: 12), + ), + ), + ), + const SizedBox(width: 8), + Expanded( + child: ElevatedButton.icon( + onPressed: controller.openAppSettings, + icon: const Icon(Icons.settings), + label: const Text('App'), + style: ElevatedButton.styleFrom( + padding: const EdgeInsets.symmetric(vertical: 12), + ), + ), + ), + ], + ), + ], + ), + ), + ], + ), + ), + ); + } + + /// Hilfsmethode zum Erstellen von Position Details + Widget _buildPositionDetail(String label, String value, IconData icon) { + return Padding( + padding: const EdgeInsets.symmetric(vertical: 4.0), + child: Row( + children: [ + Icon(icon, size: 16, color: Colors.grey[600]), + const SizedBox(width: 8), + Text( + label, + style: const TextStyle(fontWeight: FontWeight.w500), + ), + const SizedBox(width: 8), + Expanded( + child: Text( + value, + style: const TextStyle(fontFamily: 'monospace'), + ), + ), + ], + ), + ); + } +} \ No newline at end of file diff --git a/lib/routes/app_routes.dart b/lib/routes/app_routes.dart new file mode 100644 index 0000000..f57765f --- /dev/null +++ b/lib/routes/app_routes.dart @@ -0,0 +1,48 @@ +import 'package:get/get.dart'; +import '../pages/examples/geolocation_example.dart'; +import '../bindings/geolocation_binding.dart'; + +/// App Routes Konfiguration +class AppRoutes { + static const String home = '/'; + static const String geolocation = '/geolocation'; + + /// Route Pages Definition + static List pages = [ + GetPage( + name: geolocation, + page: () => const GeolocationExample(), + binding: GeolocationBinding(), // Dependency Injection + transition: Transition.cupertino, + transitionDuration: const Duration(milliseconds: 300), + ), + ]; +} + +/// Route Namen als Konstanten für typsichere Navigation +class Routes { + static const String geolocationExample = '/geolocation-example'; +} + +/// Navigation Helper Klasse +class AppNavigation { + /// Navigate to Geolocation Example + static void toGeolocation() { + Get.toNamed(AppRoutes.geolocation); + } + + /// Navigate back + static void back() { + Get.back(); + } + + /// Navigate and replace current route + static void offGeolocation() { + Get.offNamed(AppRoutes.geolocation); + } + + /// Navigate and clear all previous routes + static void offAllToGeolocation() { + Get.offAllNamed(AppRoutes.geolocation); + } +} \ No newline at end of file diff --git a/lib/services/geolocation.dart b/lib/services/geolocation.dart new file mode 100644 index 0000000..14c6916 --- /dev/null +++ b/lib/services/geolocation.dart @@ -0,0 +1,333 @@ +import 'dart:async'; +import 'package:geolocator/geolocator.dart'; +import 'package:flutter/foundation.dart'; + +/// Statische Geolocation Services für alle Plattformen +/// Unterstützt Android, iOS, Web, Windows, macOS und Linux +class GeolocationService { + /// Private Konstruktor um Instanziierung zu verhindern + GeolocationService._(); + + /// Stream Controller für kontinuierliche Position Updates + static StreamSubscription? _positionStreamSubscription; + + /// Prüft ob Location Services verfügbar und aktiviert sind + static Future isLocationServiceEnabled() async { + try { + return await Geolocator.isLocationServiceEnabled(); + } catch (e) { + if (kDebugMode) { + print('Fehler beim Prüfen der Location Services: $e'); + } + return false; + } + } + + /// Prüft die aktuellen Location Permissions + static Future checkPermission() async { + try { + return await Geolocator.checkPermission(); + } catch (e) { + if (kDebugMode) { + print('Fehler beim Prüfen der Berechtigung: $e'); + } + return LocationPermission.denied; + } + } + + /// Fordert Location Permissions an + static Future requestPermission() async { + try { + return await Geolocator.requestPermission(); + } catch (e) { + if (kDebugMode) { + print('Fehler beim Anfordern der Berechtigung: $e'); + } + return LocationPermission.denied; + } + } + + /// Prüft und fordert bei Bedarf Permissions an + static Future ensurePermissions() async { + // Prüfe ob Location Services aktiviert sind + bool serviceEnabled = await isLocationServiceEnabled(); + if (!serviceEnabled) { + if (kDebugMode) { + print('Location Services sind nicht aktiviert'); + } + return false; + } + + LocationPermission permission = await checkPermission(); + + if (permission == LocationPermission.denied) { + permission = await requestPermission(); + if (permission == LocationPermission.denied) { + if (kDebugMode) { + print('Location Permissions wurden verweigert'); + } + return false; + } + } + + if (permission == LocationPermission.deniedForever) { + if (kDebugMode) { + print('Location Permissions wurden dauerhaft verweigert'); + } + return false; + } + + return true; + } + + /// Holt die aktuelle Position einmalig + /// + /// [accuracy] - Gewünschte Genauigkeit (Standard: LocationAccuracy.high) + /// [timeLimit] - Timeout für die Anfrage (Standard: 10 Sekunden) + static Future getCurrentPosition({ + LocationAccuracy accuracy = LocationAccuracy.high, + Duration timeLimit = const Duration(seconds: 10), + }) async { + try { + // Prüfe Permissions + bool hasPermission = await ensurePermissions(); + if (!hasPermission) { + return null; + } + + // Hole aktuelle Position + Position position = await Geolocator.getCurrentPosition( + locationSettings: LocationSettings( + accuracy: accuracy, + timeLimit: timeLimit, + ), + ); + + return position; + } catch (e) { + if (kDebugMode) { + print('Fehler beim Abrufen der aktuellen Position: $e'); + } + return null; + } + } + + /// Startet kontinuierliche Position Updates + /// + /// [onPositionChanged] - Callback Funktion für neue Positionen + /// [onError] - Callback Funktion für Fehler + /// [accuracy] - Gewünschte Genauigkeit + /// [distanceFilter] - Minimale Distanz zwischen Updates in Metern + /// [intervalDuration] - Minimale Zeit zwischen Updates + static Future startPositionStream({ + required Function(Position) onPositionChanged, + Function(String)? onError, + LocationAccuracy accuracy = LocationAccuracy.high, + int distanceFilter = 10, + Duration intervalDuration = const Duration(seconds: 5), + }) async { + try { + // Prüfe Permissions + bool hasPermission = await ensurePermissions(); + if (!hasPermission) { + onError?.call('Keine Location Permissions'); + return false; + } + + // Stoppe vorherigen Stream falls aktiv + await stopPositionStream(); + + // Erstelle Location Settings basierend auf Plattform + LocationSettings locationSettings; + + if (defaultTargetPlatform == TargetPlatform.android) { + locationSettings = AndroidSettings( + accuracy: accuracy, + distanceFilter: distanceFilter, + intervalDuration: intervalDuration, + forceLocationManager: false, + ); + } else if (defaultTargetPlatform == TargetPlatform.iOS) { + locationSettings = AppleSettings( + accuracy: accuracy, + distanceFilter: distanceFilter, + activityType: ActivityType.other, + pauseLocationUpdatesAutomatically: true, + showBackgroundLocationIndicator: false, + ); + } else { + locationSettings = LocationSettings( + accuracy: accuracy, + distanceFilter: distanceFilter, + ); + } + + // Starte Position Stream + _positionStreamSubscription = Geolocator.getPositionStream( + locationSettings: locationSettings, + ).listen( + onPositionChanged, + onError: (error) { + if (kDebugMode) { + print('Position Stream Fehler: $error'); + } + onError?.call(error.toString()); + }, + ); + + return true; + } catch (e) { + if (kDebugMode) { + print('Fehler beim Starten des Position Streams: $e'); + } + onError?.call(e.toString()); + return false; + } + } + + /// Stoppt kontinuierliche Position Updates + static Future stopPositionStream() async { + try { + await _positionStreamSubscription?.cancel(); + _positionStreamSubscription = null; + } catch (e) { + if (kDebugMode) { + print('Fehler beim Stoppen des Position Streams: $e'); + } + } + } + + /// Prüft ob Position Stream aktiv ist + static bool isPositionStreamActive() { + return _positionStreamSubscription != null && !_positionStreamSubscription!.isPaused; + } + + /// Berechnet die Distanz zwischen zwei Positionen in Metern + static double calculateDistance({ + required double startLatitude, + required double startLongitude, + required double endLatitude, + required double endLongitude, + }) { + return Geolocator.distanceBetween( + startLatitude, + startLongitude, + endLatitude, + endLongitude, + ); + } + + /// Berechnet die Richtung zwischen zwei Positionen in Grad + static double calculateBearing({ + required double startLatitude, + required double startLongitude, + required double endLatitude, + required double endLongitude, + }) { + return Geolocator.bearingBetween( + startLatitude, + startLongitude, + endLatitude, + endLongitude, + ); + } + + /// Öffnet die Geräte-Einstellungen für Location Services + static Future openLocationSettings() async { + try { + return await Geolocator.openLocationSettings(); + } catch (e) { + if (kDebugMode) { + print('Fehler beim Öffnen der Location Settings: $e'); + } + return false; + } + } + + /// Öffnet die App-Einstellungen + static Future openAppSettings() async { + try { + return await Geolocator.openAppSettings(); + } catch (e) { + if (kDebugMode) { + print('Fehler beim Öffnen der App Settings: $e'); + } + return false; + } + } + + /// Formatiert Position zu einem lesbaren String + static String positionToString(Position position) { + return 'Lat: ${position.latitude.toStringAsFixed(6)}, ' + 'Lng: ${position.longitude.toStringAsFixed(6)}, ' + 'Genauigkeit: ${position.accuracy.toStringAsFixed(1)}m, ' + 'Zeit: ${position.timestamp}'; + } + + /// Konvertiert Position zu Map für JSON Serialisierung + static Map positionToMap(Position position) { + return { + 'latitude': position.latitude, + 'longitude': position.longitude, + 'accuracy': position.accuracy, + 'altitude': position.altitude, + 'speed': position.speed, + 'speedAccuracy': position.speedAccuracy, + 'heading': position.heading, + 'timestamp': position.timestamp.toIso8601String(), + }; + } + + /// Erstellt Position aus Map + static Position? positionFromMap(Map map) { + try { + return Position( + latitude: map['latitude']?.toDouble() ?? 0.0, + longitude: map['longitude']?.toDouble() ?? 0.0, + timestamp: map['timestamp'] != null + ? DateTime.parse(map['timestamp']) + : DateTime.now(), + accuracy: map['accuracy']?.toDouble() ?? 0.0, + altitude: map['altitude']?.toDouble() ?? 0.0, + altitudeAccuracy: map['altitudeAccuracy']?.toDouble() ?? 0.0, + heading: map['heading']?.toDouble() ?? 0.0, + headingAccuracy: map['headingAccuracy']?.toDouble() ?? 0.0, + speed: map['speed']?.toDouble() ?? 0.0, + speedAccuracy: map['speedAccuracy']?.toDouble() ?? 0.0, + ); + } catch (e) { + if (kDebugMode) { + print('Fehler beim Erstellen der Position aus Map: $e'); + } + return null; + } + } + + /// Cleanup Methode - sollte beim App Shutdown aufgerufen werden + static Future dispose() async { + await stopPositionStream(); + } +} + +/// Hilfsklasse für Location Permissions Status +class LocationPermissionHelper { + static String getPermissionStatusText(LocationPermission permission) { + switch (permission) { + case LocationPermission.denied: + return 'Standort-Berechtigung verweigert'; + case LocationPermission.deniedForever: + return 'Standort-Berechtigung dauerhaft verweigert'; + case LocationPermission.whileInUse: + return 'Standort-Berechtigung während App-Nutzung'; + case LocationPermission.always: + return 'Standort-Berechtigung immer'; + default: + return 'Unbekannter Berechtigungs-Status'; + } + } + + static bool isPermissionGranted(LocationPermission permission) { + return permission == LocationPermission.whileInUse || + permission == LocationPermission.always; + } +} \ No newline at end of file diff --git a/lib/utils/geo_utils.dart b/lib/utils/geo_utils.dart new file mode 100644 index 0000000..588bd2f --- /dev/null +++ b/lib/utils/geo_utils.dart @@ -0,0 +1,261 @@ +import 'dart:math' as math; +import 'package:geolocator/geolocator.dart'; + +/// Utility Klasse für erweiterte Geo-Funktionen +class GeoUtils { + /// Private Konstruktor + GeoUtils._(); + + /// Erdradius in Kilometern + static const double earthRadiusKm = 6371.0; + + /// Konvertiert Grad zu Radiant + static double degreesToRadians(double degrees) { + return degrees * math.pi / 180.0; + } + + /// Konvertiert Radiant zu Grad + static double radiansToDegrees(double radians) { + return radians * 180.0 / math.pi; + } + + /// Berechnet die Distanz zwischen zwei Punkten mit der Haversine-Formel + /// Ergebnis in Kilometern + static double calculateDistanceKm({ + required double startLat, + required double startLng, + required double endLat, + required double endLng, + }) { + double dLat = degreesToRadians(endLat - startLat); + double dLng = degreesToRadians(endLng - startLng); + + double a = math.sin(dLat / 2) * math.sin(dLat / 2) + + math.cos(degreesToRadians(startLat)) * + math.cos(degreesToRadians(endLat)) * + math.sin(dLng / 2) * + math.sin(dLng / 2); + + double c = 2 * math.atan2(math.sqrt(a), math.sqrt(1 - a)); + return earthRadiusKm * c; + } + + /// Berechnet einen neuen Punkt basierend auf Startpunkt, Distanz und Richtung + /// [distance] in Kilometern + /// [bearing] in Grad (0 = Norden, 90 = Osten) + static Map calculateDestination({ + required double startLat, + required double startLng, + required double distance, + required double bearing, + }) { + double lat1 = degreesToRadians(startLat); + double lng1 = degreesToRadians(startLng); + double brng = degreesToRadians(bearing); + double d = distance / earthRadiusKm; + + double lat2 = math.asin( + math.sin(lat1) * math.cos(d) + + math.cos(lat1) * math.sin(d) * math.cos(brng), + ); + + double lng2 = lng1 + + math.atan2( + math.sin(brng) * math.sin(d) * math.cos(lat1), + math.cos(d) - math.sin(lat1) * math.sin(lat2), + ); + + return { + 'latitude': radiansToDegrees(lat2), + 'longitude': radiansToDegrees(lng2), + }; + } + + /// Prüft ob ein Punkt innerhalb eines Kreises liegt + /// [radius] in Kilometern + static bool isPointInCircle({ + required double pointLat, + required double pointLng, + required double centerLat, + required double centerLng, + required double radius, + }) { + double distance = calculateDistanceKm( + startLat: pointLat, + startLng: pointLng, + endLat: centerLat, + endLng: centerLng, + ); + return distance <= radius; + } + + /// Berechnet die Richtung zwischen zwei Punkten + /// Ergebnis in Grad (0-360, 0 = Norden) + static double calculateBearing({ + required double startLat, + required double startLng, + required double endLat, + required double endLng, + }) { + double lat1 = degreesToRadians(startLat); + double lat2 = degreesToRadians(endLat); + double dLng = degreesToRadians(endLng - startLng); + + double y = math.sin(dLng) * math.cos(lat2); + double x = math.cos(lat1) * math.sin(lat2) - + math.sin(lat1) * math.cos(lat2) * math.cos(dLng); + + double bearing = radiansToDegrees(math.atan2(y, x)); + return (bearing + 360) % 360; + } + + /// Konvertiert Geschwindigkeit von m/s zu km/h + static double mpsToKmh(double mps) { + return mps * 3.6; + } + + /// Konvertiert Geschwindigkeit von km/h zu m/s + static double kmhToMps(double kmh) { + return kmh / 3.6; + } + + /// Formatiert Koordinaten als String + static String formatCoordinates(double lat, double lng, {int precision = 6}) { + return '${lat.toStringAsFixed(precision)}, ${lng.toStringAsFixed(precision)}'; + } + + /// Formatiert Koordinaten als DMS (Degrees, Minutes, Seconds) + static String formatCoordinatesDMS(double lat, double lng) { + String latDMS = _decimalToDMS(lat.abs(), lat >= 0 ? 'N' : 'S'); + String lngDMS = _decimalToDMS(lng.abs(), lng >= 0 ? 'E' : 'W'); + return '$latDMS, $lngDMS'; + } + + /// Hilfsfunktion für DMS Konvertierung + static String _decimalToDMS(double decimal, String direction) { + int degrees = decimal.floor(); + double minutesDecimal = (decimal - degrees) * 60; + int minutes = minutesDecimal.floor(); + double seconds = (minutesDecimal - minutes) * 60; + + return '$degrees°$minutes\'${seconds.toStringAsFixed(2)}"$direction'; + } + + /// Berechnet die Mittelpunkt zwischen mehreren Positionen + static Map? calculateCentroid(List positions) { + if (positions.isEmpty) return null; + + double x = 0, y = 0, z = 0; + + for (Position position in positions) { + double lat = degreesToRadians(position.latitude); + double lng = degreesToRadians(position.longitude); + + x += math.cos(lat) * math.cos(lng); + y += math.cos(lat) * math.sin(lng); + z += math.sin(lat); + } + + int count = positions.length; + x /= count; + y /= count; + z /= count; + + double centralLng = math.atan2(y, x); + double centralSqrt = math.sqrt(x * x + y * y); + double centralLat = math.atan2(z, centralSqrt); + + return { + 'latitude': radiansToDegrees(centralLat), + 'longitude': radiansToDegrees(centralLng), + }; + } + + /// Berechnet das Bounding Box für eine Liste von Positionen + static Map? calculateBoundingBox(List positions) { + if (positions.isEmpty) return null; + + double minLat = positions.first.latitude; + double maxLat = positions.first.latitude; + double minLng = positions.first.longitude; + double maxLng = positions.first.longitude; + + for (Position position in positions) { + if (position.latitude < minLat) minLat = position.latitude; + if (position.latitude > maxLat) maxLat = position.latitude; + if (position.longitude < minLng) minLng = position.longitude; + if (position.longitude > maxLng) maxLng = position.longitude; + } + + return { + 'minLatitude': minLat, + 'maxLatitude': maxLat, + 'minLongitude': minLng, + 'maxLongitude': maxLng, + }; + } + + /// Validiert Koordinaten + static bool isValidCoordinate(double latitude, double longitude) { + return latitude >= -90 && latitude <= 90 && longitude >= -180 && longitude <= 180; + } + + /// Berechnet die Geschwindigkeit zwischen zwei Positionen mit Zeitstempel + static double? calculateSpeed(Position position1, Position position2) { + double distance = Geolocator.distanceBetween( + position1.latitude, + position1.longitude, + position2.latitude, + position2.longitude, + ); + + int timeDifference = position2.timestamp + .difference(position1.timestamp) + .inMilliseconds; + + if (timeDifference <= 0) return null; + + // Geschwindigkeit in m/s + return distance / (timeDifference / 1000); + } + + /// Glättet GPS-Koordinaten mit einem einfachen Moving Average Filter + static List smoothTrack(List positions, {int windowSize = 5}) { + if (positions.length <= windowSize) return positions; + + List smoothed = []; + + for (int i = 0; i < positions.length; i++) { + int start = math.max(0, i - windowSize ~/ 2); + int end = math.min(positions.length - 1, i + windowSize ~/ 2); + + double avgLat = 0; + double avgLng = 0; + int count = 0; + + for (int j = start; j <= end; j++) { + avgLat += positions[j].latitude; + avgLng += positions[j].longitude; + count++; + } + + avgLat /= count; + avgLng /= count; + + smoothed.add(Position( + latitude: avgLat, + longitude: avgLng, + timestamp: positions[i].timestamp, + accuracy: positions[i].accuracy, + altitude: positions[i].altitude, + altitudeAccuracy: positions[i].altitudeAccuracy, + heading: positions[i].heading, + headingAccuracy: positions[i].headingAccuracy, + speed: positions[i].speed, + speedAccuracy: positions[i].speedAccuracy, + )); + } + + return smoothed; + } +} \ No newline at end of file diff --git a/macos/Flutter/GeneratedPluginRegistrant.swift b/macos/Flutter/GeneratedPluginRegistrant.swift index cccf817..d7ee0bc 100644 --- a/macos/Flutter/GeneratedPluginRegistrant.swift +++ b/macos/Flutter/GeneratedPluginRegistrant.swift @@ -5,6 +5,10 @@ import FlutterMacOS import Foundation +import geolocator_apple +import package_info_plus func RegisterGeneratedPlugins(registry: FlutterPluginRegistry) { + GeolocatorPlugin.register(with: registry.registrar(forPlugin: "GeolocatorPlugin")) + FPPPackageInfoPlusPlugin.register(with: registry.registrar(forPlugin: "FPPPackageInfoPlusPlugin")) } diff --git a/pubspec.lock b/pubspec.lock index 84893ab..6a0dae3 100644 --- a/pubspec.lock +++ b/pubspec.lock @@ -1,6 +1,14 @@ # Generated by pub # See https://dart.dev/tools/pub/glossary#lockfile packages: + args: + dependency: transitive + description: + name: args + sha256: d0481093c50b1da8910eb0bb301626d4d8eb7284aa739614d2b394ee09e3ea04 + url: "https://pub.dev" + source: hosted + version: "2.7.0" async: dependency: transitive description: @@ -41,6 +49,14 @@ packages: url: "https://pub.dev" source: hosted version: "1.19.1" + crypto: + dependency: transitive + description: + name: crypto + sha256: "1e445881f28f22d6140f181e07737b22f1e099a5e1ff94b0af2f9e4a463f4855" + url: "https://pub.dev" + source: hosted + version: "3.0.6" cupertino_icons: dependency: "direct main" description: @@ -49,6 +65,14 @@ packages: url: "https://pub.dev" source: hosted version: "1.0.8" + dbus: + dependency: transitive + description: + name: dbus + sha256: "79e0c23480ff85dc68de79e2cd6334add97e48f7f4865d17686dd6ea81a47e8c" + url: "https://pub.dev" + source: hosted + version: "0.7.11" fake_async: dependency: transitive description: @@ -57,6 +81,22 @@ packages: url: "https://pub.dev" source: hosted version: "1.3.3" + ffi: + dependency: transitive + description: + name: ffi + sha256: "289279317b4b16eb2bb7e271abccd4bf84ec9bdcbe999e278a94b804f5630418" + url: "https://pub.dev" + source: hosted + version: "2.1.4" + fixnum: + dependency: transitive + description: + name: fixnum + sha256: b6dc7065e46c974bc7c5f143080a6764ec7a4be6da1285ececdc37be96de53be + url: "https://pub.dev" + source: hosted + version: "1.1.1" flutter: dependency: "direct main" description: flutter @@ -75,6 +115,107 @@ packages: description: flutter source: sdk version: "0.0.0" + flutter_web_plugins: + dependency: transitive + description: flutter + source: sdk + version: "0.0.0" + geoclue: + dependency: transitive + description: + name: geoclue + sha256: c2a998c77474fc57aa00c6baa2928e58f4b267649057a1c76738656e9dbd2a7f + url: "https://pub.dev" + source: hosted + version: "0.1.1" + geolocator: + dependency: "direct main" + description: + name: geolocator + sha256: "79939537046c9025be47ec645f35c8090ecadb6fe98eba146a0d25e8c1357516" + url: "https://pub.dev" + source: hosted + version: "14.0.2" + geolocator_android: + dependency: transitive + description: + name: geolocator_android + sha256: "179c3cb66dfa674fc9ccbf2be872a02658724d1c067634e2c427cf6df7df901a" + url: "https://pub.dev" + source: hosted + version: "5.0.2" + geolocator_apple: + dependency: transitive + description: + name: geolocator_apple + sha256: dbdd8789d5aaf14cf69f74d4925ad1336b4433a6efdf2fce91e8955dc921bf22 + url: "https://pub.dev" + source: hosted + version: "2.3.13" + geolocator_linux: + dependency: transitive + description: + name: geolocator_linux + sha256: c4e966f0a7a87e70049eac7a2617f9e16fd4c585a26e4330bdfc3a71e6a721f3 + url: "https://pub.dev" + source: hosted + version: "0.2.3" + geolocator_platform_interface: + dependency: transitive + description: + name: geolocator_platform_interface + sha256: "30cb64f0b9adcc0fb36f628b4ebf4f731a2961a0ebd849f4b56200205056fe67" + url: "https://pub.dev" + source: hosted + version: "4.2.6" + geolocator_web: + dependency: transitive + description: + name: geolocator_web + sha256: b1ae9bdfd90f861fde8fd4f209c37b953d65e92823cb73c7dee1fa021b06f172 + url: "https://pub.dev" + source: hosted + version: "4.1.3" + geolocator_windows: + dependency: transitive + description: + name: geolocator_windows + sha256: "175435404d20278ffd220de83c2ca293b73db95eafbdc8131fe8609be1421eb6" + url: "https://pub.dev" + source: hosted + version: "0.2.5" + get: + dependency: "direct main" + description: + name: get + sha256: c79eeb4339f1f3deffd9ec912f8a923834bec55f7b49c9e882b8fef2c139d425 + url: "https://pub.dev" + source: hosted + version: "4.7.2" + gsettings: + dependency: transitive + description: + name: gsettings + sha256: "1b0ce661f5436d2db1e51f3c4295a49849f03d304003a7ba177d01e3a858249c" + url: "https://pub.dev" + source: hosted + version: "0.2.8" + http: + dependency: "direct main" + description: + name: http + sha256: bb2ce4590bc2667c96f318d68cac1b5a7987ec819351d32b1c987239a815e007 + url: "https://pub.dev" + source: hosted + version: "1.5.0" + http_parser: + dependency: transitive + description: + name: http_parser + sha256: "178d74305e7866013777bab2c3d8726205dc5a4dd935297175b19a23a2e66571" + url: "https://pub.dev" + source: hosted + version: "4.1.2" leak_tracker: dependency: transitive description: @@ -131,6 +272,22 @@ packages: url: "https://pub.dev" source: hosted version: "1.17.0" + package_info_plus: + dependency: transitive + description: + name: package_info_plus + sha256: "16eee997588c60225bda0488b6dcfac69280a6b7a3cf02c741895dd370a02968" + url: "https://pub.dev" + source: hosted + version: "8.3.1" + package_info_plus_platform_interface: + dependency: transitive + description: + name: package_info_plus_platform_interface + sha256: "202a487f08836a592a6bd4f901ac69b3a8f146af552bbd14407b6b41e1c3f086" + url: "https://pub.dev" + source: hosted + version: "3.2.1" path: dependency: transitive description: @@ -139,6 +296,22 @@ packages: url: "https://pub.dev" source: hosted version: "1.9.1" + petitparser: + dependency: transitive + description: + name: petitparser + sha256: "1a97266a94f7350d30ae522c0af07890c70b8e62c71e8e3920d1db4d23c057d1" + url: "https://pub.dev" + source: hosted + version: "7.0.1" + plugin_platform_interface: + dependency: transitive + description: + name: plugin_platform_interface + sha256: "4820fbfdb9478b1ebae27888254d445073732dae3d6ea81f0b7e06d5dedc3f02" + url: "https://pub.dev" + source: hosted + version: "2.1.8" sky_engine: dependency: transitive description: flutter @@ -152,6 +325,14 @@ packages: url: "https://pub.dev" source: hosted version: "1.10.1" + sprintf: + dependency: transitive + description: + name: sprintf + sha256: "1fc9ffe69d4df602376b52949af107d8f5703b77cda567c4d7d86a0693120f23" + url: "https://pub.dev" + source: hosted + version: "7.0.0" stack_trace: dependency: transitive description: @@ -192,6 +373,22 @@ packages: url: "https://pub.dev" source: hosted version: "0.7.7" + typed_data: + dependency: transitive + description: + name: typed_data + sha256: f9049c039ebfeb4cf7a7104a675823cd72dba8297f264b6637062516699fa006 + url: "https://pub.dev" + source: hosted + version: "1.4.0" + uuid: + dependency: transitive + description: + name: uuid + sha256: a5be9ef6618a7ac1e964353ef476418026db906c4facdedaa299b7a2e71690ff + url: "https://pub.dev" + source: hosted + version: "4.5.1" vector_math: dependency: transitive description: @@ -208,6 +405,38 @@ packages: url: "https://pub.dev" source: hosted version: "15.0.2" + web: + dependency: transitive + description: + name: web + sha256: "868d88a33d8a87b18ffc05f9f030ba328ffefba92d6c127917a2ba740f9cfe4a" + url: "https://pub.dev" + source: hosted + version: "1.1.1" + win32: + dependency: transitive + description: + name: win32 + sha256: d7cb55e04cd34096cd3a79b3330245f54cb96a370a1c27adb3c84b917de8b08e + url: "https://pub.dev" + source: hosted + version: "5.15.0" + xdg_directories: + dependency: transitive + description: + name: xdg_directories + sha256: "7a3f37b05d989967cdddcbb571f1ea834867ae2faa29725fd085180e0883aa15" + url: "https://pub.dev" + source: hosted + version: "1.1.0" + xml: + dependency: transitive + description: + name: xml + sha256: "971043b3a0d3da28727e40ed3e0b5d18b742fa5a68665cca88e74b7876d5e025" + url: "https://pub.dev" + source: hosted + version: "6.6.1" sdks: dart: ">=3.9.2 <4.0.0" - flutter: ">=3.18.0-18.0.pre.54" + flutter: ">=3.19.0" diff --git a/pubspec.yaml b/pubspec.yaml index 6289541..0b5e10f 100644 --- a/pubspec.yaml +++ b/pubspec.yaml @@ -13,6 +13,9 @@ dependencies: sdk: flutter cupertino_icons: ^1.0.8 + get: ^4.7.2 + http: ^1.5.0 + geolocator: ^14.0.2 dev_dependencies: flutter_test: diff --git a/windows/flutter/generated_plugin_registrant.cc b/windows/flutter/generated_plugin_registrant.cc index 8b6d468..1ece8f2 100644 --- a/windows/flutter/generated_plugin_registrant.cc +++ b/windows/flutter/generated_plugin_registrant.cc @@ -6,6 +6,9 @@ #include "generated_plugin_registrant.h" +#include void RegisterPlugins(flutter::PluginRegistry* registry) { + GeolocatorWindowsRegisterWithRegistrar( + registry->GetRegistrarForPlugin("GeolocatorWindows")); } diff --git a/windows/flutter/generated_plugins.cmake b/windows/flutter/generated_plugins.cmake index b93c4c3..7f101a7 100644 --- a/windows/flutter/generated_plugins.cmake +++ b/windows/flutter/generated_plugins.cmake @@ -3,6 +3,7 @@ # list(APPEND FLUTTER_PLUGIN_LIST + geolocator_windows ) list(APPEND FLUTTER_FFI_PLUGIN_LIST