From e153218b0103a0b2038e6cda66fbc170849a9889 Mon Sep 17 00:00:00 2001 From: Janez T Date: Mon, 13 Oct 2025 23:02:22 +0200 Subject: [PATCH] Add flutter_compass and shared_preferences_foundation dependencies; update Info.plist for motion usage description --- ios/Podfile.lock | 13 +++ ios/Runner/Info.plist | 2 + lib/screens/map_tab.dart | 201 +++++++++++++++++++++++++++++---------- 3 files changed, 166 insertions(+), 50 deletions(-) diff --git a/ios/Podfile.lock b/ios/Podfile.lock index 892646c..2d271d1 100644 --- a/ios/Podfile.lock +++ b/ios/Podfile.lock @@ -37,6 +37,8 @@ PODS: - flutter_blue_plus_darwin (0.0.2): - Flutter - FlutterMacOS + - flutter_compass (0.0.1): + - Flutter - geolocator_apple (1.2.0): - Flutter - FlutterMacOS @@ -56,18 +58,23 @@ PODS: - SDWebImage/Core (5.21.3) - share_plus (0.0.1): - Flutter + - shared_preferences_foundation (0.0.1): + - Flutter + - FlutterMacOS - SwiftyGif (5.4.5) DEPENDENCIES: - file_picker (from `.symlinks/plugins/file_picker/ios`) - Flutter (from `Flutter`) - flutter_blue_plus_darwin (from `.symlinks/plugins/flutter_blue_plus_darwin/darwin`) + - flutter_compass (from `.symlinks/plugins/flutter_compass/ios`) - geolocator_apple (from `.symlinks/plugins/geolocator_apple/darwin`) - objectbox_flutter_libs (from `.symlinks/plugins/objectbox_flutter_libs/ios`) - package_info_plus (from `.symlinks/plugins/package_info_plus/ios`) - path_provider_foundation (from `.symlinks/plugins/path_provider_foundation/darwin`) - permission_handler_apple (from `.symlinks/plugins/permission_handler_apple/ios`) - share_plus (from `.symlinks/plugins/share_plus/ios`) + - shared_preferences_foundation (from `.symlinks/plugins/shared_preferences_foundation/darwin`) SPEC REPOS: trunk: @@ -84,6 +91,8 @@ EXTERNAL SOURCES: :path: Flutter flutter_blue_plus_darwin: :path: ".symlinks/plugins/flutter_blue_plus_darwin/darwin" + flutter_compass: + :path: ".symlinks/plugins/flutter_compass/ios" geolocator_apple: :path: ".symlinks/plugins/geolocator_apple/darwin" objectbox_flutter_libs: @@ -96,6 +105,8 @@ EXTERNAL SOURCES: :path: ".symlinks/plugins/permission_handler_apple/ios" share_plus: :path: ".symlinks/plugins/share_plus/ios" + shared_preferences_foundation: + :path: ".symlinks/plugins/shared_preferences_foundation/darwin" SPEC CHECKSUMS: DKImagePickerController: 946cec48c7873164274ecc4624d19e3da4c1ef3c @@ -103,6 +114,7 @@ SPEC CHECKSUMS: file_picker: a0560bc09d61de87f12d246fc47d2119e6ef37be Flutter: cabc95a1d2626b1b06e7179b784ebcf0c0cde467 flutter_blue_plus_darwin: 20a08bfeaa0f7804d524858d3d8744bcc1b6dbc3 + flutter_compass: b236ab69b61545cce89fd58527f401a7587d5cc1 geolocator_apple: ab36aa0e8b7d7a2d7639b3b4e48308394e8cef5e ObjectBox: 7da4aceb5013d041bfafdbc6d744a26918b09757 objectbox_flutter_libs: 09b1dec1b4cd27bf1a5f9bae7ccaa7e43588bf31 @@ -111,6 +123,7 @@ SPEC CHECKSUMS: permission_handler_apple: 4ed2196e43d0651e8ff7ca3483a069d469701f2d SDWebImage: 16309af6d214ba3f77a7c6f6fdda888cb313a50a share_plus: 50da8cb520a8f0f65671c6c6a99b3617ed10a58a + shared_preferences_foundation: 9e1978ff2562383bd5676f64ec4e9aa8fa06a6f7 SwiftyGif: 706c60cf65fa2bc5ee0313beece843c8eb8194d4 PODFILE CHECKSUM: 3c63482e143d1b91d2d2560aee9fb04ecc74ac7e diff --git a/ios/Runner/Info.plist b/ios/Runner/Info.plist index 72875ed..d146971 100644 --- a/ios/Runner/Info.plist +++ b/ios/Runner/Info.plist @@ -57,5 +57,7 @@ MeshCore SAR needs precise location for accurate positioning in SAR operations NSLocationDefaultAccuracyReduced + NSMotionUsageDescription + MeshCore SAR needs access to the compass to show your heading direction on the map diff --git a/lib/screens/map_tab.dart b/lib/screens/map_tab.dart index 7f15ca7..f65037b 100644 --- a/lib/screens/map_tab.dart +++ b/lib/screens/map_tab.dart @@ -26,7 +26,7 @@ class MapTab extends StatefulWidget { State createState() => _MapTabState(); } -class _MapTabState extends State { +class _MapTabState extends State with AutomaticKeepAliveClientMixin { final MapController _mapController = MapController(); final TileCacheService _tileCache = TileCacheService(); bool _isInitialized = false; @@ -39,10 +39,17 @@ class _MapTabState extends State { StreamSubscription? _positionStreamSubscription; StreamSubscription? _compassStreamSubscription; + // Saved map position (loaded from SharedPreferences) + LatLng? _savedMapCenter; + double? _savedMapZoom; + // Default center point (will be updated based on markers) static const LatLng _defaultCenter = LatLng(46.0569, 14.5058); // Ljubljana, Slovenia static const double _defaultZoom = 13.0; + @override + bool get wantKeepAlive => true; + @override void initState() { super.initState(); @@ -59,35 +66,52 @@ class _MapTabState extends State { } void _startCompassTracking() { - // Start listening to compass events - _compassStreamSubscription = FlutterCompass.events?.listen((CompassEvent event) { - if (mounted && event.heading != null) { - setState(() { - _compassHeading = event.heading; - }); + final compassStream = FlutterCompass.events; + if (compassStream == null) { + return; + } - // Rotate map if rotation mode is enabled and we have compass heading - if (_rotateMarkerWithHeading && event.heading != null) { - debugPrint('Rotating map to compass heading: ${event.heading}'); - // Use rotateAroundPoint to set absolute rotation - final camera = _mapController.camera; - _mapController.moveAndRotate( - camera.center, - camera.zoom, - -event.heading!, - ); + // Start listening to compass events + _compassStreamSubscription = compassStream.listen( + (CompassEvent event) { + if (mounted && event.heading != null) { + setState(() { + _compassHeading = event.heading; + }); + + // Rotate map if rotation mode is enabled and we have compass heading + if (_rotateMarkerWithHeading && event.heading != null) { + // Use moveAndRotate to set absolute rotation + final camera = _mapController.camera; + _mapController.moveAndRotate( + camera.center, + camera.zoom, + -event.heading!, + ); + } } - } - }); + }, + ); } Future _loadSettings() async { final prefs = await SharedPreferences.getInstance(); if (mounted) { + // Load last map position if available + final lastLat = prefs.getDouble('map_last_latitude'); + final lastLon = prefs.getDouble('map_last_longitude'); + final lastZoom = prefs.getDouble('map_last_zoom'); + setState(() { _showLegend = prefs.getBool('map_show_legend') ?? false; _rotateMarkerWithHeading = prefs.getBool('map_rotate_with_heading') ?? false; _gpsUpdateDistance = prefs.getDouble('map_gps_update_distance') ?? 3.0; + + // Store saved position for use in build + if (lastLat != null && lastLon != null && lastZoom != null) { + _savedMapCenter = LatLng(lastLat, lastLon); + _savedMapZoom = lastZoom; + } }); } } @@ -99,6 +123,14 @@ class _MapTabState extends State { await prefs.setDouble('map_gps_update_distance', _gpsUpdateDistance); } + Future _saveMapPosition() async { + final prefs = await SharedPreferences.getInstance(); + final camera = _mapController.camera; + await prefs.setDouble('map_last_latitude', camera.center.latitude); + await prefs.setDouble('map_last_longitude', camera.center.longitude); + await prefs.setDouble('map_last_zoom', camera.zoom); + } + Future _requestLocationPermission() async { bool serviceEnabled = await Geolocator.isLocationServiceEnabled(); if (!serviceEnabled) { @@ -142,8 +174,6 @@ class _MapTabState extends State { ), ).listen((Position position) { if (mounted) { - debugPrint('Position update - Heading: ${position.heading}, Speed: ${position.speed}'); - setState(() { _currentPosition = position; }); @@ -151,7 +181,6 @@ class _MapTabState extends State { // Rotate map if rotation mode is enabled and heading is available // Heading of -1.0 means heading is unavailable if (_rotateMarkerWithHeading && position.heading != null && position.heading >= 0) { - debugPrint('Rotating map to heading: ${position.heading}'); final camera = _mapController.camera; _mapController.moveAndRotate( camera.center, @@ -195,6 +224,9 @@ class _MapTabState extends State { @override void dispose() { + // Save map position before disposing + _saveMapPosition(); + final mapProvider = context.read(); mapProvider.removeListener(_handleMapNavigation); _positionStreamSubscription?.cancel(); @@ -441,8 +473,8 @@ class _MapTabState extends State { showDialog( context: context, builder: (context) => _DetailedCompassDialog( - currentPosition: _currentPosition, - currentHeading: _currentHeading, + initialPosition: _currentPosition, + initialHeading: _currentHeading, contacts: contacts, ), ); @@ -480,6 +512,7 @@ class _MapTabState extends State { @override Widget build(BuildContext context) { + super.build(context); // Required for AutomaticKeepAliveClientMixin return Consumer2( builder: (context, contactsProvider, messagesProvider, child) { final contactsWithLocation = contactsProvider.chatContactsWithLocation; @@ -493,13 +526,20 @@ class _MapTabState extends State { ? FlutterMap( mapController: _mapController, options: MapOptions( - initialCenter: center, - initialZoom: _defaultZoom, + // Use saved position if available, otherwise use calculated center + initialCenter: _savedMapCenter ?? center, + initialZoom: _savedMapZoom ?? _defaultZoom, minZoom: 5, maxZoom: 18, interactionOptions: const InteractionOptions( flags: InteractiveFlag.all, ), + onMapEvent: (event) { + // Save map position when user stops panning/zooming + if (event is MapEventMoveEnd || event is MapEventScrollWheelZoom) { + _saveMapPosition(); + } + }, ), children: [ TileLayer( @@ -875,19 +915,80 @@ class _CompassRosePainter extends CustomPainter { } // Detailed Compass Dialog -class _DetailedCompassDialog extends StatelessWidget { - final Position? currentPosition; - final double? currentHeading; +class _DetailedCompassDialog extends StatefulWidget { + final Position? initialPosition; + final double? initialHeading; final List contacts; const _DetailedCompassDialog({ - required this.currentPosition, - required this.currentHeading, + required this.initialPosition, + required this.initialHeading, required this.contacts, }); + @override + State<_DetailedCompassDialog> createState() => _DetailedCompassDialogState(); +} + +class _DetailedCompassDialogState extends State<_DetailedCompassDialog> { + double? _currentHeading; + Position? _currentPosition; + StreamSubscription? _compassSubscription; + StreamSubscription? _positionSubscription; + + @override + void initState() { + super.initState(); + _currentHeading = widget.initialHeading; + _currentPosition = widget.initialPosition; + + // Subscribe to compass updates + final compassStream = FlutterCompass.events; + if (compassStream != null) { + _compassSubscription = compassStream.listen((event) { + if (mounted && event.heading != null) { + setState(() { + _currentHeading = event.heading; + }); + } + }); + } + + // Subscribe to position updates + _positionSubscription = Geolocator.getPositionStream( + locationSettings: const LocationSettings( + accuracy: LocationAccuracy.best, + distanceFilter: 1, + ), + ).listen((position) { + if (mounted) { + setState(() { + _currentPosition = position; + }); + } + }); + } + + @override + void dispose() { + _compassSubscription?.cancel(); + _positionSubscription?.cancel(); + super.dispose(); + } + + // Get current heading (prefer compass over GPS) + double? get currentHeading { + if (_currentHeading != null) return _currentHeading; + if (_currentPosition?.heading != null && _currentPosition!.heading >= 0) { + return _currentPosition!.heading; + } + return null; + } + @override Widget build(BuildContext context) { + final heading = currentHeading; + final position = _currentPosition; return Dialog( backgroundColor: Colors.transparent, child: Container( @@ -917,51 +1018,51 @@ class _DetailedCompassDialog extends StatelessWidget { ), const SizedBox(height: 16), // Heading and Elevation info - _buildInfoRow(context), + _buildInfoRow(context, heading, position), const SizedBox(height: 24), // Large compass SizedBox( width: 300, height: 300, child: _DetailedCompassPainter( - heading: currentHeading ?? 0, - hasHeading: currentHeading != null, - currentPosition: currentPosition, - contacts: contacts, + heading: heading ?? 0, + hasHeading: heading != null, + currentPosition: position, + contacts: widget.contacts, ), ), const SizedBox(height: 16), // Contacts list - if (contacts.isNotEmpty) _buildContactsList(context), + if (widget.contacts.isNotEmpty) _buildContactsList(context, heading, position), ], ), ), ); } - Widget _buildInfoRow(BuildContext context) { + Widget _buildInfoRow(BuildContext context, double? heading, Position? position) { return Row( mainAxisAlignment: MainAxisAlignment.spaceAround, children: [ _buildInfoCard( context, 'Heading', - currentHeading != null ? '${currentHeading!.round()}°' : '--', + heading != null ? '${heading.round()}°' : '--', Icons.explore, ), _buildInfoCard( context, 'Elevation', - currentPosition?.altitude != null - ? '${currentPosition!.altitude.round()}m' + position?.altitude != null + ? '${position!.altitude.round()}m' : '--', Icons.terrain, ), _buildInfoCard( context, 'Accuracy', - currentPosition?.accuracy != null - ? '±${currentPosition!.accuracy.round()}m' + position?.accuracy != null + ? '±${position!.accuracy.round()}m' : '--', Icons.gps_fixed, ), @@ -989,25 +1090,25 @@ class _DetailedCompassDialog extends StatelessWidget { ); } - Widget _buildContactsList(BuildContext context) { - if (currentPosition == null) { + Widget _buildContactsList(BuildContext context, double? heading, Position? position) { + if (position == null) { return const Text('Location unavailable'); } // Calculate bearings and distances - final contactsWithBearing = contacts.map((contact) { + final contactsWithBearing = widget.contacts.map((contact) { if (contact.displayLocation == null) return null; final bearing = _calculateBearing( - currentPosition!.latitude, - currentPosition!.longitude, + position.latitude, + position.longitude, contact.displayLocation!.latitude, contact.displayLocation!.longitude, ); final distance = _calculateDistance( - currentPosition!.latitude, - currentPosition!.longitude, + position.latitude, + position.longitude, contact.displayLocation!.latitude, contact.displayLocation!.longitude, );