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,
);