add geolocation View and controller and services, routes and bindings
This commit is contained in:
parent
d6c3d141ef
commit
b6bd692cd7
222
GETX_GEOLOCATION_README.md
Normal file
222
GETX_GEOLOCATION_README.md
Normal file
@ -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<GeolocationController>();
|
||||
|
||||
// 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<MyPage> {
|
||||
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!
|
||||
20
lib/bindings/geolocation_binding.dart
Normal file
20
lib/bindings/geolocation_binding.dart
Normal file
@ -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>(() => GeolocationController());
|
||||
}
|
||||
}
|
||||
|
||||
/// Alternative: Permanent Binding für App-weite Nutzung
|
||||
class GeolocationPermanentBinding extends Bindings {
|
||||
@override
|
||||
void dependencies() {
|
||||
// Permanent - Controller bleibt im Speicher
|
||||
Get.put<GeolocationController>(GeolocationController(), permanent: true);
|
||||
}
|
||||
}
|
||||
354
lib/controllers/geolocation_advanced_controller.dart
Normal file
354
lib/controllers/geolocation_advanced_controller.dart
Normal file
@ -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<Position>();
|
||||
final _previousPosition = Rxn<Position>();
|
||||
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 = <Position>[].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<Position> get positionHistory => _positionHistory;
|
||||
|
||||
@override
|
||||
void onClose() {
|
||||
stopTracking();
|
||||
super.onClose();
|
||||
}
|
||||
|
||||
/// Holt die aktuelle Position einmalig
|
||||
Future<void> 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<void> 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<void> 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<void> 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<void> toggleTracking() async {
|
||||
if (_isTracking.value) {
|
||||
await stopTracking();
|
||||
} else {
|
||||
await startTracking();
|
||||
}
|
||||
}
|
||||
|
||||
/// Öffnet die Location Einstellungen
|
||||
Future<void> openLocationSettings() async {
|
||||
await GeolocationService.openLocationSettings();
|
||||
}
|
||||
|
||||
/// Öffnet die App Einstellungen
|
||||
Future<void> openAppSettings() async {
|
||||
await GeolocationService.openAppSettings();
|
||||
}
|
||||
|
||||
/// Exportiert Tracking-Daten als Map
|
||||
Map<String, dynamic> 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<String, double>? getTrackCenter() {
|
||||
if (_positionHistory.isEmpty) return null;
|
||||
return GeoUtils.calculateCentroid(_positionHistory);
|
||||
}
|
||||
|
||||
/// Berechnet Bounding Box aller getrackten Positionen
|
||||
Map<String, double>? 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!);
|
||||
}
|
||||
}
|
||||
270
lib/controllers/geolocation_controller.dart
Normal file
270
lib/controllers/geolocation_controller.dart
Normal file
@ -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<Position>();
|
||||
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<void> 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<void> 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<void> 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<void> toggleTracking() async {
|
||||
if (_isTracking.value) {
|
||||
await stopTracking();
|
||||
} else {
|
||||
await startTracking();
|
||||
}
|
||||
}
|
||||
|
||||
/// Prüft Permission Status
|
||||
Future<void> 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<void> 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<void> 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;
|
||||
}
|
||||
}
|
||||
@ -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<MyHomePage> createState() => _MyHomePageState();
|
||||
}
|
||||
|
||||
class _MyHomePageState extends State<MyHomePage> {
|
||||
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: <Widget>[
|
||||
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),
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
75
lib/main_with_getx.dart
Normal file
75
lib/main_with_getx.dart
Normal file
@ -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,
|
||||
),
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
254
lib/pages/examples/geolocation_example.dart
Normal file
254
lib/pages/examples/geolocation_example.dart
Normal file
@ -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'),
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
48
lib/routes/app_routes.dart
Normal file
48
lib/routes/app_routes.dart
Normal file
@ -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<GetPage> 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);
|
||||
}
|
||||
}
|
||||
333
lib/services/geolocation.dart
Normal file
333
lib/services/geolocation.dart
Normal file
@ -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<Position>? _positionStreamSubscription;
|
||||
|
||||
/// Prüft ob Location Services verfügbar und aktiviert sind
|
||||
static Future<bool> 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<LocationPermission> 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<LocationPermission> 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<bool> 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<Position?> 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<bool> 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<void> 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<bool> 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<bool> 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<String, dynamic> 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<String, dynamic> 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<void> 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;
|
||||
}
|
||||
}
|
||||
261
lib/utils/geo_utils.dart
Normal file
261
lib/utils/geo_utils.dart
Normal file
@ -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<String, double> 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<String, double>? calculateCentroid(List<Position> 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<String, double>? calculateBoundingBox(List<Position> 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<Position> smoothTrack(List<Position> positions, {int windowSize = 5}) {
|
||||
if (positions.length <= windowSize) return positions;
|
||||
|
||||
List<Position> 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;
|
||||
}
|
||||
}
|
||||
@ -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"))
|
||||
}
|
||||
|
||||
231
pubspec.lock
231
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"
|
||||
|
||||
@ -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:
|
||||
|
||||
@ -6,6 +6,9 @@
|
||||
|
||||
#include "generated_plugin_registrant.h"
|
||||
|
||||
#include <geolocator_windows/geolocator_windows.h>
|
||||
|
||||
void RegisterPlugins(flutter::PluginRegistry* registry) {
|
||||
GeolocatorWindowsRegisterWithRegistrar(
|
||||
registry->GetRegistrarForPlugin("GeolocatorWindows"));
|
||||
}
|
||||
|
||||
@ -3,6 +3,7 @@
|
||||
#
|
||||
|
||||
list(APPEND FLUTTER_PLUGIN_LIST
|
||||
geolocator_windows
|
||||
)
|
||||
|
||||
list(APPEND FLUTTER_FFI_PLUGIN_LIST
|
||||
|
||||
Loading…
x
Reference in New Issue
Block a user