Add compass functionality and shared preferences support to map tab

This commit is contained in:
Janez T
2025-10-13 22:43:19 +02:00
parent 823a163123
commit 1241505a94
4 changed files with 285 additions and 4 deletions

View File

@@ -1,9 +1,12 @@
import 'dart:async'; import 'dart:async';
import 'dart:math';
import 'package:flutter/material.dart'; import 'package:flutter/material.dart';
import 'package:flutter_map/flutter_map.dart'; import 'package:flutter_map/flutter_map.dart';
import 'package:latlong2/latlong.dart'; import 'package:latlong2/latlong.dart';
import 'package:provider/provider.dart'; import 'package:provider/provider.dart';
import 'package:geolocator/geolocator.dart'; import 'package:geolocator/geolocator.dart';
import 'package:flutter_compass/flutter_compass.dart';
import 'package:shared_preferences/shared_preferences.dart';
import '../providers/contacts_provider.dart'; import '../providers/contacts_provider.dart';
import '../providers/messages_provider.dart'; import '../providers/messages_provider.dart';
import '../providers/map_provider.dart'; import '../providers/map_provider.dart';
@@ -28,9 +31,12 @@ class _MapTabState extends State<MapTab> {
bool _isInitialized = false; bool _isInitialized = false;
MapLayer _currentLayer = MapLayer.openStreetMap; MapLayer _currentLayer = MapLayer.openStreetMap;
Position? _currentPosition; Position? _currentPosition;
bool _showLegend = true; double? _compassHeading; // Compass sensor heading
bool _rotateMarkerWithHeading = false; // Toggle for rotation
bool _showLegend = false;
double _gpsUpdateDistance = 3.0; // meters double _gpsUpdateDistance = 3.0; // meters
StreamSubscription<Position>? _positionStreamSubscription; StreamSubscription<Position>? _positionStreamSubscription;
StreamSubscription<CompassEvent>? _compassStreamSubscription;
// Default center point (will be updated based on markers) // Default center point (will be updated based on markers)
static const LatLng _defaultCenter = LatLng(46.0569, 14.5058); // Ljubljana, Slovenia static const LatLng _defaultCenter = LatLng(46.0569, 14.5058); // Ljubljana, Slovenia
@@ -39,8 +45,10 @@ class _MapTabState extends State<MapTab> {
@override @override
void initState() { void initState() {
super.initState(); super.initState();
_loadSettings();
_initializeTileCache(); _initializeTileCache();
_requestLocationPermission(); _requestLocationPermission();
_startCompassTracking();
// Listen to map provider for navigation requests // Listen to map provider for navigation requests
WidgetsBinding.instance.addPostFrameCallback((_) { WidgetsBinding.instance.addPostFrameCallback((_) {
@@ -49,6 +57,41 @@ class _MapTabState extends State<MapTab> {
}); });
} }
void _startCompassTracking() {
// Start listening to compass events
_compassStreamSubscription = FlutterCompass.events?.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) {
debugPrint('Rotating map to compass heading: ${event.heading}');
_mapController.rotate(-event.heading!);
}
}
});
}
Future<void> _loadSettings() async {
final prefs = await SharedPreferences.getInstance();
if (mounted) {
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;
});
}
}
Future<void> _saveSettings() async {
final prefs = await SharedPreferences.getInstance();
await prefs.setBool('map_show_legend', _showLegend);
await prefs.setBool('map_rotate_with_heading', _rotateMarkerWithHeading);
await prefs.setDouble('map_gps_update_distance', _gpsUpdateDistance);
}
Future<void> _requestLocationPermission() async { Future<void> _requestLocationPermission() async {
bool serviceEnabled = await Geolocator.isLocationServiceEnabled(); bool serviceEnabled = await Geolocator.isLocationServiceEnabled();
if (!serviceEnabled) { if (!serviceEnabled) {
@@ -92,9 +135,18 @@ class _MapTabState extends State<MapTab> {
), ),
).listen((Position position) { ).listen((Position position) {
if (mounted) { if (mounted) {
debugPrint('Position update - Heading: ${position.heading}, Speed: ${position.speed}');
setState(() { setState(() {
_currentPosition = position; _currentPosition = position;
}); });
// 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}');
_mapController.rotate(-position.heading);
}
} }
}); });
} }
@@ -134,11 +186,25 @@ class _MapTabState extends State<MapTab> {
final mapProvider = context.read<MapProvider>(); final mapProvider = context.read<MapProvider>();
mapProvider.removeListener(_handleMapNavigation); mapProvider.removeListener(_handleMapNavigation);
_positionStreamSubscription?.cancel(); _positionStreamSubscription?.cancel();
_compassStreamSubscription?.cancel();
_mapController.dispose(); _mapController.dispose();
_tileCache.dispose(); _tileCache.dispose();
super.dispose(); super.dispose();
} }
// Get the current heading from compass or GPS
double? get _currentHeading {
// Prefer compass heading as it works when stationary
if (_compassHeading != null) {
return _compassHeading;
}
// Fall back to GPS heading when moving
if (_currentPosition?.heading != null && _currentPosition!.heading >= 0) {
return _currentPosition!.heading;
}
return null;
}
LatLng _calculateCenter(List<Contact> contacts, List<SarMarker> sarMarkers) { LatLng _calculateCenter(List<Contact> contacts, List<SarMarker> sarMarkers) {
final allPoints = <LatLng>[]; final allPoints = <LatLng>[];
@@ -272,6 +338,29 @@ class _MapTabState extends State<MapTab> {
_showLegend = value; _showLegend = value;
}); });
setModalState(() {}); setModalState(() {});
_saveSettings();
},
),
const Divider(),
// Compass rotation toggle
SwitchListTile(
secondary: const Icon(Icons.explore),
title: const Text('Rotate Map with Heading'),
subtitle: const Text('Map follows your direction when moving'),
value: _rotateMarkerWithHeading,
onChanged: (value) {
setState(() {
_rotateMarkerWithHeading = value;
// Reset map rotation when disabling
if (!_rotateMarkerWithHeading) {
_mapController.rotate(0);
} else if (_currentHeading != null) {
// Apply current heading rotation when enabling (if heading is valid)
_mapController.rotate(-_currentHeading!);
}
});
setModalState(() {});
_saveSettings();
}, },
), ),
const Divider(), const Divider(),
@@ -302,6 +391,7 @@ class _MapTabState extends State<MapTab> {
}); });
// Restart location stream with new distance // Restart location stream with new distance
_restartLocationStream(); _restartLocationStream();
_saveSettings();
}, },
), ),
Padding( Padding(
@@ -345,6 +435,12 @@ class _MapTabState extends State<MapTab> {
setState(() { setState(() {
_currentPosition = position; _currentPosition = position;
}); });
// 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) {
_mapController.rotate(-position.heading);
}
} }
}); });
} }
@@ -398,6 +494,7 @@ class _MapTabState extends State<MapTab> {
), ),
width: 40, width: 40,
height: 40, height: 40,
rotate: false, // Don't rotate with map
child: Container( child: Container(
decoration: BoxDecoration( decoration: BoxDecoration(
color: Colors.blue.withOpacity(0.3), color: Colors.blue.withOpacity(0.3),
@@ -416,7 +513,7 @@ class _MapTabState extends State<MapTab> {
], ],
), ),
child: const Icon( child: const Icon(
Icons.navigation, Icons.my_location,
color: Colors.white, color: Colors.white,
size: 16, size: 16,
), ),
@@ -440,10 +537,20 @@ class _MapTabState extends State<MapTab> {
], ],
), ),
), ),
// Compass widget - top right
if (_rotateMarkerWithHeading)
Positioned(
top: 16,
right: 16,
child: _CompassWidget(
heading: _currentHeading ?? 0,
hasHeading: _currentHeading != null,
),
),
// Map legend overlay // Map legend overlay
if (_showLegend) if (_showLegend)
Positioned( Positioned(
top: 16, top: _rotateMarkerWithHeading ? 80 : 16,
right: 16, right: 16,
child: _MapLegend( child: _MapLegend(
teamMemberCount: contactsWithLocation.length, teamMemberCount: contactsWithLocation.length,
@@ -624,3 +731,107 @@ class _LegendItem extends StatelessWidget {
} }
} }
class _CompassWidget extends StatelessWidget {
final double heading;
final bool hasHeading;
const _CompassWidget({
required this.heading,
required this.hasHeading,
});
@override
Widget build(BuildContext context) {
return Card(
child: Container(
width: 56,
height: 56,
padding: const EdgeInsets.all(8),
child: Stack(
alignment: Alignment.center,
children: [
// Compass rose background - rotates to show true north at top
Transform.rotate(
angle: heading * pi / 180,
child: CustomPaint(
size: const Size(40, 40),
painter: _CompassRosePainter(),
),
),
// Fixed needle pointing up (since map rotates)
Icon(
Icons.navigation,
color: hasHeading ? Colors.red : Colors.grey,
size: 28,
),
// Heading text
Positioned(
bottom: 0,
child: Container(
padding: const EdgeInsets.symmetric(horizontal: 4, vertical: 1),
decoration: BoxDecoration(
color: Colors.black.withOpacity(0.7),
borderRadius: BorderRadius.circular(4),
),
child: Text(
hasHeading ? '${heading.round()}°' : '--',
style: const TextStyle(
color: Colors.white,
fontSize: 9,
fontWeight: FontWeight.bold,
),
),
),
),
],
),
),
);
}
}
class _CompassRosePainter extends CustomPainter {
@override
void paint(Canvas canvas, Size size) {
final paint = Paint()
..color = Colors.grey.withOpacity(0.3)
..style = PaintingStyle.stroke
..strokeWidth = 1;
final center = Offset(size.width / 2, size.height / 2);
final radius = size.width / 2;
// Draw circle
canvas.drawCircle(center, radius, paint);
// Draw cardinal direction markers
final textPainter = TextPainter(
textDirection: TextDirection.ltr,
);
final directions = ['N', 'E', 'S', 'W'];
for (int i = 0; i < 4; i++) {
final angle = i * pi / 2 - pi / 2; // Start from North (top)
final x = center.dx + radius * 0.7 * cos(angle);
final y = center.dy + radius * 0.7 * sin(angle);
textPainter.text = TextSpan(
text: directions[i],
style: TextStyle(
color: Colors.grey.shade700,
fontSize: 10,
fontWeight: FontWeight.bold,
),
);
textPainter.layout();
textPainter.paint(
canvas,
Offset(x - textPainter.width / 2, y - textPainter.height / 2),
);
}
}
@override
bool shouldRepaint(covariant CustomPainter oldDelegate) => false;
}

View File

@@ -12,6 +12,7 @@ import objectbox_flutter_libs
import package_info_plus import package_info_plus
import path_provider_foundation import path_provider_foundation
import share_plus import share_plus
import shared_preferences_foundation
func RegisterGeneratedPlugins(registry: FlutterPluginRegistry) { func RegisterGeneratedPlugins(registry: FlutterPluginRegistry) {
FilePickerPlugin.register(with: registry.registrar(forPlugin: "FilePickerPlugin")) FilePickerPlugin.register(with: registry.registrar(forPlugin: "FilePickerPlugin"))
@@ -21,4 +22,5 @@ func RegisterGeneratedPlugins(registry: FlutterPluginRegistry) {
FPPPackageInfoPlusPlugin.register(with: registry.registrar(forPlugin: "FPPPackageInfoPlusPlugin")) FPPPackageInfoPlusPlugin.register(with: registry.registrar(forPlugin: "FPPPackageInfoPlusPlugin"))
PathProviderPlugin.register(with: registry.registrar(forPlugin: "PathProviderPlugin")) PathProviderPlugin.register(with: registry.registrar(forPlugin: "PathProviderPlugin"))
SharePlusMacosPlugin.register(with: registry.registrar(forPlugin: "SharePlusMacosPlugin")) SharePlusMacosPlugin.register(with: registry.registrar(forPlugin: "SharePlusMacosPlugin"))
SharedPreferencesPlugin.register(with: registry.registrar(forPlugin: "SharedPreferencesPlugin"))
} }

View File

@@ -206,6 +206,14 @@ packages:
url: "https://pub.dev" url: "https://pub.dev"
source: hosted source: hosted
version: "8.0.0" version: "8.0.0"
flutter_compass:
dependency: "direct main"
description:
name: flutter_compass
sha256: "1b4d7e6c95a675ec8482b5c9c9ccf1ebf0ced3dbec59dce28ad609da953de850"
url: "https://pub.dev"
source: hosted
version: "0.8.1"
flutter_lints: flutter_lints:
dependency: "direct dev" dependency: "direct dev"
description: description:
@@ -648,6 +656,62 @@ packages:
url: "https://pub.dev" url: "https://pub.dev"
source: hosted source: hosted
version: "5.0.2" version: "5.0.2"
shared_preferences:
dependency: "direct main"
description:
name: shared_preferences
sha256: "6e8bf70b7fef813df4e9a36f658ac46d107db4b4cfe1048b477d4e453a8159f5"
url: "https://pub.dev"
source: hosted
version: "2.5.3"
shared_preferences_android:
dependency: transitive
description:
name: shared_preferences_android
sha256: "34266009473bf71d748912da4bf62d439185226c03e01e2d9687bc65bbfcb713"
url: "https://pub.dev"
source: hosted
version: "2.4.15"
shared_preferences_foundation:
dependency: transitive
description:
name: shared_preferences_foundation
sha256: "6a52cfcdaeac77cad8c97b539ff688ccfc458c007b4db12be584fbe5c0e49e03"
url: "https://pub.dev"
source: hosted
version: "2.5.4"
shared_preferences_linux:
dependency: transitive
description:
name: shared_preferences_linux
sha256: "580abfd40f415611503cae30adf626e6656dfb2f0cee8f465ece7b6defb40f2f"
url: "https://pub.dev"
source: hosted
version: "2.4.1"
shared_preferences_platform_interface:
dependency: transitive
description:
name: shared_preferences_platform_interface
sha256: "57cbf196c486bc2cf1f02b85784932c6094376284b3ad5779d1b1c6c6a816b80"
url: "https://pub.dev"
source: hosted
version: "2.4.1"
shared_preferences_web:
dependency: transitive
description:
name: shared_preferences_web
sha256: c49bd060261c9a3f0ff445892695d6212ff603ef3115edbb448509d407600019
url: "https://pub.dev"
source: hosted
version: "2.4.3"
shared_preferences_windows:
dependency: transitive
description:
name: shared_preferences_windows
sha256: "94ef0f72b2d71bc3e700e025db3710911bd51a71cefb65cc609dd0d9a982e3c1"
url: "https://pub.dev"
source: hosted
version: "2.4.1"
sky_engine: sky_engine:
dependency: transitive dependency: transitive
description: flutter description: flutter
@@ -823,4 +887,4 @@ packages:
version: "6.6.1" version: "6.6.1"
sdks: sdks:
dart: ">=3.9.2 <4.0.0" dart: ">=3.9.2 <4.0.0"
flutter: ">=3.29.0" flutter: ">=3.35.0"

View File

@@ -53,6 +53,7 @@ dependencies:
# Location services # Location services
geolocator: ^14.0.2 geolocator: ^14.0.2
flutter_compass: ^0.8.0
# Utilities # Utilities
intl: ^0.20.2 intl: ^0.20.2
@@ -63,6 +64,9 @@ dependencies:
share_plus: ^10.1.3 share_plus: ^10.1.3
path_provider: ^2.1.5 path_provider: ^2.1.5
# Persistent storage
shared_preferences: ^2.3.3
dev_dependencies: dev_dependencies:
flutter_test: flutter_test:
sdk: flutter sdk: flutter