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