From 1b3c3e620c679536eaa813d41caec8334170946a Mon Sep 17 00:00:00 2001 From: stopflock Date: Thu, 28 Aug 2025 19:09:38 -0500 Subject: [PATCH] put map position save/restore into its own file --- lib/widgets/map/map_position_manager.dart | 148 +++++++++++++++++++++ lib/widgets/map_view.dart | 149 +++------------------- 2 files changed, 165 insertions(+), 132 deletions(-) create mode 100644 lib/widgets/map/map_position_manager.dart diff --git a/lib/widgets/map/map_position_manager.dart b/lib/widgets/map/map_position_manager.dart new file mode 100644 index 0000000..53f2b08 --- /dev/null +++ b/lib/widgets/map/map_position_manager.dart @@ -0,0 +1,148 @@ +import 'package:flutter/material.dart'; +import 'package:flutter_map_animations/flutter_map_animations.dart'; +import 'package:latlong2/latlong.dart'; +import 'package:shared_preferences/shared_preferences.dart'; + +import '../../dev_config.dart'; +import '../../screens/home_screen.dart' show FollowMeMode; + +/// Manages map position persistence, initial positioning, and follow-me mode storage. +/// Handles saving/loading last map position and moving to initial locations. +class MapPositionManager { + LatLng? _initialLocation; + double? _initialZoom; + bool _hasMovedToInitialLocation = false; + + /// Get the initial location (if any was loaded) + LatLng? get initialLocation => _initialLocation; + + /// Get the initial zoom (if any was loaded) + double? get initialZoom => _initialZoom; + + /// Whether we've already moved to the initial location + bool get hasMovedToInitialLocation => _hasMovedToInitialLocation; + + /// Load the last map position from persistent storage. + /// Call this during initialization to set up initial location. + Future loadLastMapPosition() async { + try { + final prefs = await SharedPreferences.getInstance(); + final lat = prefs.getDouble(kLastMapLatKey); + final lng = prefs.getDouble(kLastMapLngKey); + final zoom = prefs.getDouble(kLastMapZoomKey); + + 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('[MapPositionManager] Loaded last map position: ${_initialLocation!.latitude}, ${_initialLocation!.longitude}, zoom: $_initialZoom'); + } else { + debugPrint('[MapPositionManager] Invalid saved coordinates, using defaults'); + } + } catch (e) { + debugPrint('[MapPositionManager] Failed to load last map position: $e'); + } + } + + /// Move to initial location if we have one and haven't moved yet. + /// Call this after the map controller is ready. + void moveToInitialLocationIfNeeded(AnimatedMapController controller) { + if (!_hasMovedToInitialLocation && _initialLocation != null) { + 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('[MapPositionManager] Moved to initial location: ${_initialLocation!.latitude}, ${_initialLocation!.longitude}'); + } else { + debugPrint('[MapPositionManager] Invalid initial location, not moving: ${_initialLocation!.latitude}, ${_initialLocation!.longitude}, zoom: $zoom'); + } + } catch (e) { + debugPrint('[MapPositionManager] Failed to move to initial location: $e'); + } + } + } + + /// Save the current map position to persistent storage. + /// Call this when the map position changes. + Future saveMapPosition(LatLng location, double zoom) async { + try { + // Validate coordinates and zoom before saving + if (!_isValidCoordinate(location.latitude) || + !_isValidCoordinate(location.longitude) || + !_isValidZoom(zoom)) { + debugPrint('[MapPositionManager] Invalid map position, not saving: lat=${location.latitude}, lng=${location.longitude}, zoom=$zoom'); + return; + } + + final prefs = await SharedPreferences.getInstance(); + await prefs.setDouble(kLastMapLatKey, location.latitude); + await prefs.setDouble(kLastMapLngKey, location.longitude); + await prefs.setDouble(kLastMapZoomKey, zoom); + debugPrint('[MapPositionManager] Saved last map position: ${location.latitude}, ${location.longitude}, zoom: $zoom'); + } catch (e) { + debugPrint('[MapPositionManager] Failed to save last map position: $e'); + } + } + + /// Save the follow-me mode to persistent storage + static Future saveFollowMeMode(FollowMeMode mode) async { + try { + final prefs = await SharedPreferences.getInstance(); + await prefs.setInt(kFollowMeModeKey, mode.index); + debugPrint('[MapPositionManager] Saved follow-me mode: $mode'); + } catch (e) { + debugPrint('[MapPositionManager] Failed to save follow-me mode: $e'); + } + } + + /// Load the follow-me mode from persistent storage + static Future 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('[MapPositionManager] Loaded follow-me mode: $mode'); + return mode; + } + } catch (e) { + debugPrint('[MapPositionManager] 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 clearStoredMapPosition() async { + try { + final prefs = await SharedPreferences.getInstance(); + await prefs.remove(kLastMapLatKey); + await prefs.remove(kLastMapLngKey); + await prefs.remove(kLastMapZoomKey); + debugPrint('[MapPositionManager] Cleared stored map position'); + } catch (e) { + debugPrint('[MapPositionManager] Failed to clear stored map position: $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; + } +} \ No newline at end of file diff --git a/lib/widgets/map_view.dart b/lib/widgets/map_view.dart index ec0a812..cdbe9fc 100644 --- a/lib/widgets/map_view.dart +++ b/lib/widgets/map_view.dart @@ -7,7 +7,6 @@ import 'package:geolocator/geolocator.dart'; import 'package:provider/provider.dart'; import 'package:http/http.dart' as http; import 'package:collection/collection.dart'; -import 'package:shared_preferences/shared_preferences.dart'; import '../app_state.dart'; import '../services/offline_area_service.dart'; @@ -21,6 +20,7 @@ import 'camera_provider_with_cache.dart'; import 'map/camera_markers.dart'; import 'map/direction_cones.dart'; import 'map/map_overlays.dart'; +import 'map/map_position_manager.dart'; import 'network_status_indicator.dart'; import '../dev_config.dart'; import '../screens/home_screen.dart' show FollowMeMode; @@ -49,12 +49,10 @@ class MapViewState extends State { StreamSubscription? _positionSub; LatLng? _currentLatLng; - LatLng? _initialLocation; - double? _initialZoom; - bool _hasMovedToInitialLocation = false; late final CameraProviderWithCache _cameraProvider; late final SimpleTileHttpClient _tileHttpClient; + late final MapPositionManager _positionManager; // Track profile changes to trigger camera refresh List? _lastEnabledProfiles; @@ -73,12 +71,13 @@ class MapViewState extends State { OfflineAreaService(); _controller = widget.controller; _tileHttpClient = SimpleTileHttpClient(); + _positionManager = MapPositionManager(); // Load last map position before initializing GPS - _loadLastMapPosition().then((_) { + _positionManager.loadLastMapPosition().then((_) { // Move to last known position after loading and widget is built WidgetsBinding.instance.addPostFrameCallback((_) { - _moveToInitialLocationIfNeeded(); + _positionManager.moveToInitialLocationIfNeeded(_controller); }); }); _initLocation(); @@ -93,42 +92,7 @@ class MapViewState extends State { }); } - /// 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; - } @@ -153,89 +117,15 @@ class MapViewState extends State { _initLocation(); } - /// Save the last map position to persistent storage - Future _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(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 map position: $e'); - } - } - - /// Load the last map position from persistent storage - Future _loadLastMapPosition() async { - try { - final prefs = await SharedPreferences.getInstance(); - final lat = prefs.getDouble(kLastMapLatKey); - final lng = prefs.getDouble(kLastMapLngKey); - final zoom = prefs.getDouble(kLastMapZoomKey); - - 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 map position: $e'); - } - } - - /// Save the follow-me mode to persistent storage - static Future 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 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 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'); - } - } + /// Expose static methods from MapPositionManager for external access + static Future saveFollowMeMode(FollowMeMode mode) => + MapPositionManager.saveFollowMeMode(mode); + + static Future loadFollowMeMode() => + MapPositionManager.loadFollowMeMode(); + + static Future clearStoredMapPosition() => + MapPositionManager.clearStoredMapPosition(); @@ -496,8 +386,8 @@ class MapViewState extends State { key: ValueKey('map_${appState.offlineMode}_${appState.selectedTileType?.id ?? 'none'}_$_mapRebuildKey'), mapController: _controller.mapController, options: MapOptions( - initialCenter: _currentLatLng ?? _initialLocation ?? LatLng(37.7749, -122.4194), - initialZoom: _initialZoom ?? 15, + initialCenter: _currentLatLng ?? _positionManager.initialLocation ?? LatLng(37.7749, -122.4194), + initialZoom: _positionManager.initialZoom ?? 15, maxZoom: 19, onPositionChanged: (pos, gesture) { setState(() {}); // Instant UI update for zoom, etc. @@ -526,12 +416,7 @@ class MapViewState extends State { // 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); - } + _positionManager.saveMapPosition(pos.center, pos.zoom); }); // Request more cameras on any map movement/zoom at valid zoom level (slower debounce)