reopen to last location

This commit is contained in:
stopflock
2025-08-28 15:36:31 -05:00
parent 728cef22af
commit 3026b88230
3 changed files with 146 additions and 28 deletions
+5 -3
View File
@@ -37,9 +37,11 @@ const Duration kDebounceCameraRefresh = Duration(milliseconds: 500);
const Duration kFollowMeAnimationDuration = Duration(milliseconds: 600);
const double kMinSpeedForRotationMps = 1.0; // Minimum speed (m/s) to apply rotation
// Last known location storage
const String kLastKnownLatKey = 'last_known_latitude';
const String kLastKnownLngKey = 'last_known_longitude';
// Last map location and settings storage
const String kLastMapLatKey = 'last_map_latitude';
const String kLastMapLngKey = 'last_map_longitude';
const String kLastMapZoomKey = 'last_map_zoom';
const String kFollowMeModeKey = 'follow_me_mode';
// Tile/OSM fetch retry parameters (for tunable backoff)
const int kTileFetchMaxAttempts = 3;
+14
View File
@@ -36,6 +36,18 @@ class _HomeScreenState extends State<HomeScreen> with TickerProviderStateMixin {
void initState() {
super.initState();
_mapController = AnimatedMapController(vsync: this);
// Load saved follow-me mode
_loadFollowMeMode();
}
/// Load the saved follow-me mode
Future<void> _loadFollowMeMode() async {
final savedMode = await MapViewState.loadFollowMeMode();
if (mounted) {
setState(() {
_followMeMode = savedMode;
});
}
}
@override
@@ -132,6 +144,8 @@ class _HomeScreenState extends State<HomeScreen> with TickerProviderStateMixin {
_followMeMode = _getNextFollowMeMode();
debugPrint('[HomeScreen] Follow mode changed: $oldMode$_followMeMode');
});
// Save the new follow-me mode
MapViewState.saveFollowMeMode(_followMeMode);
// If enabling follow-me, retry location init in case permission was granted
if (_followMeMode != FollowMeMode.off) {
_mapViewKey.currentState?.retryLocationInit();
+127 -25
View File
@@ -45,10 +45,13 @@ class MapViewState extends State<MapView> {
late final AnimatedMapController _controller;
final Debouncer _cameraDebounce = Debouncer(kDebounceCameraRefresh);
final Debouncer _tileDebounce = Debouncer(const Duration(milliseconds: 150));
final Debouncer _mapPositionDebounce = Debouncer(const Duration(milliseconds: 1000));
StreamSubscription<Position>? _positionSub;
LatLng? _currentLatLng;
LatLng? _initialLocation;
double? _initialZoom;
bool _hasMovedToInitialLocation = false;
late final CameraProviderWithCache _cameraProvider;
late final SimpleTileHttpClient _tileHttpClient;
@@ -71,8 +74,13 @@ class MapViewState extends State<MapView> {
_controller = widget.controller;
_tileHttpClient = SimpleTileHttpClient();
// Load last known location before initializing GPS
_loadInitialLocation();
// Load last map position before initializing GPS
_loadLastMapPosition().then((_) {
// Move to last known position after loading and widget is built
WidgetsBinding.instance.addPostFrameCallback((_) {
_moveToInitialLocationIfNeeded();
});
});
_initLocation();
// Set up camera overlay caching
@@ -85,9 +93,41 @@ class MapViewState extends State<MapView> {
});
}
/// Load the initial location (last known location or default)
Future<void> _loadInitialLocation() async {
_initialLocation = await _loadLastKnownLocation();
/// Move to initial location if we have one and haven't moved yet
void _moveToInitialLocationIfNeeded() {
if (!_hasMovedToInitialLocation && _initialLocation != null && mounted) {
try {
final zoom = _initialZoom ?? 15.0;
// Double-check coordinates are valid before moving
if (_isValidCoordinate(_initialLocation!.latitude) &&
_isValidCoordinate(_initialLocation!.longitude) &&
_isValidZoom(zoom)) {
_controller.mapController.move(_initialLocation!, zoom);
_hasMovedToInitialLocation = true;
debugPrint('[MapView] Moved to initial location: ${_initialLocation!.latitude}, ${_initialLocation!.longitude}');
} else {
debugPrint('[MapView] Invalid initial location, not moving: ${_initialLocation!.latitude}, ${_initialLocation!.longitude}, zoom: $zoom');
}
} catch (e) {
debugPrint('[MapView] Failed to move to initial location: $e');
}
}
}
/// Validate that a coordinate value is valid (not NaN, not infinite, within bounds)
bool _isValidCoordinate(double value) {
return !value.isNaN &&
!value.isInfinite &&
value >= -180.0 &&
value <= 180.0;
}
/// Validate that a zoom level is valid
bool _isValidZoom(double zoom) {
return !zoom.isNaN &&
!zoom.isInfinite &&
zoom >= 1.0 &&
zoom <= 25.0;
}
@@ -97,6 +137,7 @@ class MapViewState extends State<MapView> {
_positionSub?.cancel();
_cameraDebounce.dispose();
_tileDebounce.dispose();
_mapPositionDebounce.dispose();
_cameraProvider.removeListener(_onCamerasUpdated);
_tileHttpClient.close();
super.dispose();
@@ -112,34 +153,88 @@ class MapViewState extends State<MapView> {
_initLocation();
}
/// Save the last known location to persistent storage
Future<void> _saveLastKnownLocation(LatLng location) async {
/// Save the last map position to persistent storage
Future<void> _saveLastMapPosition(LatLng location, double zoom) async {
try {
// Validate coordinates and zoom before saving
if (!_isValidCoordinate(location.latitude) ||
!_isValidCoordinate(location.longitude) ||
!_isValidZoom(zoom)) {
debugPrint('[MapView] Invalid map position, not saving: lat=${location.latitude}, lng=${location.longitude}, zoom=$zoom');
return;
}
final prefs = await SharedPreferences.getInstance();
await prefs.setDouble(kLastKnownLatKey, location.latitude);
await prefs.setDouble(kLastKnownLngKey, location.longitude);
debugPrint('[MapView] Saved last known location: ${location.latitude}, ${location.longitude}');
await prefs.setDouble(kLastMapLatKey, location.latitude);
await prefs.setDouble(kLastMapLngKey, location.longitude);
await prefs.setDouble(kLastMapZoomKey, zoom);
debugPrint('[MapView] Saved last map position: ${location.latitude}, ${location.longitude}, zoom: $zoom');
} catch (e) {
debugPrint('[MapView] Failed to save last known location: $e');
debugPrint('[MapView] Failed to save last map position: $e');
}
}
/// Load the last known location from persistent storage
Future<LatLng?> _loadLastKnownLocation() async {
/// Load the last map position from persistent storage
Future<void> _loadLastMapPosition() async {
try {
final prefs = await SharedPreferences.getInstance();
final lat = prefs.getDouble(kLastKnownLatKey);
final lng = prefs.getDouble(kLastKnownLngKey);
final lat = prefs.getDouble(kLastMapLatKey);
final lng = prefs.getDouble(kLastMapLngKey);
final zoom = prefs.getDouble(kLastMapZoomKey);
if (lat != null && lng != null) {
final location = LatLng(lat, lng);
debugPrint('[MapView] Loaded last known location: ${location.latitude}, ${location.longitude}');
return location;
if (lat != null && lng != null &&
_isValidCoordinate(lat) && _isValidCoordinate(lng)) {
final validZoom = zoom != null && _isValidZoom(zoom) ? zoom : 15.0;
_initialLocation = LatLng(lat, lng);
_initialZoom = validZoom;
debugPrint('[MapView] Loaded last map position: ${_initialLocation!.latitude}, ${_initialLocation!.longitude}, zoom: $_initialZoom');
} else {
debugPrint('[MapView] Invalid saved coordinates, using defaults');
}
} catch (e) {
debugPrint('[MapView] Failed to load last known location: $e');
debugPrint('[MapView] Failed to load last map position: $e');
}
}
/// Save the follow-me mode to persistent storage
static Future<void> saveFollowMeMode(FollowMeMode mode) async {
try {
final prefs = await SharedPreferences.getInstance();
await prefs.setInt(kFollowMeModeKey, mode.index);
debugPrint('[MapView] Saved follow-me mode: $mode');
} catch (e) {
debugPrint('[MapView] Failed to save follow-me mode: $e');
}
}
/// Load the follow-me mode from persistent storage
static Future<FollowMeMode> loadFollowMeMode() async {
try {
final prefs = await SharedPreferences.getInstance();
final modeIndex = prefs.getInt(kFollowMeModeKey);
if (modeIndex != null && modeIndex < FollowMeMode.values.length) {
final mode = FollowMeMode.values[modeIndex];
debugPrint('[MapView] Loaded follow-me mode: $mode');
return mode;
}
} catch (e) {
debugPrint('[MapView] Failed to load follow-me mode: $e');
}
// Default to northUp if no saved mode
return FollowMeMode.northUp;
}
/// Clear any stored map position (useful for recovery from invalid data)
static Future<void> clearStoredMapPosition() async {
try {
final prefs = await SharedPreferences.getInstance();
await prefs.remove(kLastMapLatKey);
await prefs.remove(kLastMapLngKey);
await prefs.remove(kLastMapZoomKey);
debugPrint('[MapView] Cleared stored map position');
} catch (e) {
debugPrint('[MapView] Failed to clear stored map position: $e');
}
return null;
}
@@ -214,9 +309,6 @@ class MapViewState extends State<MapView> {
final latLng = LatLng(position.latitude, position.longitude);
setState(() => _currentLatLng = latLng);
// Save this as the last known location
_saveLastKnownLocation(latLng);
// Back to original pattern - directly check widget parameter
if (widget.followMeMode != FollowMeMode.off) {
WidgetsBinding.instance.addPostFrameCallback((_) {
@@ -405,7 +497,7 @@ class MapViewState extends State<MapView> {
mapController: _controller.mapController,
options: MapOptions(
initialCenter: _currentLatLng ?? _initialLocation ?? LatLng(37.7749, -122.4194),
initialZoom: 15,
initialZoom: _initialZoom ?? 15,
maxZoom: 19,
onPositionChanged: (pos, gesture) {
setState(() {}); // Instant UI update for zoom, etc.
@@ -432,6 +524,16 @@ class MapViewState extends State<MapView> {
}
_lastZoom = currentZoom;
// Save map position (debounced to avoid excessive writes)
_mapPositionDebounce(() {
// Only save if position and zoom are valid
if (_isValidCoordinate(pos.center.latitude) &&
_isValidCoordinate(pos.center.longitude) &&
_isValidZoom(pos.zoom)) {
_saveLastMapPosition(pos.center, pos.zoom);
}
});
// Request more cameras on any map movement/zoom at valid zoom level (slower debounce)
if (pos.zoom >= 10) {
_cameraDebounce(_refreshCamerasFromProvider);