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

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