feat: Add GPS toggle and node sorting

This commit is contained in:
Janez T
2026-03-17 09:30:36 +01:00
parent 59f6df4260
commit cd165d2372
3 changed files with 126 additions and 17 deletions

View File

@@ -0,0 +1,48 @@
import 'package:geolocator/geolocator.dart';
import 'package:latlong2/latlong.dart';
import '../models/contact.dart';
class RelayCandidateSorter {
const RelayCandidateSorter();
List<Contact> sortByDistanceFromSelf(
List<Contact> contacts, {
required LatLng? selfPoint,
}) {
final sorted = List<Contact>.from(contacts);
sorted.sort((a, b) {
if (selfPoint != null) {
final distanceCompare = _distanceFrom(
selfPoint,
a,
).compareTo(_distanceFrom(selfPoint, b));
if (distanceCompare != 0) {
return distanceCompare;
}
}
final nameCompare = a.displayName.compareTo(b.displayName);
if (nameCompare != 0) {
return nameCompare;
}
return a.publicKeyHex.compareTo(b.publicKeyHex);
});
return sorted;
}
double _distanceFrom(LatLng selfPoint, Contact contact) {
final location = contact.displayLocation;
if (location == null) {
return double.infinity;
}
return Geolocator.distanceBetween(
selfPoint.latitude,
selfPoint.longitude,
location.latitude,
location.longitude,
);
}
}

View File

