Merge pull request #218 from zjs81/dev-mapOverlap

Show overlaps in public keys of repeaters
This commit is contained in:
zjs81 2026-03-23 18:51:14 -07:00 committed by GitHub
commit 4c492f69ef
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
38 changed files with 430 additions and 124 deletions

View file

@ -4943,6 +4943,17 @@ class MeshCoreConnector extends ChangeNotifier {
);
}
bool hasValidLocation(double? latitude, double? longitude) {
const double epsilon = 1e-6;
final lat = latitude ?? 0.0;
final lon = longitude ?? 0.0;
return (lat.abs() > epsilon || lon.abs() > epsilon) &&
lat >= -90.0 &&
lat <= 90.0 &&
lon >= -180.0 &&
lon <= 180.0;
}
void _handlePayloadAdvertReceived(
Uint8List rawPacket,
Uint8List payload,
@ -4980,6 +4991,9 @@ class MeshCoreConnector extends ChangeNotifier {
latitude = advert.readInt32LE() / 1e6;
longitude = advert.readInt32LE() / 1e6;
}
// Validate location values if present
hasLocation = hasValidLocation(latitude, longitude);
if (hasName && advert.remaining > 0) {
name = advert.readCString();
}
@ -5045,20 +5059,8 @@ class MeshCoreConnector extends ChangeNotifier {
// CRITICAL: Preserve user's path override when contact is refreshed from device
_contacts[existingIndex] = existing.copyWith(
latitude:
hasLocation &&
latitude != null &&
latitude.abs() <= 90 &&
(latitude != 0 || longitude != 0)
? latitude
: existing.latitude,
longitude:
hasLocation &&
longitude != null &&
longitude.abs() <= 180 &&
(latitude != 0 || longitude != 0)
? longitude
: existing.longitude,
latitude: hasLocation ? latitude : existing.latitude,
longitude: hasLocation ? longitude : existing.longitude,
name: hasName ? name : existing.name,
path: Uint8List.fromList(path.reversed.toList()),
pathLength: path.length,
@ -5226,6 +5228,10 @@ class MeshCoreConnector extends ChangeNotifier {
markChannelRead(channelIndex);
notifyListeners();
}
void deleteAllPaths() {
_pathHistoryService?.clearAllHistories();
}
}
const int _phRouteMask = 0x03;

View file

@ -1941,5 +1941,7 @@
"appSettings_maxMessageRetriesSubtitle": "Брой опити за повторно изпращане, преди съобщението да бъде маркирано като неуспешно.",
"path_routeWeight": "{weight}/{max}",
"settings_multiAck": "Мулти-потвърди: {value}",
"settings_telemetryModeUpdated": "Режим на телеметрията е обновен"
"settings_telemetryModeUpdated": "Режим на телеметрията е обновен",
"map_showOverlaps": "Покриване на ключа на повтаряча",
"map_runTraceWithReturnPath": "Върни се по същия път."
}

View file

@ -1969,5 +1969,7 @@
"appSettings_maxMessageRetriesSubtitle": "Anzahl der Versuche, eine Nachricht erneut zu senden, bevor sie als fehlgeschlagen markiert wird.",
"path_routeWeight": "{weight}/{max}",
"settings_telemetryModeUpdated": "Telemetriemodus aktualisiert",
"settings_multiAck": "Mehrfach-Bestätigungen: {value}"
"settings_multiAck": "Mehrfach-Bestätigungen: {value}",
"map_showOverlaps": "Überlappungen der Repeater-Taste",
"map_runTraceWithReturnPath": "Auf dem gleichen Pfad zurückkehren."
}

View file

