add geolocation View and controller and services, routes and bindings

This commit is contained in:
atseirjo 2025-10-20 15:14:42 +02:00
parent d6c3d141ef
commit b6bd692cd7
15 changed files with 2117 additions and 33 deletions

222
GETX_GEOLOCATION_README.md Normal file
View 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!

View 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);
}
}

View 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!);
}
}

View 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;
}
}

View File

@ -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
View 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,
),
),
),
],
),
),
);
}
}

View 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'),
),
),
],
),
);
}
}

View 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);
}
}

View 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
View 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;
}
}

View File

@ -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"))
}

View File

@ -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"

View File

@ -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:

View File

@ -6,6 +6,9 @@
#include "generated_plugin_registrant.h"
#include <geolocator_windows/geolocator_windows.h>
void RegisterPlugins(flutter::PluginRegistry* registry) {
GeolocatorWindowsRegisterWithRegistrar(
registry->GetRegistrarForPlugin("GeolocatorWindows"));
}

View File

@ -3,6 +3,7 @@
#
list(APPEND FLUTTER_PLUGIN_LIST
geolocator_windows
)
list(APPEND FLUTTER_FFI_PLUGIN_LIST