@@ -11,6 +11,7 @@ import '../../providers/app_provider.dart';
import '../../providers/connection_provider.dart'; import '../../providers/connection_provider.dart';
import '../../services/contact_route_resolver.dart'; import '../../services/contact_route_resolver.dart';
import '../../services/path_history_service.dart'; import '../../services/path_history_service.dart';
import '../../services/relay_candidate_sorter.dart';
import '../../services/route_hash_preferences.dart'; import '../../services/route_hash_preferences.dart';
class ContactRouteDialogResult { class ContactRouteDialogResult {
@@ -72,6 +73,8 @@ class _ContactRouteDialogState extends State<ContactRouteDialog> {
late final TextEditingController _controller; late final TextEditingController _controller;
late final TextEditingController _relaySearchController; late final TextEditingController _relaySearchController;
final PathHistoryService _pathHistoryService = PathHistoryService(); final PathHistoryService _pathHistoryService = PathHistoryService();
final RelayCandidateSorter _relayCandidateSorter =
const RelayCandidateSorter();
int _selectedHashSize = RouteHashPreferences.defaultHashSize; int _selectedHashSize = RouteHashPreferences.defaultHashSize;
ParsedContactRoute? _parsedRoute; ParsedContactRoute? _parsedRoute;
String? _errorText; String? _errorText;
@@ -133,13 +136,16 @@ class _ContactRouteDialogState extends State<ContactRouteDialog> {
} }
} }
List<Contact> get _routeCandidates => List<Contact> _routeCandidates({required LatLng? selfPoint}) =>
_relayCandidateSorter.sortByDistanceFromSelf(
widget.availableContacts widget.availableContacts
.where( .where(
(contact) => contact.isRepeater && contact.displayLocation != null, (contact) =>
contact.isRepeater && contact.displayLocation != null,
) )
.toList() .toList(),
..sort((a, b) => a.displayName.compareTo(b.displayName)); selfPoint: selfPoint,
);
List<Contact> _mapSelectionForText(String text) { List<Contact> _mapSelectionForText(String text) {
final tokens = text final tokens = text
@@ -169,7 +175,8 @@ class _ContactRouteDialogState extends State<ContactRouteDialog> {
final contactHashSize = widget.contact.hasPath final contactHashSize = widget.contact.hasPath
? widget.contact.pathHashSize ? widget.contact.pathHashSize
: null; : null;
final hashSize = contactHashSize ?? await RouteHashPreferences.getHashSize(); final hashSize =
contactHashSize ?? await RouteHashPreferences.getHashSize();
if (!mounted) return; if (!mounted) return;
setState(() { setState(() {
_selectedHashSize = hashSize; _selectedHashSize = hashSize;
@@ -697,18 +704,12 @@ class _ContactRouteDialogState extends State<ContactRouteDialog> {
children: [ children: [
Row( Row(
children: [ children: [
Text( Text('Path Size', style: Theme.of(context).textTheme.labelLarge),
'Path Size',
style: Theme.of(context).textTheme.labelLarge,
),
const Spacer(), const Spacer(),
SegmentedButton<int>( SegmentedButton<int>(
segments: [ segments: [
for (final size in RouteHashPreferences.supportedSizes) for (final size in RouteHashPreferences.supportedSizes)
ButtonSegment<int>( ButtonSegment<int>(value: size, label: Text('${size}B')),
value: size,
label: Text('${size}B'),
),
], ],
selected: {_selectedHashSize}, selected: {_selectedHashSize},
onSelectionChanged: (selection) { onSelectionChanged: (selection) {
@@ -822,7 +823,6 @@ class _ContactRouteDialogState extends State<ContactRouteDialog> {
@override @override
Widget build(BuildContext context) { Widget build(BuildContext context) {
final appProvider = context.watch<AppProvider>(); final appProvider = context.watch<AppProvider>();
final routeCandidates = _routeCandidates;
final connectionProvider = context.watch<ConnectionProvider>(); final connectionProvider = context.watch<ConnectionProvider>();
final selfPoint = final selfPoint =
connectionProvider.deviceInfo.advLat != null && connectionProvider.deviceInfo.advLat != null &&
@@ -834,6 +834,7 @@ class _ContactRouteDialogState extends State<ContactRouteDialog> {
connectionProvider.deviceInfo.advLon! / 1e6, connectionProvider.deviceInfo.advLon! / 1e6,
) )
: null; : null;
final routeCandidates = _routeCandidates(selfPoint: selfPoint);
final recipientLocation = widget.contact.displayLocation; final recipientLocation = widget.contact.displayLocation;
final recipientPoint = recipientLocation == null final recipientPoint = recipientLocation == null
? null ? null

View File

@@ -0,0 +1,60 @@
import 'dart:typed_data';
import 'package:flutter_test/flutter_test.dart';
import 'package:latlong2/latlong.dart';
import 'package:meshcore_sar_app/models/contact.dart';
import 'package:meshcore_sar_app/services/relay_candidate_sorter.dart';
void main() {
test('sorts relay candidates by distance from self point', () {
final sorter = RelayCandidateSorter();
final sorted = sorter.sortByDistanceFromSelf([
_contact(seed: 1, name: 'Far', lat: 46.08, lon: 14.60),
_contact(seed: 2, name: 'Near', lat: 46.0570, lon: 14.5060),
_contact(seed: 3, name: 'Middle', lat: 46.06, lon: 14.52),
], selfPoint: const LatLng(46.0569, 14.5058));
expect(sorted.map((contact) => contact.displayName).toList(), [
'Near',
'Middle',
'Far',
]);
});
test('falls back to stable name ordering when self point is unavailable', () {
final sorter = RelayCandidateSorter();
final sorted = sorter.sortByDistanceFromSelf([
_contact(seed: 1, name: 'Zulu', lat: 46.08, lon: 14.60),
_contact(seed: 2, name: 'Alpha', lat: 46.0570, lon: 14.5060),
], selfPoint: null);
expect(sorted.map((contact) => contact.displayName).toList(), [
'Alpha',
'Zulu',
]);
});
}
Contact _contact({
required int seed,
required String name,
required double lat,
required double lon,
}) {
final publicKey = Uint8List(32)..fillRange(0, 32, seed);
return Contact(
publicKey: publicKey,
type: ContactType.repeater,
flags: 0,
outPathLen: -1,
outPath: Uint8List(64),
advName: name,
lastAdvert: DateTime.now().millisecondsSinceEpoch ~/ 1000,
advLat: (lat * 1e6).round(),
advLon: (lon * 1e6).round(),
lastMod: DateTime.now().millisecondsSinceEpoch ~/ 1000,
);
}