@ -878,6 +878,7 @@
"map_chatNodes": "Chat Nodes",
"map_repeaters": "Repeaters",
"map_otherNodes": "Other Nodes",
"map_showOverlaps": "Repeater Key Overlaps",
"map_keyPrefix": "Key Prefix",
"map_filterByKeyPrefix": "Filter by key prefix",
"map_publicKeyPrefix": "Public key prefix",
@ -891,7 +892,8 @@
"map_joinRoom": "Join Room",
"map_manageRepeater": "Manage Repeater",
"map_tapToAdd": "Tap on nodes to add them to the path.",
"map_runTrace": "Run Path Trace",
"map_runTrace": "Run path trace",
"map_runTraceWithReturnPath": "Return back on the same path.",
"map_removeLast": "Remove Last",
"map_pathTraceCancelled": "Path trace cancelled.",
"mapCache_title": "Offline Map Cache",

View file

@ -1969,5 +1969,7 @@
"appSettings_maxMessageRetriesSubtitle": "Número de intentos de reintento antes de marcar un mensaje como fallido.",
"path_routeWeight": "{weight}/{max}",
"settings_telemetryModeUpdated": "Modo de telemetría actualizado",
"settings_multiAck": "Multi-ACKs: {value}"
"settings_multiAck": "Multi-ACKs: {value}",
"map_showOverlaps": "Superposiciones de tecla repetidora",
"map_runTraceWithReturnPath": "Volver atrás por el mismo camino."
}

View file

@ -1941,5 +1941,7 @@
"appSettings_maxMessageRetriesSubtitle": "Nombre de tentatives de relance avant de marquer un message comme ayant échoué.",
"path_routeWeight": "{weight}/{max}",
"settings_multiAck": "Multi-ACKs : {value}",
"settings_telemetryModeUpdated": "Le mode télémétrie a été mis à jour"
"settings_telemetryModeUpdated": "Le mode télémétrie a été mis à jour",
"map_showOverlaps": "Chevauchement de la touche répétitive",
"map_runTraceWithReturnPath": "Revenir sur le même chemin."
}

View file

@ -1941,5 +1941,7 @@
"appSettings_maxMessageRetriesSubtitle": "Numero di tentativi di riprova prima di considerare un messaggio come fallito.",
"path_routeWeight": "{weight}/{max}",
"settings_telemetryModeUpdated": "Modalità telemetria aggiornata",
"settings_multiAck": "Multi-ACKs: {value}"
"settings_multiAck": "Multi-ACKs: {value}",
"map_showOverlaps": "Sovrapposizioni della chiave ripetitore",
"map_runTraceWithReturnPath": "Tornare indietro sullo stesso percorso"
}

View file

