Add show overlaps in public keys of repeaters functionality and localization support

This commit is contained in:
Winston Lowe
2026-02-22 16:00:51 -08:00
parent cac0cc15eb
commit c81c3efe7c
37 changed files with 242 additions and 77 deletions
+132 -50
View File
@@ -1,3 +1,4 @@
import 'dart:collection';
import 'dart:math';
import 'dart:typed_data';
@@ -52,7 +53,7 @@ class MapScreen extends StatefulWidget {
class _MapScreenState extends State<MapScreen> {
// Zoom level at which node labels start to appear
static const double _labelZoomThreshold = 12.0;
static const double _labelZoomThreshold = 14.0;
final MapController _mapController = MapController();
final MapMarkerService _markerService = MapMarkerService();
@@ -329,7 +330,9 @@ class _MapScreenState extends State<MapScreen> {
if (!_isBuildingPathTrace)
IconButton(
icon: const Icon(Icons.radar),
onPressed: () => _startPath(),
onPressed: () => _startPath(
LatLng(connector.selfLatitude!, connector.selfLongitude!),
),
tooltip: context.l10n.contacts_pathTrace,
),
if (!_isBuildingPathTrace)
@@ -580,6 +583,7 @@ class _MapScreenState extends State<MapScreen> {
// Index known-location repeaters by their 1-byte hash.
// null value = two repeaters share the same hash byte (ambiguous collision).
final repeaterByHash = <int, Contact?>{};
for (final c in withLocation) {
if (c.type == advTypeRepeater) {
if (repeaterByHash.containsKey(c.publicKey[0])) {
@@ -595,6 +599,11 @@ class _MapScreenState extends State<MapScreen> {
for (final contact in allContacts) {
if (contact.hasLocation) continue;
if (contact.lastSeen.isBefore(
DateTime.now().subtract(const Duration(hours: 24)),
)) {
continue; // skip stale contacts
}
final anchorSet = <LatLng>{};
@@ -641,10 +650,19 @@ class _MapScreenState extends State<MapScreen> {
continue; // discard implausible guesses near (0, 0)
}
} else {
double lat = 0, lon = 0;
double lat = 0, lon = 0, weight = 1.0;
int counted = 0;
for (final a in anchors) {
lat += a.latitude;
lon += a.longitude;
if (counted == 0) {
lat = a.latitude;
lon = a.longitude;
} else {
lat += a.latitude * weight;
lon += a.longitude * weight;
}
// weight subsequent anchors less to create a bias towards the first (if more than 2)
weight = weight / 2;
counted++;
}
position = _offsetGuessedPosition(
LatLng(lat / anchors.length, lon / anchors.length),
@@ -812,31 +830,67 @@ class _MapScreenState extends State<MapScreen> {
return markers;
}
List<Contact> _filterContactsBySettings(
List<Contact> contacts,
dynamic settings,
) {
List<Contact> filtered = [];
bool addContact = false;
for (final contact in contacts) {
addContact = false;
if (!contact.hasLocation) continue;
// Apply node type filters
if (contact.type == advTypeRepeater &&
(settings.mapShowRepeaters ||
_isBuildingPathTrace ||
settings.mapShowOverlaps)) {
addContact = true;
}
if (contact.type == advTypeChat &&
(settings.mapShowChatNodes || _isBuildingPathTrace)) {
addContact = true;
}
if (contact.type != advTypeChat &&
contact.type != advTypeRepeater &&
(settings.mapShowOtherNodes ||
_isBuildingPathTrace ||
settings.mapShowOverlaps)) {
addContact = true;
}
final hasOverlap = contacts
.where(
(c) =>
c.publicKeyHex != contact.publicKeyHex &&
c.publicKey.first == contact.publicKey.first &&
(c.type == advTypeRepeater || c.type == advTypeRoom) &&
(contact.type == advTypeRepeater ||
contact.type == advTypeRoom),
)
.firstOrNull;
if (hasOverlap == null &&
settings.mapShowOverlaps &&
!_isBuildingPathTrace) {
addContact = false;
}
if (addContact) {
filtered.add(contact);
}
}
return filtered;
}
List<Marker> _buildMarkers(
List<Contact> contacts,
settings, {
required bool showLabels,
}) {
final markers = <Marker>[];
for (final contact in contacts) {
if (!contact.hasLocation) continue;
// Apply node type filters
if (contact.type == advTypeRepeater &&
(!settings.mapShowRepeaters && !_isBuildingPathTrace)) {
continue;
}
if (contact.type == advTypeChat &&
!(settings.mapShowChatNodes && !_isBuildingPathTrace)) {
continue;
}
if (contact.type != advTypeChat &&
contact.type != advTypeRepeater &&
(!settings.mapShowOtherNodes && !_isBuildingPathTrace)) {
continue;
}
final filteredContacts = _filterContactsBySettings(contacts, settings);
for (final contact in filteredContacts) {
final marker = Marker(
point: LatLng(contact.latitude!, contact.longitude!),
width: 35,
@@ -852,7 +906,9 @@ class _MapScreenState extends State<MapScreen> {
Container(
padding: const EdgeInsets.all(4),
decoration: BoxDecoration(
color: _getNodeColor(contact.type),
color: settings.mapShowOverlaps && !_isBuildingPathTrace
? Colors.red
: _getNodeColor(contact.type),
shape: BoxShape.circle,
border: Border.all(color: Colors.white, width: 2),
boxShadow: [
@@ -879,7 +935,9 @@ class _MapScreenState extends State<MapScreen> {
markers.add(
_buildNodeLabelMarker(
point: LatLng(contact.latitude!, contact.longitude!),
label: contact.name,
label: settings.mapShowOverlaps && !_isBuildingPathTrace
? "${contact.publicKeyHex.substring(0, 2)}:${contact.name}"
: contact.name,
),
);
}
@@ -959,20 +1017,12 @@ class _MapScreenState extends State<MapScreen> {
int markerCount,
int guessedCount,
) {
int nodeCount = 0;
for (final contact in contactsWithLocation) {
// Apply node type filters
if (contact.type == advTypeRepeater && !settings.mapShowRepeaters) {
continue;
}
if (contact.type == advTypeChat && !settings.mapShowChatNodes) continue;
if (contact.type != advTypeChat &&
contact.type != advTypeRepeater &&
!settings.mapShowOtherNodes) {
continue;
}
nodeCount++;
}
final filteredContacts = _filterContactsBySettings(
contactsWithLocation,
settings,
);
final nodeCount = filteredContacts.length;
return Positioned(
top: 16,
@@ -1846,6 +1896,15 @@ class _MapScreenState extends State<MapScreen> {
},
contentPadding: EdgeInsets.zero,
),
CheckboxListTile(
title: Text(context.l10n.map_showOverlaps),
value: settings.mapShowOverlaps,
onChanged: (value) {
service.setMapShowOverlaps(value ?? true);
},
contentPadding: EdgeInsets.zero,
),
const SizedBox(height: 16),
Text(
context.l10n.map_keyPrefix,
@@ -2004,12 +2063,13 @@ class _MapScreenState extends State<MapScreen> {
});
}
void _startPath() {
void _startPath(LatLng position) {
setState(() {
_isBuildingPathTrace = true;
_pathTrace.clear();
_points.clear();
_polylines.clear();
_points.add(position);
});
}
@@ -2055,14 +2115,14 @@ class _MapScreenState extends State<MapScreen> {
.join(','),
style: TextStyle(fontSize: 18),
),
const SizedBox(height: 6),
// const SizedBox(height: 6),
Wrap(
alignment: WrapAlignment.center,
spacing: 8,
runSpacing: 8,
spacing: 1,
runSpacing: 1,
children: [
if (_pathTrace.isNotEmpty)
ElevatedButton(
IconButton(
onPressed: () {
Navigator.push(
context,
@@ -2077,15 +2137,37 @@ class _MapScreenState extends State<MapScreen> {
_isBuildingPathTrace = false;
});
},
child: Text(l10n.map_runTrace),
tooltip: "Path Trace",
icon: const Icon(Icons.arrow_forward_outlined),
),
if (_pathTrace.isNotEmpty)
ElevatedButton(
IconButton(
onPressed: () {
Navigator.push(
context,
MaterialPageRoute(
builder: (context) => PathTraceMapScreen(
title: l10n.contacts_pathTrace,
path: Uint8List.fromList(_pathTrace),
flipPathAround: true,
),
),
);
setState(() {
_isBuildingPathTrace = false;
});
},
tooltip: "Build Return Path",
icon: const Icon(Icons.replay),
),
if (_pathTrace.isNotEmpty)
IconButton(
onPressed: _removePath,
child: Text(l10n.map_removeLast),
tooltip: "Remove Last Point",
icon: const Icon(Icons.delete),
),
if (_pathTrace.isEmpty)
ElevatedButton(
IconButton(
onPressed: () {
setState(() {
_isBuildingPathTrace = false;
@@ -2097,7 +2179,7 @@ class _MapScreenState extends State<MapScreen> {
SnackBar(content: Text(l10n.map_pathTraceCancelled)),
);
},
child: Text(l10n.common_cancel),
icon: const Icon(Icons.close),
),
],
),
+7 -4
View File
@@ -311,10 +311,13 @@ class _SettingsScreenState extends State<SettingsScreen> {
),
),
ListTile(
leading: const Icon(Icons.cell_tower),
title: Text(l10n.settings_sendAdvertisement),
subtitle: Text(l10n.settings_sendAdvertisementSubtitle),
onTap: () => _sendAdvert(context, connector),
leading: const Icon(Icons.delete_outline, color: Colors.red),
title: Text("Delete All Paths"),
subtitle: Text(
"Clear all path data from contacts.",
style: TextStyle(color: Colors.red[700]),
),
onTap: () => connector.deleteAllPaths(),
),
const Divider(height: 1),
ListTile(