Better location / gps maybe

This commit is contained in:
stopflock
2025-12-12 16:26:50 -06:00
parent 8493679526
commit 5312456a15
4 changed files with 273 additions and 44 deletions

View File

@@ -2,7 +2,9 @@
"2.1.3": {
"content": [
"• Fixed nodes losing their greyed-out appearance when map is moved while viewing a node's tag sheet",
"• Nodes now stay properly dimmed when viewing tags until the sheet is closed, regardless of map movement"
"• Improved GPS location handling - follow-me button is now greyed out when location is unavailable",
"• Added approximate location fallback - if precise location is denied, app will use approximate location",
"• Higher frequency GPS updates when follow-me modes are active for smoother tracking (1-second updates vs 5-second)"
]
},
"2.1.2": {

View File

@@ -433,16 +433,18 @@ class _HomeScreenState extends State<HomeScreen> with TickerProviderStateMixin {
IconButton(
tooltip: _getFollowMeTooltip(appState.followMeMode),
icon: Icon(_getFollowMeIcon(appState.followMeMode)),
onPressed: () {
final oldMode = appState.followMeMode;
final newMode = _getNextFollowMeMode(oldMode);
debugPrint('[HomeScreen] Follow mode changed: $oldMode$newMode');
appState.setFollowMeMode(newMode);
// If enabling follow-me, retry location init in case permission was granted
if (newMode != FollowMeMode.off) {
_mapViewKey.currentState?.retryLocationInit();
}
},
onPressed: _mapViewKey.currentState?.hasLocation == true
? () {
final oldMode = appState.followMeMode;
final newMode = _getNextFollowMeMode(oldMode);
debugPrint('[HomeScreen] Follow mode changed: $oldMode$newMode');
appState.setFollowMeMode(newMode);
// If enabling follow-me, retry location init in case permission was granted
if (newMode != FollowMeMode.off) {
_mapViewKey.currentState?.retryLocationInit();
}
}
: null, // Grey out when no location
),
AnimatedBuilder(
animation: LocalizationService.instance,
@@ -490,6 +492,10 @@ class _HomeScreenState extends State<HomeScreen> with TickerProviderStateMixin {
_isNodeLimitActive = isLimited;
});
},
onLocationStatusChanged: () {
// Re-render when location status changes (for follow-me button state)
setState(() {});
},
onUserGesture: () {
// Only clear selected node if tag sheet is not open
// This prevents nodes from losing their grey-out when map is moved while viewing tags

View File

@@ -15,29 +15,91 @@ import '../../models/node_profile.dart';
class GpsController {
StreamSubscription<Position>? _positionSub;
LatLng? _currentLatLng;
bool _hasLocation = false;
Timer? _retryTimer;
/// Get the current GPS location (if available)
LatLng? get currentLocation => _currentLatLng;
/// Whether we currently have a valid GPS location
bool get hasLocation => _hasLocation;
/// Initialize GPS location tracking
Future<void> initializeLocation() async {
final perm = await Geolocator.requestPermission();
if (perm == LocationPermission.denied ||
perm == LocationPermission.deniedForever) {
debugPrint('[GpsController] Location permission denied');
// Check if location services are enabled first
bool serviceEnabled = await Geolocator.isLocationServiceEnabled();
if (!serviceEnabled) {
debugPrint('[GpsController] Location services disabled');
_hasLocation = false;
_scheduleRetry();
return;
}
_positionSub = Geolocator.getPositionStream().listen((Position position) {
final latLng = LatLng(position.latitude, position.longitude);
_currentLatLng = latLng;
debugPrint('[GpsController] GPS position updated: ${latLng.latitude}, ${latLng.longitude}');
});
final perm = await Geolocator.requestPermission();
debugPrint('[GpsController] Location permission result: $perm');
if (perm == LocationPermission.denied ||
perm == LocationPermission.deniedForever) {
debugPrint('[GpsController] Precise location permission denied, trying approximate location');
// Try approximate location as fallback
try {
await Geolocator.getCurrentPosition(
desiredAccuracy: LocationAccuracy.low,
timeLimit: const Duration(seconds: 10),
);
debugPrint('[GpsController] Approximate location available, proceeding with location stream');
// If we got here, approximate location works, continue with stream setup below
} catch (e) {
debugPrint('[GpsController] Approximate location also unavailable: $e');
_hasLocation = false;
_scheduleRetry();
return;
}
} else if (perm == LocationPermission.whileInUse || perm == LocationPermission.always) {
debugPrint('[GpsController] Location permission granted: $perm');
// Permission is granted, continue with normal setup
} else {
debugPrint('[GpsController] Unexpected permission state: $perm');
_hasLocation = false;
_scheduleRetry();
return;
}
_positionSub?.cancel(); // Cancel any existing subscription
debugPrint('[GpsController] Starting GPS position stream');
_positionSub = Geolocator.getPositionStream(
locationSettings: const LocationSettings(
accuracy: LocationAccuracy.high,
distanceFilter: 5, // Update when moved at least 5 meters (standard frequency)
),
).listen(
(Position position) {
final latLng = LatLng(position.latitude, position.longitude);
_currentLatLng = latLng;
if (!_hasLocation) {
debugPrint('[GpsController] GPS location acquired');
}
_hasLocation = true;
_cancelRetry(); // Got location, stop retrying
debugPrint('[GpsController] GPS position updated: ${latLng.latitude}, ${latLng.longitude} (accuracy: ${position.accuracy}m)');
},
onError: (error) {
debugPrint('[GpsController] Position stream error: $error');
if (_hasLocation) {
debugPrint('[GpsController] GPS location lost, starting retry attempts');
}
_hasLocation = false;
_currentLatLng = null;
_scheduleRetry(); // Lost location, start retrying
},
);
}
/// Retry location initialization (e.g., after permission granted)
Future<void> retryLocationInit() async {
debugPrint('[GpsController] Retrying location initialization');
debugPrint('[GpsController] Manual retry of location initialization');
_cancelRetry(); // Cancel automatic retries, this is a manual retry
await initializeLocation();
}
@@ -50,6 +112,9 @@ class GpsController {
}) {
debugPrint('[GpsController] Follow-me mode changed: $oldMode$newMode');
// Restart position stream with appropriate frequency for new mode
_restartPositionStream(newMode);
// Only act when follow-me is first enabled and we have a current location
if (newMode != FollowMeMode.off &&
oldMode == FollowMeMode.off &&
@@ -98,6 +163,8 @@ class GpsController {
}) {
final latLng = LatLng(position.latitude, position.longitude);
_currentLatLng = latLng;
_hasLocation = true;
_cancelRetry(); // Got location, stop any retries
// Notify that location was updated (for setState, etc.)
onLocationUpdated();
@@ -169,38 +236,184 @@ class GpsController {
VoidCallback? onMapMovedProgrammatically,
}) async {
final perm = await Geolocator.requestPermission();
if (perm == LocationPermission.denied ||
perm == LocationPermission.deniedForever) {
debugPrint('[GpsController] Location permission denied');
// Check if location services are enabled first
bool serviceEnabled = await Geolocator.isLocationServiceEnabled();
if (!serviceEnabled) {
debugPrint('[GpsController] Location services disabled');
_hasLocation = false;
_scheduleRetry();
return;
}
_positionSub = Geolocator.getPositionStream().listen((Position position) {
// Get the current follow-me mode from the app state each time
final currentFollowMeMode = getCurrentFollowMeMode();
final proximityAlertsEnabled = getProximityAlertsEnabled();
final proximityAlertDistance = getProximityAlertDistance();
final nearbyNodes = getNearbyNodes();
final enabledProfiles = getEnabledProfiles();
processPositionUpdate(
position: position,
followMeMode: currentFollowMeMode,
controller: controller,
onLocationUpdated: onLocationUpdated,
proximityAlertsEnabled: proximityAlertsEnabled,
proximityAlertDistance: proximityAlertDistance,
nearbyNodes: nearbyNodes,
enabledProfiles: enabledProfiles,
onMapMovedProgrammatically: onMapMovedProgrammatically,
);
final perm = await Geolocator.requestPermission();
debugPrint('[GpsController] Location permission result: $perm');
if (perm == LocationPermission.denied ||
perm == LocationPermission.deniedForever) {
debugPrint('[GpsController] Precise location permission denied, trying approximate location');
// Try approximate location as fallback
try {
await Geolocator.getCurrentPosition(
desiredAccuracy: LocationAccuracy.low,
timeLimit: const Duration(seconds: 10),
);
debugPrint('[GpsController] Approximate location available, proceeding with location stream');
// If we got here, approximate location works, continue with stream setup below
} catch (e) {
debugPrint('[GpsController] Approximate location also unavailable: $e');
_hasLocation = false;
_scheduleRetry();
return;
}
} else if (perm == LocationPermission.whileInUse || perm == LocationPermission.always) {
debugPrint('[GpsController] Location permission granted: $perm');
// Permission is granted, continue with normal setup
} else {
debugPrint('[GpsController] Unexpected permission state: $perm');
_hasLocation = false;
_scheduleRetry();
return;
}
_positionSub?.cancel(); // Cancel any existing subscription
debugPrint('[GpsController] Starting GPS position stream');
_positionSub = Geolocator.getPositionStream(
locationSettings: const LocationSettings(
accuracy: LocationAccuracy.high,
distanceFilter: 5, // Update when moved at least 5 meters (standard frequency)
),
).listen(
(Position position) {
if (!_hasLocation) {
debugPrint('[GpsController] GPS location acquired');
}
_hasLocation = true;
_cancelRetry(); // Got location, stop retrying
// Get the current follow-me mode from the app state each time
final currentFollowMeMode = getCurrentFollowMeMode();
final proximityAlertsEnabled = getProximityAlertsEnabled();
final proximityAlertDistance = getProximityAlertDistance();
final nearbyNodes = getNearbyNodes();
final enabledProfiles = getEnabledProfiles();
processPositionUpdate(
position: position,
followMeMode: currentFollowMeMode,
controller: controller,
onLocationUpdated: onLocationUpdated,
proximityAlertsEnabled: proximityAlertsEnabled,
proximityAlertDistance: proximityAlertDistance,
nearbyNodes: nearbyNodes,
enabledProfiles: enabledProfiles,
onMapMovedProgrammatically: onMapMovedProgrammatically,
);
},
onError: (error) {
debugPrint('[GpsController] Position stream error: $error');
if (_hasLocation) {
debugPrint('[GpsController] GPS location lost, starting retry attempts');
}
_hasLocation = false;
_currentLatLng = null;
onLocationUpdated(); // Notify UI that location was lost
_scheduleRetry(); // Lost location, start retrying
},
);
}
/// Schedule periodic retry attempts to get location
void _scheduleRetry() {
_retryTimer?.cancel();
_retryTimer = Timer.periodic(const Duration(seconds: 15), (timer) {
debugPrint('[GpsController] Automatic retry of location initialization (attempt ${timer.tick})');
initializeLocation(); // This will cancel the timer if successful
});
}
/// Cancel any scheduled retry attempts
void _cancelRetry() {
if (_retryTimer != null) {
debugPrint('[GpsController] Canceling location retry timer');
_retryTimer?.cancel();
_retryTimer = null;
}
}
/// Restart position stream with frequency optimized for follow-me mode
void _restartPositionStream(FollowMeMode followMeMode) {
if (_positionSub == null || !_hasLocation) {
// No active stream or no location - let normal initialization handle it
return;
}
_positionSub?.cancel();
// Use higher frequency when follow-me is enabled
if (followMeMode != FollowMeMode.off) {
debugPrint('[GpsController] Starting high-frequency GPS updates for follow-me mode');
_positionSub = Geolocator.getPositionStream(
locationSettings: const LocationSettings(
accuracy: LocationAccuracy.high,
distanceFilter: 1, // Update when moved at least 1 meter
),
).listen(
(Position position) {
final latLng = LatLng(position.latitude, position.longitude);
_currentLatLng = latLng;
if (!_hasLocation) {
debugPrint('[GpsController] GPS location acquired');
}
_hasLocation = true;
_cancelRetry(); // Got location, stop retrying
debugPrint('[GpsController] GPS position updated: ${latLng.latitude}, ${latLng.longitude} (accuracy: ${position.accuracy}m)');
},
onError: (error) {
debugPrint('[GpsController] Position stream error: $error');
if (_hasLocation) {
debugPrint('[GpsController] GPS location lost, starting retry attempts');
}
_hasLocation = false;
_currentLatLng = null;
_scheduleRetry(); // Lost location, start retrying
},
);
} else {
debugPrint('[GpsController] Starting standard-frequency GPS updates');
_positionSub = Geolocator.getPositionStream(
locationSettings: const LocationSettings(
accuracy: LocationAccuracy.high,
distanceFilter: 5, // Update when moved at least 5 meters
),
).listen(
(Position position) {
final latLng = LatLng(position.latitude, position.longitude);
_currentLatLng = latLng;
if (!_hasLocation) {
debugPrint('[GpsController] GPS location acquired');
}
_hasLocation = true;
_cancelRetry(); // Got location, stop retrying
debugPrint('[GpsController] GPS position updated: ${latLng.latitude}, ${latLng.longitude} (accuracy: ${position.accuracy}m)');
},
onError: (error) {
debugPrint('[GpsController] Position stream error: $error');
if (_hasLocation) {
debugPrint('[GpsController] GPS location lost, starting retry attempts');
}
_hasLocation = false;
_currentLatLng = null;
_scheduleRetry(); // Lost location, start retrying
},
);
}
}
/// Dispose of GPS resources
void dispose() {
_positionSub?.cancel();
_positionSub = null;
_cancelRetry();
debugPrint('[GpsController] GPS controller disposed');
}
}

View File

@@ -44,6 +44,7 @@ class MapView extends StatefulWidget {
this.onSuspectedLocationTap,
this.onSearchPressed,
this.onNodeLimitChanged,
this.onLocationStatusChanged,
});
final FollowMeMode followMeMode;
@@ -54,6 +55,7 @@ class MapView extends StatefulWidget {
final void Function(SuspectedLocation)? onSuspectedLocationTap;
final VoidCallback? onSearchPressed;
final void Function(bool isLimited)? onNodeLimitChanged;
final VoidCallback? onLocationStatusChanged;
@override
State<MapView> createState() => MapViewState();
@@ -121,7 +123,10 @@ class MapViewState extends State<MapView> {
_gpsController.initializeWithCallback(
followMeMode: widget.followMeMode,
controller: _controller,
onLocationUpdated: () => setState(() {}),
onLocationUpdated: () {
setState(() {});
widget.onLocationStatusChanged?.call(); // Notify parent about location status change
},
getCurrentFollowMeMode: () {
// Use mounted check to avoid calling context when widget is disposed
if (mounted) {
@@ -230,6 +235,9 @@ class MapViewState extends State<MapView> {
LatLng? getUserLocation() {
return _gpsController.currentLocation;
}
/// Whether we currently have a valid GPS location
bool get hasLocation => _gpsController.hasLocation;
/// Expose static methods from MapPositionManager for external access
static Future<void> clearStoredMapPosition() =>