@ -3052,6 +3052,12 @@ abstract class AppLocalizations {
/// **'Other Nodes'**
String get map_otherNodes;
/// No description provided for @map_showOverlaps.
///
/// In en, this message translates to:
/// **'Repeater Key Overlaps'**
String get map_showOverlaps;
/// No description provided for @map_keyPrefix.
///
/// In en, this message translates to:
@ -3133,9 +3139,15 @@ abstract class AppLocalizations {
/// No description provided for @map_runTrace.
///
/// In en, this message translates to:
/// **'Run Path Trace'**
/// **'Run path trace'**
String get map_runTrace;
/// No description provided for @map_runTraceWithReturnPath.
///
/// In en, this message translates to:
/// **'Return back on the same path.'**
String get map_runTraceWithReturnPath;
/// No description provided for @map_removeLast.
///
/// In en, this message translates to:

View file

@ -1689,6 +1689,9 @@ class AppLocalizationsBg extends AppLocalizations {
@override
String get map_otherNodes => 'Други възли';
@override
String get map_showOverlaps => 'Покриване на ключа на повтаряча';
@override
String get map_keyPrefix => 'Префикс на ключа';
@ -1733,6 +1736,9 @@ class AppLocalizationsBg extends AppLocalizations {
@override
String get map_runTrace => 'Изпълни Път на Следване';
@override
String get map_runTraceWithReturnPath => 'Върни се по същия път.';
@override
String get map_removeLast => 'Премахни Последно';

View file

@ -1686,6 +1686,9 @@ class AppLocalizationsDe extends AppLocalizations {
@override
String get map_otherNodes => 'Andere Knoten';
@override
String get map_showOverlaps => 'Überlappungen der Repeater-Taste';
@override
String get map_keyPrefix => 'Schlüsselpräfix';
@ -1730,6 +1733,10 @@ class AppLocalizationsDe extends AppLocalizations {
@override
String get map_runTrace => 'Pfadverlauf ausführen';
@override
String get map_runTraceWithReturnPath =>
'Auf dem gleichen Pfad zurückkehren.';
@override
String get map_removeLast => 'Letztes Entfernen';

View file

@ -1656,6 +1656,9 @@ class AppLocalizationsEn extends AppLocalizations {
@override
String get map_otherNodes => 'Other Nodes';
@override
String get map_showOverlaps => 'Repeater Key Overlaps';
@override
String get map_keyPrefix => 'Key Prefix';
@ -1696,7 +1699,10 @@ class AppLocalizationsEn extends AppLocalizations {
String get map_tapToAdd => 'Tap on nodes to add them to the path.';
@override
String get map_runTrace => 'Run Path Trace';
String get map_runTrace => 'Run path trace';
@override
String get map_runTraceWithReturnPath => 'Return back on the same path.';
@override
String get map_removeLast => 'Remove Last';

View file

@ -1685,6 +1685,9 @@ class AppLocalizationsEs extends AppLocalizations {
@override
String get map_otherNodes => 'Otros Nodos';
@override
String get map_showOverlaps => 'Superposiciones de tecla repetidora';
@override
String get map_keyPrefix => 'Prefijo de clave';
@ -1728,6 +1731,9 @@ class AppLocalizationsEs extends AppLocalizations {
@override
String get map_runTrace => 'Ejecutar Rastreo de Ruta';
@override
String get map_runTraceWithReturnPath => 'Volver atrás por el mismo camino.';
@override
String get map_removeLast => 'Eliminar último';

View file

@ -1695,6 +1695,9 @@ class AppLocalizationsFr extends AppLocalizations {
@override
String get map_otherNodes => 'Autres nœuds';
@override
String get map_showOverlaps => 'Chevauchement de la touche répétitive';
@override
String get map_keyPrefix => 'Préfixe clé';
@ -1739,6 +1742,9 @@ class AppLocalizationsFr extends AppLocalizations {
@override
String get map_runTrace => 'Exécuter la traçage de chemin';
@override
String get map_runTraceWithReturnPath => 'Revenir sur le même chemin.';
@override
String get map_removeLast => 'Supprimer le dernier';

View file

@ -1687,6 +1687,9 @@ class AppLocalizationsIt extends AppLocalizations {
@override
String get map_otherNodes => 'Altri Nodi';
@override
String get map_showOverlaps => 'Sovrapposizioni della chiave ripetitore';
@override
String get map_keyPrefix => 'Prefisso Chiave';
@ -1729,6 +1732,10 @@ class AppLocalizationsIt extends AppLocalizations {
@override
String get map_runTrace => 'Esegui Path Trace';
@override
String get map_runTraceWithReturnPath =>
'Tornare indietro sullo stesso percorso';
@override
String get map_removeLast => 'Rimuovi ultimo';

View file

@ -1674,6 +1674,9 @@ class AppLocalizationsNl extends AppLocalizations {
@override
String get map_otherNodes => 'Andere Nodes';
@override
String get map_showOverlaps => 'Herhalingssleutel overlapt';
@override
String get map_keyPrefix => 'Prefix sleutel';
@ -1718,6 +1721,9 @@ class AppLocalizationsNl extends AppLocalizations {
@override
String get map_runTrace => 'Padeshulp traceren';
@override
String get map_runTraceWithReturnPath => 'Terugkeren op hetzelfde pad.';
@override
String get map_removeLast => 'Verwijder Laatste';

View file

@ -1698,6 +1698,9 @@ class AppLocalizationsPl extends AppLocalizations {
@override
String get map_otherNodes => 'Inne węzły';
@override
String get map_showOverlaps => 'Nakładające się klucze powtarzalne';
@override
String get map_keyPrefix => 'Prefiks klucza';
@ -1741,6 +1744,9 @@ class AppLocalizationsPl extends AppLocalizations {
@override
String get map_runTrace => 'Uruchom ślad ścieżki';
@override
String get map_runTraceWithReturnPath => 'Wróć z powrotem tą samą ścieżką';
@override
String get map_removeLast => 'Usuń ostatni';

View file

@ -1686,6 +1686,9 @@ class AppLocalizationsPt extends AppLocalizations {
@override
String get map_otherNodes => 'Outros Nós';
@override
String get map_showOverlaps => 'Sobreposições da Chave Repeater';
@override
String get map_keyPrefix => 'Prefixo Chave';
@ -1729,6 +1732,9 @@ class AppLocalizationsPt extends AppLocalizations {
@override
String get map_runTrace => 'Executar Traçado de Caminho';
@override
String get map_runTraceWithReturnPath => 'Retornar ao mesmo caminho.';
@override
String get map_removeLast => 'Remover Último';

View file

@ -1689,6 +1689,9 @@ class AppLocalizationsRu extends AppLocalizations {
@override
String get map_otherNodes => 'Другие ноды';
@override
String get map_showOverlaps => 'Перекрытия ключа повтора';
@override
String get map_keyPrefix => 'Префикс ключа';
@ -1732,6 +1735,9 @@ class AppLocalizationsRu extends AppLocalizations {
@override
String get map_runTrace => 'Запустить трассировку пути';
@override
String get map_runTraceWithReturnPath => 'Вернуться обратно по тому же пути';
@override
String get map_removeLast => 'Удалить последний';

View file

@ -1675,6 +1675,9 @@ class AppLocalizationsSk extends AppLocalizations {
@override
String get map_otherNodes => 'Ostatné uzly';
@override
String get map_showOverlaps => 'Prekrývanie opakovača kľúča';
@override
String get map_keyPrefix => 'Päťciferné predpona';
@ -1718,6 +1721,9 @@ class AppLocalizationsSk extends AppLocalizations {
@override
String get map_runTrace => 'Spustiť trasovaním cesty';
@override
String get map_runTraceWithReturnPath => 'Vráťte sa späť po tej istej ceste.';
@override
String get map_removeLast => 'Odstrániť posledný';

View file

@ -1671,6 +1671,9 @@ class AppLocalizationsSl extends AppLocalizations {
@override
String get map_otherNodes => 'Druge vozlišča';
@override
String get map_showOverlaps => 'Prekrivanje ključa ponovnega predvajanja';
@override
String get map_keyPrefix => 'Predpona ključa';
@ -1713,6 +1716,9 @@ class AppLocalizationsSl extends AppLocalizations {
@override
String get map_runTrace => 'Zaženi sledenje poti';
@override
String get map_runTraceWithReturnPath => 'Vrni se nazaj po isti poti.';
@override
String get map_removeLast => 'Odstrani Zadnji';

View file

@ -1664,6 +1664,9 @@ class AppLocalizationsSv extends AppLocalizations {
@override
String get map_otherNodes => 'Andra noder';
@override
String get map_showOverlaps => 'Repeater-nyckelöverlappningar';
@override
String get map_keyPrefix => 'Nyckelprefix';
@ -1707,6 +1710,9 @@ class AppLocalizationsSv extends AppLocalizations {
@override
String get map_runTrace => 'Kör spårsökning';
@override
String get map_runTraceWithReturnPath => 'Gå tillbaka på samma väg';
@override
String get map_removeLast => 'Ta bort sista';

View file

@ -1684,6 +1684,9 @@ class AppLocalizationsUk extends AppLocalizations {
@override
String get map_otherNodes => 'Інші вузли';
@override
String get map_showOverlaps => 'Перекриття ключа повторювача';
@override
String get map_keyPrefix => 'Префікс ключа';
@ -1727,6 +1730,9 @@ class AppLocalizationsUk extends AppLocalizations {
@override
String get map_runTrace => 'Виконати трасування шляху';
@override
String get map_runTraceWithReturnPath => 'Повернутися назад тим же шляхом';
@override
String get map_removeLast => 'Видалити останній';

View file

@ -1582,6 +1582,9 @@ class AppLocalizationsZh extends AppLocalizations {
@override
String get map_otherNodes => '其他节点';
@override
String get map_showOverlaps => '重复键重叠';
@override
String get map_keyPrefix => '关键字前缀';
@ -1624,6 +1627,9 @@ class AppLocalizationsZh extends AppLocalizations {
@override
String get map_runTrace => '运行路径追踪';
@override
String get map_runTraceWithReturnPath => '沿着相同的路径返回';
@override
String get map_removeLast => '移除最后一个';

View file

@ -1941,5 +1941,7 @@
"appSettings_maxMessageRetriesSubtitle": "Aantal pogingen om een bericht opnieuw te versturen voordat het als mislukt wordt gemarkeerd",
"path_routeWeight": "{weight}/{max}",
"settings_telemetryModeUpdated": "Telemetrie-modus bijgewerkt",
"settings_multiAck": "Multi-ACKs: {value}"
"settings_multiAck": "Multi-ACKs: {value}",
"map_showOverlaps": "Herhalingssleutel overlapt",
"map_runTraceWithReturnPath": "Terugkeren op hetzelfde pad."
}

View file

@ -1979,5 +1979,7 @@
"appSettings_maxMessageRetriesSubtitle": "Liczba prób ponownego wysłania wiadomości przed oznaczaniem jej jako nieudanej",
"path_routeWeight": "{weight}/{max}",
"settings_telemetryModeUpdated": "Tryb telemetryczny zaktualizowany",
"settings_multiAck": "Wiele potwierdzeń: {value}"
"settings_multiAck": "Wiele potwierdzeń: {value}",
"map_showOverlaps": "Nakładające się klucze powtarzalne",
"map_runTraceWithReturnPath": "Wróć z powrotem tą samą ścieżką"
}

View file

@ -1941,5 +1941,7 @@
"appSettings_maxMessageRetriesSubtitle": "Número de tentativas de reenvio antes de classificar uma mensagem como falha.",
"path_routeWeight": "{weight}/{max}",
"settings_telemetryModeUpdated": "Modo de telemetria atualizado",
"settings_multiAck": "Multi-ACKs: {value}"
"settings_multiAck": "Multi-ACKs: {value}",
"map_showOverlaps": "Sobreposições da Chave Repeater",
"map_runTraceWithReturnPath": "Retornar ao mesmo caminho."
}

View file

@ -1181,5 +1181,7 @@
"appSettings_maxMessageRetriesSubtitle": "Количество попыток повторной отправки сообщения перед тем, как пометить его как неудачное.",
"path_routeWeight": "{weight}/{max}",
"settings_telemetryModeUpdated": "Режим телеметрии обновлен",
"settings_multiAck": "Мульти-ACK: {value}"
"settings_multiAck": "Мульти-ACK: {value}",
"map_showOverlaps": "Перекрытия ключа повтора",
"map_runTraceWithReturnPath": "Вернуться обратно по тому же пути"
}

View file

@ -1941,5 +1941,7 @@
"appSettings_maxMessageRetriesSubtitle": "Počet pokusov o odošleť pred označením správy ako neúspešnej",
"path_routeWeight": "{weight}/{max}",
"settings_telemetryModeUpdated": "Režim telemetrie bol aktualizovaný",
"settings_multiAck": "Viaceré ACK: {value}"
"settings_multiAck": "Viaceré ACK: {value}",
"map_showOverlaps": "Prekrývanie opakovača kľúča",
"map_runTraceWithReturnPath": "Vráťte sa späť po tej istej ceste."
}

View file

@ -1941,5 +1941,7 @@
"appSettings_maxMessageRetriesSubtitle": "Število poskusov ponovnega poslanja, preden se sporočilo označuje kot neuspešno",
"path_routeWeight": "{weight}/{max}",
"settings_multiAck": "Večkratni potrditvi: {value}",
"settings_telemetryModeUpdated": "Način telemetrije posodobljen"
"settings_telemetryModeUpdated": "Način telemetrije posodobljen",
"map_showOverlaps": "Prekrivanje ključa ponovnega predvajanja",
"map_runTraceWithReturnPath": "Vrni se nazaj po isti poti."
}

View file

@ -1941,5 +1941,7 @@
"appSettings_maxMessageRetriesSubtitle": "Antal försök att skicka om ett meddelande innan det markeras som misslyckat.",
"path_routeWeight": "{weight}/{max}",
"settings_telemetryModeUpdated": "Telemetri-läge uppdaterat",
"settings_multiAck": "Multi-ACKs: {value}"
"settings_multiAck": "Multi-ACKs: {value}",
"map_showOverlaps": "Repeater-nyckelöverlappningar",
"map_runTraceWithReturnPath": "Gå tillbaka på samma väg"
}

View file

@ -1941,5 +1941,7 @@
"appSettings_maxMessageRetriesSubtitle": "Кількість спроб повторного відправлення повідомлення перед тим, як позначити його як невдале",
"path_routeWeight": "{weight}/{max}",
"settings_telemetryModeUpdated": "Режим телеметрії оновлено",
"settings_multiAck": "Багатократне підтвердження: {value}"
"settings_multiAck": "Багатократне підтвердження: {value}",
"map_showOverlaps": "Перекриття ключа повторювача",
"map_runTraceWithReturnPath": "Повернутися назад тим же шляхом"
}

View file

@ -1946,5 +1946,7 @@
"appSettings_maxMessageRetriesSubtitle": "在将消息标记为失败之前,允许尝试的次数",
"path_routeWeight": "{weight}/{max}",
"settings_multiAck": "多重ACK{value}",
"settings_telemetryModeUpdated": "遥测模式已更新"
"settings_telemetryModeUpdated": "遥测模式已更新",
"map_showOverlaps": "重复键重叠",
"map_runTraceWithReturnPath": "沿着相同的路径返回"
}

View file

@ -18,6 +18,7 @@ class AppSettings {
final bool mapShowRepeaters;
final bool mapShowChatNodes;
final bool mapShowOtherNodes;
final bool mapShowOverlaps;
final double mapTimeFilterHours; // 0 = all time
final bool mapKeyPrefixEnabled;
final String mapKeyPrefix;
@ -53,6 +54,7 @@ class AppSettings {
this.mapShowRepeaters = true,
this.mapShowChatNodes = true,
this.mapShowOtherNodes = true,
this.mapShowOverlaps = false,
this.mapTimeFilterHours = 0, // Default to all time
this.mapKeyPrefixEnabled = false,
this.mapKeyPrefix = '',
@ -92,6 +94,7 @@ class AppSettings {
'map_show_repeaters': mapShowRepeaters,
'map_show_chat_nodes': mapShowChatNodes,
'map_show_other_nodes': mapShowOtherNodes,
'map_show_overlaps': mapShowOverlaps,
'map_time_filter_hours': mapTimeFilterHours,
'map_key_prefix_enabled': mapKeyPrefixEnabled,
'map_key_prefix': mapKeyPrefix,
@ -137,6 +140,7 @@ class AppSettings {
mapShowRepeaters: json['map_show_repeaters'] as bool? ?? true,
mapShowChatNodes: json['map_show_chat_nodes'] as bool? ?? true,
mapShowOtherNodes: json['map_show_other_nodes'] as bool? ?? true,
mapShowOverlaps: json['map_show_overlaps'] as bool? ?? false,
mapTimeFilterHours:
(json['map_time_filter_hours'] as num?)?.toDouble() ?? 0,
mapKeyPrefixEnabled: json['map_key_prefix_enabled'] as bool? ?? false,
@ -196,6 +200,7 @@ class AppSettings {
bool? mapShowRepeaters,
bool? mapShowChatNodes,
bool? mapShowOtherNodes,
bool? mapShowOverlaps,
double? mapTimeFilterHours,
bool? mapKeyPrefixEnabled,
String? mapKeyPrefix,
@ -231,6 +236,7 @@ class AppSettings {
mapShowRepeaters: mapShowRepeaters ?? this.mapShowRepeaters,
mapShowChatNodes: mapShowChatNodes ?? this.mapShowChatNodes,
mapShowOtherNodes: mapShowOtherNodes ?? this.mapShowOtherNodes,
mapShowOverlaps: mapShowOverlaps ?? this.mapShowOverlaps,
mapTimeFilterHours: mapTimeFilterHours ?? this.mapTimeFilterHours,
mapKeyPrefixEnabled: mapKeyPrefixEnabled ?? this.mapKeyPrefixEnabled,
mapKeyPrefix: mapKeyPrefix ?? this.mapKeyPrefix,

View file

@ -183,12 +183,13 @@ class Contact {
final lastMod = reader.readUInt32LE();
double? lat, lon;
final latRaw = reader.readInt32LE();
final lonRaw = reader.readInt32LE();
if (latRaw != 0 || lonRaw != 0) {
lat = latRaw / 1e6;
lon = lonRaw / 1e6;
if (reader.remaining >= 8) {
final latRaw = reader.readInt32LE();
final lonRaw = reader.readInt32LE();
if (latRaw != 0 || lonRaw != 0) {
lat = latRaw / 1e6;
lon = lonRaw / 1e6;
}
}
return Contact(

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)
@ -477,10 +480,12 @@ class _MapScreenState extends State<MapScreen> {
point: highlightPosition,
width: 40,
height: 40,
child: Icon(
Icons.location_on_outlined,
color: Colors.red[600],
size: 34,
child: IgnorePointer(
child: Icon(
Icons.location_on_outlined,
color: Colors.red[600],
size: 34,
),
),
),
if (!_isBuildingPathTrace)
@ -503,28 +508,33 @@ class _MapScreenState extends State<MapScreen> {
),
width: 40,
height: 40,
child: Container(
padding: const EdgeInsets.all(4),
decoration: BoxDecoration(
color: Colors.teal,
shape: BoxShape.circle,
border: Border.all(
color: Colors.white,
width: 2,
),
boxShadow: [
BoxShadow(
color: Colors.black.withValues(alpha: 0.3),
blurRadius: 4,
offset: const Offset(0, 2),
child: IgnorePointer(
ignoring: true,
child: Container(
padding: const EdgeInsets.all(4),
decoration: BoxDecoration(
color: Colors.teal,
shape: BoxShape.circle,
border: Border.all(
color: Colors.white,
width: 2,
),
],
),
alignment: Alignment.center,
child: const Icon(
Icons.person_pin_circle,
color: Colors.white,
size: 20,
boxShadow: [
BoxShadow(
color: Colors.black.withValues(
alpha: 0.3,
),
blurRadius: 4,
offset: const Offset(0, 2),
),
],
),
alignment: Alignment.center,
child: const Icon(
Icons.person_pin_circle,
color: Colors.white,
size: 20,
),
),
),
),
@ -544,6 +554,7 @@ class _MapScreenState extends State<MapScreen> {
),
if (!_isBuildingPathTrace)
_buildLegend(
contacts,
contactsWithLocation,
settings,
sharedMarkers.length,
@ -580,6 +591,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 +607,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 +658,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 +838,70 @@ class _MapScreenState extends State<MapScreen> {
return markers;
}
List<Contact> _filterContactsBySettings(
List<Contact> contacts,
dynamic settings, {
bool noLocations = false,
}) {
List<Contact> filtered = [];
bool addContact = false;
for (final contact in contacts) {
addContact = false;
if (!contact.hasLocation && !noLocations) {
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 +917,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 +946,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,
),
);
}
@ -954,25 +1023,25 @@ class _MapScreenState extends State<MapScreen> {
}
Widget _buildLegend(
List<Contact> contacts,
List<Contact> contactsWithLocation,
settings,
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(
contacts,
settings,
noLocations: false,
);
final filteredContactsAll = _filterContactsBySettings(
contacts,
settings,
noLocations: true,
);
final nodeCount = filteredContacts.length;
final nodeCountAll = filteredContactsAll.length;
return Positioned(
top: 16,
@ -1008,6 +1077,54 @@ class _MapScreenState extends State<MapScreen> {
fontSize: 14,
),
),
Row(
children: [
Icon(
Icons.location_on,
size: 16,
color: Colors.grey,
),
Text(
": $nodeCount",
style: const TextStyle(
fontWeight: FontWeight.bold,
fontSize: 14,
),
),
],
),
Row(
children: [
const Icon(
Icons.wrong_location,
size: 16,
color: Colors.grey,
),
Text(
": ${nodeCountAll - nodeCount}",
style: const TextStyle(
fontWeight: FontWeight.bold,
fontSize: 14,
),
),
],
),
Row(
children: [
const Icon(
Icons.add_outlined,
size: 16,
color: Colors.grey,
),
Text(
": $nodeCountAll",
style: const TextStyle(
fontWeight: FontWeight.bold,
fontSize: 14,
),
),
],
),
Text(
context.l10n.map_pinsCount(markerCount),
style: const TextStyle(
@ -1846,6 +1963,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 +2130,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 +2182,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 +2204,37 @@ class _MapScreenState extends State<MapScreen> {
_isBuildingPathTrace = false;
});
},
child: Text(l10n.map_runTrace),
tooltip: l10n.map_runTrace,
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: l10n.map_runTraceWithReturnPath,
icon: const Icon(Icons.replay),
),
if (_pathTrace.isNotEmpty)
IconButton(
onPressed: _removePath,
child: Text(l10n.map_removeLast),
tooltip: l10n.map_removeLast,
icon: const Icon(Icons.undo),
),
if (_pathTrace.isEmpty)
ElevatedButton(
IconButton(
onPressed: () {
setState(() {
_isBuildingPathTrace = false;
@ -2097,7 +2246,8 @@ class _MapScreenState extends State<MapScreen> {
SnackBar(content: Text(l10n.map_pathTraceCancelled)),
);
},
child: Text(l10n.common_cancel),
tooltip: l10n.common_cancel,
icon: const Icon(Icons.close),
),
],
),

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(
@ -657,14 +660,6 @@ class _SettingsScreenState extends State<SettingsScreen> {
);
}
void _sendAdvert(BuildContext context, MeshCoreConnector connector) {
final l10n = context.l10n;
connector.sendSelfAdvert(flood: true);
ScaffoldMessenger.of(
context,
).showSnackBar(SnackBar(content: Text(l10n.settings_advertisementSent)));
}
void _syncTime(BuildContext context, MeshCoreConnector connector) {
final l10n = context.l10n;
connector.syncTime();

View file

@ -64,6 +64,10 @@ class AppSettingsService extends ChangeNotifier {
await updateSettings(_settings.copyWith(mapShowOtherNodes: value));
}
Future<void> setMapShowOverlaps(bool value) async {
await updateSettings(_settings.copyWith(mapShowOverlaps: value));
}
Future<void> setMapTimeFilterHours(double value) async {
await updateSettings(_settings.copyWith(mapTimeFilterHours: value));
}

View file

@ -565,6 +565,16 @@ class PathHistoryService extends ChangeNotifier {
_floodStats.remove(oldest);
}
}
void clearAllHistories() {
_cache.clear();
_cacheAccessOrder.clear();
_autoRotationIndex.clear();
_floodStats.clear();
_storage.clearAllPathHistories();
_version = 0;
notifyListeners();
}
}
class _DeferredPathRecord {