From 1f2dfc555b8b165336869a843b5b1170f5117174 Mon Sep 17 00:00:00 2001 From: zach Date: Fri, 6 Mar 2026 15:02:37 -0700 Subject: [PATCH 1/4] Add guessed node location map keys and translations Adds map_showGuessedLocations and map_guessedLocation to app_en.arb and translates them across all 14 supported locales. Regenerates dart localizations. --- lib/l10n/app_bg.arb | 4 +- lib/l10n/app_de.arb | 4 +- lib/l10n/app_en.arb | 2 + lib/l10n/app_es.arb | 4 +- lib/l10n/app_fr.arb | 4 +- lib/l10n/app_it.arb | 4 +- lib/l10n/app_localizations.dart | 12 + lib/l10n/app_localizations_bg.dart | 7 + lib/l10n/app_localizations_de.dart | 7 + lib/l10n/app_localizations_en.dart | 6 + lib/l10n/app_localizations_es.dart | 7 + lib/l10n/app_localizations_fr.dart | 7 + lib/l10n/app_localizations_it.dart | 6 + lib/l10n/app_localizations_nl.dart | 7 + lib/l10n/app_localizations_pl.dart | 7 + lib/l10n/app_localizations_pt.dart | 7 + lib/l10n/app_localizations_ru.dart | 7 + lib/l10n/app_localizations_sk.dart | 7 + lib/l10n/app_localizations_sl.dart | 6 + lib/l10n/app_localizations_sv.dart | 7 + lib/l10n/app_localizations_uk.dart | 7 + lib/l10n/app_localizations_zh.dart | 6 + lib/l10n/app_nl.arb | 4 +- lib/l10n/app_pl.arb | 4 +- lib/l10n/app_pt.arb | 4 +- lib/l10n/app_ru.arb | 4 +- lib/l10n/app_sk.arb | 4 +- lib/l10n/app_sl.arb | 4 +- lib/l10n/app_sv.arb | 4 +- lib/l10n/app_uk.arb | 4 +- lib/l10n/app_zh.arb | 4 +- lib/models/app_settings.dart | 8 + lib/screens/chat_screen.dart | 1 + lib/screens/contacts_screen.dart | 1 + lib/screens/map_screen.dart | 284 +++++++++++++++++++++++- lib/screens/path_trace_map.dart | 146 +++++++++++- lib/services/app_settings_service.dart | 4 + lib/services/path_history_service.dart | 5 + lib/widgets/path_management_dialog.dart | 1 + 39 files changed, 587 insertions(+), 34 deletions(-) diff --git a/lib/l10n/app_bg.arb b/lib/l10n/app_bg.arb index 832e090..9733738 100644 --- a/lib/l10n/app_bg.arb +++ b/lib/l10n/app_bg.arb @@ -1826,5 +1826,7 @@ "contactsSettings_overwriteOldestSubtitle": "Когато списъкът с контакти е пълен, най-старият неключов контакт ще бъде заменен.", "discoveredContacts_deleteContactAll": "Изтриване на Всички Открити Контакти", "discoveredContacts_deleteContactAllContent": "Сигурни ли сте, че искате да изтриете всички открити контакти?", - "common_deleteAll": "Изтрий всичко" + "common_deleteAll": "Изтрий всичко", + "map_guessedLocation": "Предполагано местоположение", + "map_showGuessedLocations": "Покажете местоположенията на предположените възли." } diff --git a/lib/l10n/app_de.arb b/lib/l10n/app_de.arb index c1d344f..ecfd8e4 100644 --- a/lib/l10n/app_de.arb +++ b/lib/l10n/app_de.arb @@ -1854,5 +1854,7 @@ "contactsSettings_overwriteOldestSubtitle": "Wenn die Kontaktliste voll ist, wird der älteste nicht favorisierte Kontakt ersetzt.", "common_deleteAll": "Alles löschen", "discoveredContacts_deleteContactAllContent": "Sind Sie sicher, dass Sie alle gefundenen Kontakte löschen möchten?", - "discoveredContacts_deleteContactAll": "Alle entdeckten Kontakte löschen" + "discoveredContacts_deleteContactAll": "Alle entdeckten Kontakte löschen", + "map_showGuessedLocations": "Zeige die vermuteten Knotenpositionen", + "map_guessedLocation": "Geschätzter Ort" } diff --git a/lib/l10n/app_en.arb b/lib/l10n/app_en.arb index 99221dc..12c9844 100644 --- a/lib/l10n/app_en.arb +++ b/lib/l10n/app_en.arb @@ -775,6 +775,8 @@ "map_publicKeyPrefix": "Public key prefix", "map_markers": "Markers", "map_showSharedMarkers": "Show shared markers", + "map_showGuessedLocations": "Show guessed node locations", + "map_guessedLocation": "Guessed location", "map_lastSeenTime": "Last Seen Time", "map_sharedPin": "Shared pin", "map_joinRoom": "Join Room", diff --git a/lib/l10n/app_es.arb b/lib/l10n/app_es.arb index 1d3c4d5..82ffa55 100644 --- a/lib/l10n/app_es.arb +++ b/lib/l10n/app_es.arb @@ -1854,5 +1854,7 @@ "contactsSettings_overwriteOldestSubtitle": "Cuando la lista de contactos esté llena, se reemplazará el contacto no favorito más antiguo.", "common_deleteAll": "Eliminar todo", "discoveredContacts_deleteContactAll": "Eliminar Todos los Contactos Descubiertos", - "discoveredContacts_deleteContactAllContent": "¿Está seguro de que desea eliminar todos los contactos descubiertos!" + "discoveredContacts_deleteContactAllContent": "¿Está seguro de que desea eliminar todos los contactos descubiertos!", + "map_guessedLocation": "Ubicación estimada", + "map_showGuessedLocations": "Mostrar las ubicaciones estimadas de los nodos." } diff --git a/lib/l10n/app_fr.arb b/lib/l10n/app_fr.arb index 1eae5b9..42d6ecb 100644 --- a/lib/l10n/app_fr.arb +++ b/lib/l10n/app_fr.arb @@ -1826,5 +1826,7 @@ "contactsSettings_overwriteOldestSubtitle": "Lorsque la liste de contacts est pleine, le contact le plus ancien non favori sera remplacé.", "common_deleteAll": "Supprimer tout", "discoveredContacts_deleteContactAll": "Supprimer tous les contacts découverts", - "discoveredContacts_deleteContactAllContent": "Êtes-vous sûr de vouloir supprimer tous les contacts découverts ?" + "discoveredContacts_deleteContactAllContent": "Êtes-vous sûr de vouloir supprimer tous les contacts découverts ?", + "map_showGuessedLocations": "Afficher les emplacements des nœuds estimés", + "map_guessedLocation": "Lieu deviné" } diff --git a/lib/l10n/app_it.arb b/lib/l10n/app_it.arb index 9a1f1a4..f3f3f53 100644 --- a/lib/l10n/app_it.arb +++ b/lib/l10n/app_it.arb @@ -1826,5 +1826,7 @@ "contactsSettings_overwriteOldestSubtitle": "Quando l'elenco dei contatti è pieno, il contatto più vecchio non tra i preferiti verrà sostituito.", "common_deleteAll": "Elimina tutto", "discoveredContacts_deleteContactAllContent": "Sei sicuro di voler eliminare tutti i contatti scoperti?", - "discoveredContacts_deleteContactAll": "Eliminare tutti i contatti scoperti" + "discoveredContacts_deleteContactAll": "Eliminare tutti i contatti scoperti", + "map_guessedLocation": "Località indovinata", + "map_showGuessedLocations": "Mostra le posizioni stimate dei nodi" } diff --git a/lib/l10n/app_localizations.dart b/lib/l10n/app_localizations.dart index 0eee461..7a77e19 100644 --- a/lib/l10n/app_localizations.dart +++ b/lib/l10n/app_localizations.dart @@ -2638,6 +2638,18 @@ abstract class AppLocalizations { /// **'Show shared markers'** String get map_showSharedMarkers; + /// No description provided for @map_showGuessedLocations. + /// + /// In en, this message translates to: + /// **'Show guessed node locations'** + String get map_showGuessedLocations; + + /// No description provided for @map_guessedLocation. + /// + /// In en, this message translates to: + /// **'Guessed location'** + String get map_guessedLocation; + /// No description provided for @map_lastSeenTime. /// /// In en, this message translates to: diff --git a/lib/l10n/app_localizations_bg.dart b/lib/l10n/app_localizations_bg.dart index aa597c5..85dcf42 100644 --- a/lib/l10n/app_localizations_bg.dart +++ b/lib/l10n/app_localizations_bg.dart @@ -1443,6 +1443,13 @@ class AppLocalizationsBg extends AppLocalizations { @override String get map_showSharedMarkers => 'Покажи споделени маркери'; + @override + String get map_showGuessedLocations => + 'Покажете местоположенията на предположените възли.'; + + @override + String get map_guessedLocation => 'Предполагано местоположение'; + @override String get map_lastSeenTime => 'Последна видяна дата'; diff --git a/lib/l10n/app_localizations_de.dart b/lib/l10n/app_localizations_de.dart index 479a050..67dcad8 100644 --- a/lib/l10n/app_localizations_de.dart +++ b/lib/l10n/app_localizations_de.dart @@ -1442,6 +1442,13 @@ class AppLocalizationsDe extends AppLocalizations { @override String get map_showSharedMarkers => 'Zeige gemeinsam genutzte Marker'; + @override + String get map_showGuessedLocations => + 'Zeige die vermuteten Knotenpositionen'; + + @override + String get map_guessedLocation => 'Geschätzter Ort'; + @override String get map_lastSeenTime => 'Letzte Sichtung'; diff --git a/lib/l10n/app_localizations_en.dart b/lib/l10n/app_localizations_en.dart index 2fbc97a..87742a6 100644 --- a/lib/l10n/app_localizations_en.dart +++ b/lib/l10n/app_localizations_en.dart @@ -1421,6 +1421,12 @@ class AppLocalizationsEn extends AppLocalizations { @override String get map_showSharedMarkers => 'Show shared markers'; + @override + String get map_showGuessedLocations => 'Show guessed node locations'; + + @override + String get map_guessedLocation => 'Guessed location'; + @override String get map_lastSeenTime => 'Last Seen Time'; diff --git a/lib/l10n/app_localizations_es.dart b/lib/l10n/app_localizations_es.dart index 6227e5f..18166ae 100644 --- a/lib/l10n/app_localizations_es.dart +++ b/lib/l10n/app_localizations_es.dart @@ -1440,6 +1440,13 @@ class AppLocalizationsEs extends AppLocalizations { @override String get map_showSharedMarkers => 'Mostrar marcadores compartidos'; + @override + String get map_showGuessedLocations => + 'Mostrar las ubicaciones estimadas de los nodos.'; + + @override + String get map_guessedLocation => 'Ubicación estimada'; + @override String get map_lastSeenTime => 'Última vez que se vio'; diff --git a/lib/l10n/app_localizations_fr.dart b/lib/l10n/app_localizations_fr.dart index 7ce62b9..7f36c78 100644 --- a/lib/l10n/app_localizations_fr.dart +++ b/lib/l10n/app_localizations_fr.dart @@ -1447,6 +1447,13 @@ class AppLocalizationsFr extends AppLocalizations { @override String get map_showSharedMarkers => 'Afficher les marqueurs partagés'; + @override + String get map_showGuessedLocations => + 'Afficher les emplacements des nœuds estimés'; + + @override + String get map_guessedLocation => 'Lieu deviné'; + @override String get map_lastSeenTime => 'Dernière fois vu'; diff --git a/lib/l10n/app_localizations_it.dart b/lib/l10n/app_localizations_it.dart index d173456..f84f461 100644 --- a/lib/l10n/app_localizations_it.dart +++ b/lib/l10n/app_localizations_it.dart @@ -1439,6 +1439,12 @@ class AppLocalizationsIt extends AppLocalizations { @override String get map_showSharedMarkers => 'Mostra i segnaposto condivisi'; + @override + String get map_showGuessedLocations => 'Mostra le posizioni stimate dei nodi'; + + @override + String get map_guessedLocation => 'Località indovinata'; + @override String get map_lastSeenTime => 'Ultimo Tempo di Visualizzazione'; diff --git a/lib/l10n/app_localizations_nl.dart b/lib/l10n/app_localizations_nl.dart index 2a873a5..5ed036d 100644 --- a/lib/l10n/app_localizations_nl.dart +++ b/lib/l10n/app_localizations_nl.dart @@ -1434,6 +1434,13 @@ class AppLocalizationsNl extends AppLocalizations { @override String get map_showSharedMarkers => 'Toon gedeelde markeringen'; + @override + String get map_showGuessedLocations => + 'Toon de voorspelde locaties van de knopen'; + + @override + String get map_guessedLocation => 'Geroerde locatie'; + @override String get map_lastSeenTime => 'Laatste Bekeken Tijd'; diff --git a/lib/l10n/app_localizations_pl.dart b/lib/l10n/app_localizations_pl.dart index e8ee410..70c8061 100644 --- a/lib/l10n/app_localizations_pl.dart +++ b/lib/l10n/app_localizations_pl.dart @@ -1440,6 +1440,13 @@ class AppLocalizationsPl extends AppLocalizations { @override String get map_showSharedMarkers => 'Pokaż współdzielone znaki.'; + @override + String get map_showGuessedLocations => + 'Wyświetl lokalizacje zgadanych węzłów'; + + @override + String get map_guessedLocation => 'Wydana lokalizacja'; + @override String get map_lastSeenTime => 'Ostatni raz widiany'; diff --git a/lib/l10n/app_localizations_pt.dart b/lib/l10n/app_localizations_pt.dart index 93e1ffb..aa37f8a 100644 --- a/lib/l10n/app_localizations_pt.dart +++ b/lib/l10n/app_localizations_pt.dart @@ -1441,6 +1441,13 @@ class AppLocalizationsPt extends AppLocalizations { @override String get map_showSharedMarkers => 'Mostrar marcadores compartilhados'; + @override + String get map_showGuessedLocations => + 'Mostrar as localizações dos nós estimados'; + + @override + String get map_guessedLocation => 'Localização estimada'; + @override String get map_lastSeenTime => 'Último Tempo de Visualização'; diff --git a/lib/l10n/app_localizations_ru.dart b/lib/l10n/app_localizations_ru.dart index 1e59160..affb256 100644 --- a/lib/l10n/app_localizations_ru.dart +++ b/lib/l10n/app_localizations_ru.dart @@ -1442,6 +1442,13 @@ class AppLocalizationsRu extends AppLocalizations { @override String get map_showSharedMarkers => 'Показывать общие метки'; + @override + String get map_showGuessedLocations => + 'Отобразить предполагаемые места расположения узлов'; + + @override + String get map_guessedLocation => 'Угаданное место'; + @override String get map_lastSeenTime => 'Время последнего появления'; diff --git a/lib/l10n/app_localizations_sk.dart b/lib/l10n/app_localizations_sk.dart index 6e68a97..0861ca8 100644 --- a/lib/l10n/app_localizations_sk.dart +++ b/lib/l10n/app_localizations_sk.dart @@ -1435,6 +1435,13 @@ class AppLocalizationsSk extends AppLocalizations { @override String get map_showSharedMarkers => 'Zobraziť zdieľané značky'; + @override + String get map_showGuessedLocations => + 'Zobraziť umiestnenia odhadnutých uzlov'; + + @override + String get map_guessedLocation => 'Odhadnutá lokalita'; + @override String get map_lastSeenTime => 'Posledný čas sledovania'; diff --git a/lib/l10n/app_localizations_sl.dart b/lib/l10n/app_localizations_sl.dart index 9b08106..0b4bd0f 100644 --- a/lib/l10n/app_localizations_sl.dart +++ b/lib/l10n/app_localizations_sl.dart @@ -1431,6 +1431,12 @@ class AppLocalizationsSl extends AppLocalizations { @override String get map_showSharedMarkers => 'Pokaži skupno označenja'; + @override + String get map_showGuessedLocations => 'Pokaži lokacije domnevnih not.'; + + @override + String get map_guessedLocation => 'Predpostavljena lokacija'; + @override String get map_lastSeenTime => 'Datum zadnjega vpogleda'; diff --git a/lib/l10n/app_localizations_sv.dart b/lib/l10n/app_localizations_sv.dart index 2a41947..7281f10 100644 --- a/lib/l10n/app_localizations_sv.dart +++ b/lib/l10n/app_localizations_sv.dart @@ -1427,6 +1427,13 @@ class AppLocalizationsSv extends AppLocalizations { @override String get map_showSharedMarkers => 'Visa delade markörer'; + @override + String get map_showGuessedLocations => + 'Visa upp de antagna nodernas placeringar'; + + @override + String get map_guessedLocation => 'Gissad plats'; + @override String get map_lastSeenTime => 'Senaste Visats Tid'; diff --git a/lib/l10n/app_localizations_uk.dart b/lib/l10n/app_localizations_uk.dart index 87d2318..5bf6da2 100644 --- a/lib/l10n/app_localizations_uk.dart +++ b/lib/l10n/app_localizations_uk.dart @@ -1441,6 +1441,13 @@ class AppLocalizationsUk extends AppLocalizations { @override String get map_showSharedMarkers => 'Показувати спільні маркери'; + @override + String get map_showGuessedLocations => + 'Показати місцезнаходження передбачених вузлів'; + + @override + String get map_guessedLocation => 'Визначено місцезнаходження'; + @override String get map_lastSeenTime => 'Час останньої активності'; diff --git a/lib/l10n/app_localizations_zh.dart b/lib/l10n/app_localizations_zh.dart index eb79671..c3ee3e6 100644 --- a/lib/l10n/app_localizations_zh.dart +++ b/lib/l10n/app_localizations_zh.dart @@ -1363,6 +1363,12 @@ class AppLocalizationsZh extends AppLocalizations { @override String get map_showSharedMarkers => '显示共享标记'; + @override + String get map_showGuessedLocations => '显示猜测的节点位置'; + + @override + String get map_guessedLocation => '猜测的位置'; + @override String get map_lastSeenTime => '最后在线时间'; diff --git a/lib/l10n/app_nl.arb b/lib/l10n/app_nl.arb index ce533e7..cde7457 100644 --- a/lib/l10n/app_nl.arb +++ b/lib/l10n/app_nl.arb @@ -1826,5 +1826,7 @@ "contactsSettings_overwriteOldestSubtitle": "Wanneer de contactenlijst vol is, wordt de oudste niet-favoriete contactpersoon vervangen.", "common_deleteAll": "Alles verwijderen", "discoveredContacts_deleteContactAll": "Verwijder alle ontdekte contacten", - "discoveredContacts_deleteContactAllContent": "Weet u zeker dat u alle ontdekte contacten wilt verwijderen?" + "discoveredContacts_deleteContactAllContent": "Weet u zeker dat u alle ontdekte contacten wilt verwijderen?", + "map_guessedLocation": "Geroerde locatie", + "map_showGuessedLocations": "Toon de voorspelde locaties van de knopen" } diff --git a/lib/l10n/app_pl.arb b/lib/l10n/app_pl.arb index 509be40..41152a5 100644 --- a/lib/l10n/app_pl.arb +++ b/lib/l10n/app_pl.arb @@ -1826,5 +1826,7 @@ "contactsSettings_overwriteOldestSubtitle": "Gdy lista kontaktów jest pełna, najstarszy nieulubiony kontakt zostanie zastąpiony.", "common_deleteAll": "Usuń wszystko", "discoveredContacts_deleteContactAllContent": "Czy na pewno chcesz usunąć wszystkie znalezione kontakty?", - "discoveredContacts_deleteContactAll": "Usuń wszystkie odkryte kontakty" + "discoveredContacts_deleteContactAll": "Usuń wszystkie odkryte kontakty", + "map_guessedLocation": "Wydana lokalizacja", + "map_showGuessedLocations": "Wyświetl lokalizacje zgadanych węzłów" } diff --git a/lib/l10n/app_pt.arb b/lib/l10n/app_pt.arb index 8989d29..f843119 100644 --- a/lib/l10n/app_pt.arb +++ b/lib/l10n/app_pt.arb @@ -1826,5 +1826,7 @@ "contactsSettings_overwriteOldestSubtitle": "Quando a lista de contatos estiver cheia, o contato mais antigo não favoritado será substituído.", "common_deleteAll": "Excluir Tudo", "discoveredContacts_deleteContactAll": "Excluir Todos os Contatos Descobertos", - "discoveredContacts_deleteContactAllContent": "Tem certeza de que deseja excluir todos os contatos descobertos?" + "discoveredContacts_deleteContactAllContent": "Tem certeza de que deseja excluir todos os contatos descobertos?", + "map_guessedLocation": "Localização estimada", + "map_showGuessedLocations": "Mostrar as localizações dos nós estimados" } diff --git a/lib/l10n/app_ru.arb b/lib/l10n/app_ru.arb index 42b440f..0897cba 100644 --- a/lib/l10n/app_ru.arb +++ b/lib/l10n/app_ru.arb @@ -1066,5 +1066,7 @@ "contactsSettings_overwriteOldestSubtitle": "Когда список контактов заполнен, будет заменен самый старый контакт, который не находится в избранном.", "common_deleteAll": "Удалить все", "discoveredContacts_deleteContactAllContent": "Вы уверены, что хотите удалить все обнаруженные контакты?", - "discoveredContacts_deleteContactAll": "Удалить Все Обнаруженные Контакты" + "discoveredContacts_deleteContactAll": "Удалить Все Обнаруженные Контакты", + "map_guessedLocation": "Угаданное место", + "map_showGuessedLocations": "Отобразить предполагаемые места расположения узлов" } diff --git a/lib/l10n/app_sk.arb b/lib/l10n/app_sk.arb index 7fea7e3..16aae9b 100644 --- a/lib/l10n/app_sk.arb +++ b/lib/l10n/app_sk.arb @@ -1826,5 +1826,7 @@ "contactsSettings_overwriteOldestSubtitle": "Keď je zoznam kontaktov plný, bude nahradený najstarší neoznačený kontakt.", "discoveredContacts_deleteContactAll": "Zmazať všetky objavené kontakty", "common_deleteAll": "Zmazať všetko", - "discoveredContacts_deleteContactAllContent": "Ste si istí, že chcete zmazať všetky objavené kontakty?" + "discoveredContacts_deleteContactAllContent": "Ste si istí, že chcete zmazať všetky objavené kontakty?", + "map_showGuessedLocations": "Zobraziť umiestnenia odhadnutých uzlov", + "map_guessedLocation": "Odhadnutá lokalita" } diff --git a/lib/l10n/app_sl.arb b/lib/l10n/app_sl.arb index 8dadec8..97d913f 100644 --- a/lib/l10n/app_sl.arb +++ b/lib/l10n/app_sl.arb @@ -1826,5 +1826,7 @@ "contactsSettings_overwriteOldestSubtitle": "Ko je seznam stikov poln, bo najstarejši nestarševski stik zamenjan.", "common_deleteAll": "Izbriši vse", "discoveredContacts_deleteContactAllContent": "Ste prepričani, da želite izbrisati vse odkrite kontakte?", - "discoveredContacts_deleteContactAll": "Izbriši vse odkrite kontakte" + "discoveredContacts_deleteContactAll": "Izbriši vse odkrite kontakte", + "map_guessedLocation": "Predpostavljena lokacija", + "map_showGuessedLocations": "Pokaži lokacije domnevnih not." } diff --git a/lib/l10n/app_sv.arb b/lib/l10n/app_sv.arb index 2467015..b451082 100644 --- a/lib/l10n/app_sv.arb +++ b/lib/l10n/app_sv.arb @@ -1826,5 +1826,7 @@ "contactsSettings_overwriteOldestSubtitle": "När kontaktlistan är full ersätts den äldsta icke-favoriterade kontakten.", "common_deleteAll": "Ta bort alla", "discoveredContacts_deleteContactAllContent": "Är du säker på att du vill ta bort alla upptäckta kontakter?", - "discoveredContacts_deleteContactAll": "Ta bort alla upptäckta kontakter" + "discoveredContacts_deleteContactAll": "Ta bort alla upptäckta kontakter", + "map_guessedLocation": "Gissad plats", + "map_showGuessedLocations": "Visa upp de antagna nodernas placeringar" } diff --git a/lib/l10n/app_uk.arb b/lib/l10n/app_uk.arb index dc48f09..fb60b81 100644 --- a/lib/l10n/app_uk.arb +++ b/lib/l10n/app_uk.arb @@ -1826,5 +1826,7 @@ "contactsSettings_overwriteOldestSubtitle": "Коли список контактів заповнений, найстарший контакт без позначки улюбленого буде замінений.", "common_deleteAll": "Видалити все", "discoveredContacts_deleteContactAll": "Видалити всі виявлені контакти", - "discoveredContacts_deleteContactAllContent": "Ви впевнені, що хочете видалити всі виявлені контакти?" + "discoveredContacts_deleteContactAllContent": "Ви впевнені, що хочете видалити всі виявлені контакти?", + "map_showGuessedLocations": "Показати місцезнаходження передбачених вузлів", + "map_guessedLocation": "Визначено місцезнаходження" } diff --git a/lib/l10n/app_zh.arb b/lib/l10n/app_zh.arb index ad58f4c..51bd60c 100644 --- a/lib/l10n/app_zh.arb +++ b/lib/l10n/app_zh.arb @@ -1831,5 +1831,7 @@ "contactsSettings_overwriteOldestSubtitle": "当联系人列表已满时,将替换最老的非收藏联系人。", "common_deleteAll": "删除全部", "discoveredContacts_deleteContactAllContent": "您确定要删除所有发现的联系人吗?", - "discoveredContacts_deleteContactAll": "删除所有发现的联系人" + "discoveredContacts_deleteContactAll": "删除所有发现的联系人", + "map_showGuessedLocations": "显示猜测的节点位置", + "map_guessedLocation": "猜测的位置" } diff --git a/lib/models/app_settings.dart b/lib/models/app_settings.dart index 62ba9ca..abcc729 100644 --- a/lib/models/app_settings.dart +++ b/lib/models/app_settings.dart @@ -22,6 +22,7 @@ class AppSettings { final bool mapKeyPrefixEnabled; final String mapKeyPrefix; final bool mapShowMarkers; + final bool mapShowGuessedLocations; final bool enableMessageTracing; final Map? mapCacheBounds; final int mapCacheMinZoom; @@ -48,6 +49,7 @@ class AppSettings { this.mapKeyPrefixEnabled = false, this.mapKeyPrefix = '', this.mapShowMarkers = true, + this.mapShowGuessedLocations = true, this.enableMessageTracing = false, this.mapCacheBounds, this.mapCacheMinZoom = 10, @@ -78,6 +80,7 @@ class AppSettings { 'map_key_prefix_enabled': mapKeyPrefixEnabled, 'map_key_prefix': mapKeyPrefix, 'map_show_markers': mapShowMarkers, + 'map_show_guessed_locations': mapShowGuessedLocations, 'enable_message_tracing': enableMessageTracing, 'map_cache_bounds': mapCacheBounds, 'map_cache_min_zoom': mapCacheMinZoom, @@ -115,6 +118,8 @@ class AppSettings { mapKeyPrefixEnabled: json['map_key_prefix_enabled'] as bool? ?? false, mapKeyPrefix: json['map_key_prefix'] as String? ?? '', mapShowMarkers: json['map_show_markers'] as bool? ?? true, + mapShowGuessedLocations: + json['map_show_guessed_locations'] as bool? ?? true, enableMessageTracing: json['enable_message_tracing'] as bool? ?? false, mapCacheBounds: (json['map_cache_bounds'] as Map?)?.map( (key, value) => MapEntry(key.toString(), (value as num).toDouble()), @@ -159,6 +164,7 @@ class AppSettings { bool? mapKeyPrefixEnabled, String? mapKeyPrefix, bool? mapShowMarkers, + bool? mapShowGuessedLocations, bool? enableMessageTracing, Object? mapCacheBounds = _unset, int? mapCacheMinZoom, @@ -185,6 +191,8 @@ class AppSettings { mapKeyPrefixEnabled: mapKeyPrefixEnabled ?? this.mapKeyPrefixEnabled, mapKeyPrefix: mapKeyPrefix ?? this.mapKeyPrefix, mapShowMarkers: mapShowMarkers ?? this.mapShowMarkers, + mapShowGuessedLocations: + mapShowGuessedLocations ?? this.mapShowGuessedLocations, enableMessageTracing: enableMessageTracing ?? this.enableMessageTracing, mapCacheBounds: mapCacheBounds == _unset ? this.mapCacheBounds diff --git a/lib/screens/chat_screen.dart b/lib/screens/chat_screen.dart index 7c8fcfb..0075040 100644 --- a/lib/screens/chat_screen.dart +++ b/lib/screens/chat_screen.dart @@ -818,6 +818,7 @@ class _ChatScreenState extends State { title: context.l10n.contacts_repeaterPathTrace, path: Uint8List.fromList(pathBytes), flipPathRound: true, + targetContact: widget.contact, ), ), ), diff --git a/lib/screens/contacts_screen.dart b/lib/screens/contacts_screen.dart index 47bac9c..e5cc785 100644 --- a/lib/screens/contacts_screen.dart +++ b/lib/screens/contacts_screen.dart @@ -1131,6 +1131,7 @@ class _ContactsScreenState extends State contact.name, ), path: contact.traceRouteBytes ?? Uint8List(0), + targetContact: contact, ), ), ); diff --git a/lib/screens/map_screen.dart b/lib/screens/map_screen.dart index 2ec71a0..c4808f1 100644 --- a/lib/screens/map_screen.dart +++ b/lib/screens/map_screen.dart @@ -15,6 +15,7 @@ import '../models/app_settings.dart'; import '../models/channel.dart'; import '../models/contact.dart'; import '../services/app_settings_service.dart'; +import '../services/path_history_service.dart'; import '../services/map_marker_service.dart'; import '../services/map_tile_cache_service.dart'; import '../utils/contact_search.dart'; @@ -64,6 +65,8 @@ class _MapScreenState extends State { final List _polylines = []; bool _legendExpanded = false; bool _showNodeLabels = true; + List<_GuessedLocation> _cachedGuessedLocations = []; + String _guessedLocationsCacheKey = ''; @override void initState() { @@ -119,8 +122,8 @@ class _MapScreenState extends State { @override Widget build(BuildContext context) { - return Consumer2( - builder: (context, connector, settingsService, child) { + return Consumer3( + builder: (context, connector, settingsService, pathHistory, child) { final tileCache = context.read(); final settings = settingsService.settings; final contacts = connector.contacts; @@ -160,6 +163,31 @@ class _MapScreenState extends State { .where((c) => c.hasLocation) .toList(); + // All contacts with a known location — used as anchors regardless of + // time/key-prefix filters so that repeaters are always available. + final allContactsWithLocation = contacts + .where((c) => c.hasLocation) + .toList(); + + // Compute guessed locations with caching + final maxRangeKm = _estimateLoRaRangeKm(connector); + final cacheKey = + '${filteredByKeyPrefix.length}:${allContactsWithLocation.length}:${pathHistory.version}:${connector.currentSf}:${connector.currentBwHz}:${connector.currentTxPower}:${settings.mapShowGuessedLocations}'; + if (cacheKey != _guessedLocationsCacheKey) { + _guessedLocationsCacheKey = cacheKey; + _cachedGuessedLocations = settings.mapShowGuessedLocations + ? _computeGuessedLocations( + filteredByKeyPrefix, + allContactsWithLocation, + pathHistory, + maxRangeKm, + ) + : []; + } + final guessedLocations = settings.mapShowGuessedLocations + ? _cachedGuessedLocations + : <_GuessedLocation>[]; + _polylines.clear(); _polylines.addAll( _points.length > 1 @@ -430,6 +458,8 @@ class _MapScreenState extends State { size: 34, ), ), + if (!_isBuildingPathTrace) + ...guessedLocations.map(_buildGuessedMarker), ..._buildMarkers( contactsWithLocation, settings, @@ -489,6 +519,7 @@ class _MapScreenState extends State { contactsWithLocation, settings, sharedMarkers.length, + guessedLocations.length, ), if (_isBuildingPathTrace) _buildPathTraceOverlay(), ], @@ -512,6 +543,201 @@ class _MapScreenState extends State { ); } + List<_GuessedLocation> _computeGuessedLocations( + List allContacts, + List withLocation, + PathHistoryService pathHistory, + double? maxRangeKm, + ) { + // Index known-location repeaters by their 1-byte hash. + // null value = two repeaters share the same hash byte (ambiguous collision). + final repeaterByHash = {}; + for (final c in withLocation) { + if (c.type == advTypeRepeater) { + if (repeaterByHash.containsKey(c.publicKey[0])) { + repeaterByHash[c.publicKey[0]] = null; // collision: can't disambiguate + } else { + repeaterByHash[c.publicKey[0]] = c; + } + } + } + + final result = <_GuessedLocation>[]; + + for (final contact in allContacts) { + if (contact.hasLocation) continue; + + final anchorSet = {}; + + // Collect the contact-side (last-hop) repeater from every known path. + // path = [device-side hop, ..., contact-side hop] + // Only path.last is actually within radio range of the contact — using + // earlier bytes would anchor against our own side of the network. + final pathSets = >[ + contact.path.toList(), + ...pathHistory + .getRecentPaths(contact.publicKeyHex) + .map((r) => r.pathBytes), + ]; + final lastHopBytes = {}; + for (final pathBytes in pathSets) { + if (pathBytes.isEmpty) continue; + final lastHop = pathBytes.last; + lastHopBytes.add(lastHop); + final r = repeaterByHash[lastHop]; + if (r != null) anchorSet.add(LatLng(r.latitude!, r.longitude!)); + } + + // Fallback: for any last-hop byte with no GPS repeater, average the + // positions of contacts with known GPS that share the same last hop. + // Those contacts are all adjacent to the same unknown repeater, so their + // centroid is a reasonable proxy for its location. + for (final byte in lastHopBytes) { + if (repeaterByHash.containsKey(byte)) continue; + for (final c in withLocation) { + if (c.path.isNotEmpty && c.path.last == byte) { + anchorSet.add(LatLng(c.latitude!, c.longitude!)); + } + } + } + + // Filter anchors that are geometrically inconsistent with radio range. + // Two anchors more than 2 * maxRange apart cannot both be in direct radio + // range of the same node, so isolated outliers are removed. + final anchors = maxRangeKm != null && anchorSet.length > 1 + ? _filterConsistentAnchors(anchorSet.toList(), maxRangeKm) + : anchorSet.toList(); + + if (anchors.isEmpty) continue; + + final LatLng position; + if (anchors.length == 1) { + // Offset single-anchor guesses so they don't overlap the repeater marker. + // Use the contact's public key byte as a deterministic angle seed. + const offsetDeg = 0.003; // ~330 m at the equator + final angle = (contact.publicKey[1] / 255.0) * 2 * pi; + position = LatLng( + anchors[0].latitude + offsetDeg * cos(angle), + anchors[0].longitude + offsetDeg * sin(angle), + ); + } else { + double lat = 0, lon = 0; + for (final a in anchors) { + lat += a.latitude; + lon += a.longitude; + } + position = LatLng(lat / anchors.length, lon / anchors.length); + } + result.add( + _GuessedLocation( + contact: contact, + position: position, + highConfidence: anchors.length >= 2, + ), + ); + } + + return result; + } + + /// Estimates the free-space maximum LoRa range in km from the connected + /// device's current radio parameters. Returns null if parameters are unknown. + double? _estimateLoRaRangeKm(MeshCoreConnector connector) { + final freqHz = connector.currentFreqHz; + final bwHz = connector.currentBwHz; + final sf = connector.currentSf; + final txPower = connector.currentTxPower; + if (freqHz == null || bwHz == null || sf == null || txPower == null) { + return null; + } + // LoRa receiver sensitivity = thermal noise + NF + required demod SNR + const noiseFigureDb = 6.0; + final thermalNoiseDbm = -174.0 + 10 * log(bwHz.toDouble()) / ln10; + final sensitivityDbm = + thermalNoiseDbm + noiseFigureDb + _sfToRequiredSnrDb(sf); + // FSPL at max range equals link budget: + // FSPL = 20*log10(d_m) + 20*log10(f_hz) - 147.55 + final linkBudgetDb = txPower.toDouble() - sensitivityDbm; + final exponent = + (linkBudgetDb + 147.55 - 20 * log(freqHz.toDouble()) / ln10) / 20; + return pow(10, exponent) / 1000; + } + + double _sfToRequiredSnrDb(int sf) { + switch (sf) { + case 5: + return -2.5; + case 6: + return -5.0; + case 7: + return -7.5; + case 8: + return -10.0; + case 9: + return -12.5; + case 10: + return -15.0; + case 11: + return -17.5; + case 12: + return -20.0; + default: + return -10.0; + } + } + + /// Removes anchors that have no neighbour within 2 * maxRangeKm. + /// A node cannot be simultaneously in radio range of two points farther apart + /// than twice the expected maximum range. + List _filterConsistentAnchors( + List anchors, + double maxRangeKm, + ) { + const distance = Distance(); + final maxDistM = maxRangeKm * 2000; + return anchors + .where( + (a) => anchors.any((b) => b != a && distance(a, b) <= maxDistM), + ) + .toList(); + } + + Marker _buildGuessedMarker(_GuessedLocation guess) { + final color = _getNodeColor(guess.contact.type); + return Marker( + point: guess.position, + width: 35, + height: 35, + child: GestureDetector( + onTap: () => _showNodeInfo( + context, + guess.contact, + guessedPosition: guess.position, + ), + child: Container( + padding: const EdgeInsets.all(4), + decoration: BoxDecoration( + color: color.withValues(alpha: guess.highConfidence ? 0.55 : 0.30), + 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: const Icon( + Icons.not_listed_location, + color: Colors.white, + size: 20, + ), + ), + ), + ); + } + List _buildMarkers( List contacts, settings, { @@ -657,6 +883,7 @@ class _MapScreenState extends State { List contactsWithLocation, settings, int markerCount, + int guessedCount, ) { int nodeCount = 0; for (final contact in contactsWithLocation) { @@ -696,7 +923,12 @@ class _MapScreenState extends State { crossAxisAlignment: CrossAxisAlignment.start, children: [ Text( - context.l10n.map_nodesCount(nodeCount), + context.l10n.map_nodesCount( + nodeCount + + (settings.mapShowGuessedLocations + ? guessedCount + : 0), + ), style: const TextStyle( fontWeight: FontWeight.bold, fontSize: 14, @@ -764,6 +996,12 @@ class _MapScreenState extends State { context.l10n.map_pinPublic, Colors.orange, ), + if (settings.mapShowGuessedLocations && guessedCount > 0) + _buildLegendItem( + Icons.not_listed_location, + context.l10n.map_guessedLocation, + Colors.grey, + ), ], ), ), @@ -952,7 +1190,11 @@ class _MapScreenState extends State { ); } - void _showNodeInfo(BuildContext context, Contact contact) { + void _showNodeInfo( + BuildContext context, + Contact contact, { + LatLng? guessedPosition, + }) { showDialog( context: context, builder: (dialogContext) => AlertDialog( @@ -972,10 +1214,16 @@ class _MapScreenState extends State { children: [ _buildInfoRow('Type', contact.typeLabel), _buildInfoRow('Path', contact.pathLabel), - _buildInfoRow( - 'Location', - '${contact.latitude!.toStringAsFixed(6)}, ${contact.longitude!.toStringAsFixed(6)}', - ), + if (contact.hasLocation) + _buildInfoRow( + 'Location', + '${contact.latitude!.toStringAsFixed(6)}, ${contact.longitude!.toStringAsFixed(6)}', + ) + else if (guessedPosition != null) + _buildInfoRow( + 'Est. Location', + '~${guessedPosition.latitude.toStringAsFixed(6)}, ${guessedPosition.longitude.toStringAsFixed(6)}', + ), _buildInfoRow( context.l10n.map_lastSeen, _formatLastSeen(contact.lastSeen), @@ -1481,6 +1729,14 @@ class _MapScreenState extends State { }, contentPadding: EdgeInsets.zero, ), + CheckboxListTile( + title: Text(context.l10n.map_showGuessedLocations), + value: settings.mapShowGuessedLocations, + onChanged: (value) { + service.setMapShowGuessedLocations(value ?? true); + }, + contentPadding: EdgeInsets.zero, + ), const SizedBox(height: 16), Text( context.l10n.map_keyPrefix, @@ -1744,6 +2000,18 @@ class _MapScreenState extends State { } } +class _GuessedLocation { + final Contact contact; + final LatLng position; + final bool highConfidence; + + _GuessedLocation({ + required this.contact, + required this.position, + required this.highConfidence, + }); +} + class _MarkerPayload { final LatLng position; final String label; diff --git a/lib/screens/path_trace_map.dart b/lib/screens/path_trace_map.dart index 5f86cc1..465d7db 100644 --- a/lib/screens/path_trace_map.dart +++ b/lib/screens/path_trace_map.dart @@ -54,6 +54,7 @@ class PathTraceMapScreen extends StatefulWidget { final int? repeaterId; final bool flipPathRound; final bool reversePathRound; + final Contact? targetContact; const PathTraceMapScreen({ super.key, @@ -62,6 +63,7 @@ class PathTraceMapScreen extends StatefulWidget { this.repeaterId, this.flipPathRound = false, this.reversePathRound = false, + this.targetContact, }); @override @@ -78,6 +80,11 @@ class _PathTraceMapScreenState extends State { bool _failed2Loaded = false; bool _hasData = false; PathTraceData? _traceData; + // Inferred positions for hops that have no GPS location, keyed by hop byte. + Map _inferredHopPositions = {}; + // Endpoint position for the target contact (GPS or guessed). + LatLng? _targetContactPosition; + bool _targetContactIsGuessed = false; List _points = []; List _polylines = []; LatLng? _initialCenter = LatLng(0, 0); @@ -242,25 +249,90 @@ class _PathTraceMapScreenState extends State { } }); + // For hops with no GPS contact, infer position from other contacts + // with known GPS that share the same last-hop byte. + final Map inferredPositions = {}; + for (final hop in pathData) { + final contact = pathContacts[hop]; + if (contact != null && contact.hasLocation) continue; + final peers = connector.contacts + .where( + (c) => + c.hasLocation && + c.path.isNotEmpty && + c.path.last == hop, + ) + .toList(); + if (peers.isNotEmpty) { + final lat = + peers.map((c) => c.latitude!).reduce((a, b) => a + b) / + peers.length; + final lon = + peers.map((c) => c.longitude!).reduce((a, b) => a + b) / + peers.length; + inferredPositions[hop] = LatLng(lat, lon); + } + } + setState(() { _isLoading = false; _hasData = true; + _inferredHopPositions = inferredPositions; _traceData = PathTraceData( pathData: pathData, snrData: snrData, pathContacts: pathContacts, ); + // Compute endpoint position for the target contact. + LatLng? targetPos; + bool targetGuessed = false; + final target = widget.targetContact; + if (target != null) { + if (target.hasLocation) { + targetPos = LatLng(target.latitude!, target.longitude!); + } else if (pathData.isNotEmpty) { + // Infer from the last hop: average GPS contacts sharing that hop. + final lastHop = pathData.last; + final peers = connector.contacts + .where( + (c) => + c.hasLocation && + c.path.isNotEmpty && + c.path.last == lastHop, + ) + .toList(); + if (peers.isNotEmpty) { + final lat = + peers.map((c) => c.latitude!).reduce((a, b) => a + b) / + peers.length; + final lon = + peers.map((c) => c.longitude!).reduce((a, b) => a + b) / + peers.length; + const offsetDeg = 0.003; + final angle = (target.publicKey[1] / 255.0) * 2 * pi; + targetPos = LatLng( + lat + offsetDeg * cos(angle), + lon + offsetDeg * sin(angle), + ); + targetGuessed = true; + } + } + } + _targetContactPosition = targetPos; + _targetContactIsGuessed = targetGuessed; + _points = []; _points.add(LatLng(connector.selfLatitude!, connector.selfLongitude!)); for (final hop in _traceData!.pathData) { final contact = _traceData!.pathContacts[hop]; - if (contact != null && - contact.hasLocation && - contact.latitude != null && - contact.longitude != null) { + if (contact != null && contact.hasLocation) { _points.add(LatLng(contact.latitude!, contact.longitude!)); + } else { + final inferred = inferredPositions[hop]; + if (inferred != null) _points.add(inferred); } } + if (targetPos != null) _points.add(targetPos); _polylines = _points.length > 1 ? [ Polyline( @@ -382,8 +454,13 @@ class _PathTraceMapScreenState extends State { final markers = []; for (final hop in pathData) { final contact = _traceData!.pathContacts[hop]; - if (contact == null || !contact.hasLocation) continue; - final point = LatLng(contact.latitude!, contact.longitude!); + final inferred = _inferredHopPositions[hop]; + final hasGps = contact != null && contact.hasLocation; + if (!hasGps && inferred == null) continue; + final point = hasGps + ? LatLng(contact.latitude!, contact.longitude!) + : inferred!; + final label = hop.toRadixString(16).padLeft(2, '0').toUpperCase(); markers.add( Marker( point: point, @@ -392,7 +469,9 @@ class _PathTraceMapScreenState extends State { child: Container( padding: const EdgeInsets.all(4), decoration: BoxDecoration( - color: Colors.green, + color: hasGps + ? Colors.green + : Colors.orange.withValues(alpha: 0.75), shape: BoxShape.circle, border: Border.all(color: Colors.white, width: 2), boxShadow: [ @@ -405,10 +484,7 @@ class _PathTraceMapScreenState extends State { ), alignment: Alignment.center, child: Text( - contact.publicKey - .sublist(0, 1) - .map((b) => b.toRadixString(16).padLeft(2, '0').toUpperCase()) - .join(), + hasGps ? label : '~$label', style: const TextStyle( color: Colors.white, fontWeight: FontWeight.bold, @@ -419,7 +495,12 @@ class _PathTraceMapScreenState extends State { ), ); if (showLabels) { - markers.add(_buildNodeLabelMarker(point: point, label: contact.name)); + markers.add( + _buildNodeLabelMarker( + point: point, + label: contact?.name ?? '~$label', + ), + ); } } @@ -468,6 +549,47 @@ class _PathTraceMapScreenState extends State { } } + // Add target contact endpoint marker. + final targetPos = _targetContactPosition; + if (targetPos != null) { + final isGuessed = _targetContactIsGuessed; + final targetName = widget.targetContact?.name ?? '?'; + markers.add( + Marker( + point: targetPos, + width: 35, + height: 35, + child: Container( + padding: const EdgeInsets.all(4), + decoration: BoxDecoration( + color: isGuessed + ? Colors.purple.withValues(alpha: 0.55) + : Colors.red, + 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), + ), + ], + ), + alignment: Alignment.center, + child: const Icon(Icons.person, color: Colors.white, size: 18), + ), + ), + ); + if (showLabels) { + markers.add( + _buildNodeLabelMarker( + point: targetPos, + label: isGuessed ? '~$targetName' : targetName, + ), + ); + } + } + return markers; } diff --git a/lib/services/app_settings_service.dart b/lib/services/app_settings_service.dart index eacf26f..c74fa40 100644 --- a/lib/services/app_settings_service.dart +++ b/lib/services/app_settings_service.dart @@ -80,6 +80,10 @@ class AppSettingsService extends ChangeNotifier { await updateSettings(_settings.copyWith(mapShowMarkers: value)); } + Future setMapShowGuessedLocations(bool value) async { + await updateSettings(_settings.copyWith(mapShowGuessedLocations: value)); + } + Future setEnableMessageTracing(bool value) async { await updateSettings(_settings.copyWith(enableMessageTracing: value)); } diff --git a/lib/services/path_history_service.dart b/lib/services/path_history_service.dart index 1314f48..3da87cc 100644 --- a/lib/services/path_history_service.dart +++ b/lib/services/path_history_service.dart @@ -15,6 +15,9 @@ class PathHistoryService extends ChangeNotifier { final List _cacheAccessOrder = []; static const int _maxHistoryEntries = 100; + + int _version = 0; + int get version => _version; static const int _autoRotationTopCount = 3; PathHistoryService(this._storage); @@ -185,6 +188,7 @@ class PathHistoryService extends ChangeNotifier { ) { var history = _cache[contactPubKeyHex]; if (history == null) return; + _version++; final existing = _findPathRecord(contactPubKeyHex, pathBytes); if (existing != null) { @@ -241,6 +245,7 @@ class PathHistoryService extends ChangeNotifier { _cache[contactPubKeyHex] = loaded; _trackAccess(contactPubKeyHex); _evictIfNeeded(); + _version++; notifyListeners(); } }); diff --git a/lib/widgets/path_management_dialog.dart b/lib/widgets/path_management_dialog.dart index c2b6d12..384f92b 100644 --- a/lib/widgets/path_management_dialog.dart +++ b/lib/widgets/path_management_dialog.dart @@ -79,6 +79,7 @@ class _PathManagementDialogState extends State<_PathManagementDialog> { title: context.l10n.contacts_repeaterPathTrace, path: Uint8List.fromList(pathBytes), flipPathRound: true, + targetContact: widget.contact, ), ), ), From 7c479f912184447e18893dcce59f25dfdd04b6ab Mon Sep 17 00:00:00 2001 From: zach Date: Fri, 6 Mar 2026 15:03:12 -0700 Subject: [PATCH 2/4] Formatted --- lib/screens/map_screen.dart | 7 +++---- lib/screens/path_trace_map.dart | 5 +---- 2 files changed, 4 insertions(+), 8 deletions(-) diff --git a/lib/screens/map_screen.dart b/lib/screens/map_screen.dart index c4808f1..79ce54a 100644 --- a/lib/screens/map_screen.dart +++ b/lib/screens/map_screen.dart @@ -555,7 +555,8 @@ class _MapScreenState extends State { for (final c in withLocation) { if (c.type == advTypeRepeater) { if (repeaterByHash.containsKey(c.publicKey[0])) { - repeaterByHash[c.publicKey[0]] = null; // collision: can't disambiguate + repeaterByHash[c.publicKey[0]] = + null; // collision: can't disambiguate } else { repeaterByHash[c.publicKey[0]] = c; } @@ -696,9 +697,7 @@ class _MapScreenState extends State { const distance = Distance(); final maxDistM = maxRangeKm * 2000; return anchors - .where( - (a) => anchors.any((b) => b != a && distance(a, b) <= maxDistM), - ) + .where((a) => anchors.any((b) => b != a && distance(a, b) <= maxDistM)) .toList(); } diff --git a/lib/screens/path_trace_map.dart b/lib/screens/path_trace_map.dart index 465d7db..df5b19a 100644 --- a/lib/screens/path_trace_map.dart +++ b/lib/screens/path_trace_map.dart @@ -257,10 +257,7 @@ class _PathTraceMapScreenState extends State { if (contact != null && contact.hasLocation) continue; final peers = connector.contacts .where( - (c) => - c.hasLocation && - c.path.isNotEmpty && - c.path.last == hop, + (c) => c.hasLocation && c.path.isNotEmpty && c.path.last == hop, ) .toList(); if (peers.isNotEmpty) { From b2770ef028ec25190bfaf23efaacd7cd0544f312 Mon Sep 17 00:00:00 2001 From: zach Date: Fri, 6 Mar 2026 15:11:21 -0700 Subject: [PATCH 3/4] fix ai suggestions --- lib/screens/map_screen.dart | 8 +++++++- lib/services/path_history_service.dart | 2 ++ 2 files changed, 9 insertions(+), 1 deletion(-) diff --git a/lib/screens/map_screen.dart b/lib/screens/map_screen.dart index 79ce54a..0956b96 100644 --- a/lib/screens/map_screen.dart +++ b/lib/screens/map_screen.dart @@ -171,8 +171,14 @@ class _MapScreenState extends State { // Compute guessed locations with caching final maxRangeKm = _estimateLoRaRangeKm(connector); + final filteredKeys = filteredByKeyPrefix + .map((c) => c.publicKeyHex) + .join(','); + final anchorKeys = allContactsWithLocation + .map((c) => c.publicKeyHex) + .join(','); final cacheKey = - '${filteredByKeyPrefix.length}:${allContactsWithLocation.length}:${pathHistory.version}:${connector.currentSf}:${connector.currentBwHz}:${connector.currentTxPower}:${settings.mapShowGuessedLocations}'; + '$filteredKeys|$anchorKeys|${pathHistory.version}:${connector.currentSf}:${connector.currentBwHz}:${connector.currentTxPower}:${settings.mapShowGuessedLocations}'; if (cacheKey != _guessedLocationsCacheKey) { _guessedLocationsCacheKey = cacheKey; _cachedGuessedLocations = settings.mapShowGuessedLocations diff --git a/lib/services/path_history_service.dart b/lib/services/path_history_service.dart index 3da87cc..569fada 100644 --- a/lib/services/path_history_service.dart +++ b/lib/services/path_history_service.dart @@ -281,6 +281,7 @@ class PathHistoryService extends ChangeNotifier { _autoRotationIndex.remove(contactPubKeyHex); _floodStats.remove(contactPubKeyHex); await _storage.clearPathHistory(contactPubKeyHex); + _version++; notifyListeners(); } @@ -300,6 +301,7 @@ class PathHistoryService extends ChangeNotifier { ); await _storage.savePathHistory(contactPubKeyHex, _cache[contactPubKeyHex]!); + _version++; notifyListeners(); } From 81548fdc21a6cb6ccb8a53d5044d64ae2d2c3013 Mon Sep 17 00:00:00 2001 From: zach Date: Fri, 6 Mar 2026 15:18:48 -0700 Subject: [PATCH 4/4] ai fixes --- lib/screens/map_screen.dart | 7 +++++-- lib/screens/path_trace_map.dart | 6 +++++- 2 files changed, 10 insertions(+), 3 deletions(-) diff --git a/lib/screens/map_screen.dart b/lib/screens/map_screen.dart index 0956b96..3d94701 100644 --- a/lib/screens/map_screen.dart +++ b/lib/screens/map_screen.dart @@ -172,10 +172,13 @@ class _MapScreenState extends State { // Compute guessed locations with caching final maxRangeKm = _estimateLoRaRangeKm(connector); final filteredKeys = filteredByKeyPrefix - .map((c) => c.publicKeyHex) + .map((c) => '${c.publicKeyHex}:${c.path.join("-")}') .join(','); final anchorKeys = allContactsWithLocation - .map((c) => c.publicKeyHex) + .map( + (c) => + '${c.publicKeyHex}:${c.latitude}:${c.longitude}:${c.path.isNotEmpty ? c.path.last : ""}', + ) .join(','); final cacheKey = '$filteredKeys|$anchorKeys|${pathHistory.version}:${connector.currentSf}:${connector.currentBwHz}:${connector.currentTxPower}:${settings.mapShowGuessedLocations}'; diff --git a/lib/screens/path_trace_map.dart b/lib/screens/path_trace_map.dart index df5b19a..c6d800e 100644 --- a/lib/screens/path_trace_map.dart +++ b/lib/screens/path_trace_map.dart @@ -289,7 +289,11 @@ class _PathTraceMapScreenState extends State { targetPos = LatLng(target.latitude!, target.longitude!); } else if (pathData.isNotEmpty) { // Infer from the last hop: average GPS contacts sharing that hop. - final lastHop = pathData.last; + // For a round-trip path (flipPathRound), the target-side hop sits + // in the middle of the symmetric sequence; .last is the local side. + final lastHop = (widget.flipPathRound && pathData.length > 1) + ? pathData[(pathData.length - 1) ~/ 2] + : pathData.last; final peers = connector.contacts .where( (c) =>