diff --git a/lib/l10n/app_bg.arb b/lib/l10n/app_bg.arb index e9f46c6..b94b2cb 100644 --- a/lib/l10n/app_bg.arb +++ b/lib/l10n/app_bg.arb @@ -1718,5 +1718,29 @@ "losPointName": "Име на точката", "losShowPanelTooltip": "Показване на LOS панел", "losHidePanelTooltip": "Скриване на LOS панела", - "losElevationAttribution": "Данни за надморска височина: Open-Meteo (CC BY 4.0)" -} \ No newline at end of file + "losElevationAttribution": "Данни за надморска височина: Open-Meteo (CC BY 4.0)", + "losLegendRadioHorizon": "Радиохоризонт", + "losLegendLosBeam": "Линия на видимост", + "losLegendTerrain": "Терен", + "losFrequencyLabel": "Честота", + "losFrequencyInfoTooltip": "Преглед на детайли за изчислението", + "losFrequencyDialogTitle": "Изчисляване на радиохоризонта", + "losFrequencyDialogDescription": "Започвайки от k={baselineK} при {baselineFreq} MHz, изчислението коригира k-фактора за текущата {frequencyMHz} MHz лента, която определя границата на извития радиохоризонт.", + "@losFrequencyDialogDescription": { + "description": "Explain how the calculation uses the baseline frequency and derived k-factor.", + "placeholders": { + "baselineK": { + "type": "double" + }, + "baselineFreq": { + "type": "double" + }, + "frequencyMHz": { + "type": "double" + }, + "kFactor": { + "type": "double" + } + } + } +} diff --git a/lib/l10n/app_de.arb b/lib/l10n/app_de.arb index bdea574..3963e31 100644 --- a/lib/l10n/app_de.arb +++ b/lib/l10n/app_de.arb @@ -1746,5 +1746,29 @@ "losPointName": "Punktname", "losShowPanelTooltip": "LOS-Panel anzeigen", "losHidePanelTooltip": "LOS-Panel ausblenden", - "losElevationAttribution": "Höhendaten: Open-Meteo (CC BY 4.0)" -} \ No newline at end of file + "losElevationAttribution": "Höhendaten: Open-Meteo (CC BY 4.0)", + "losLegendRadioHorizon": "Funkhorizont", + "losLegendLosBeam": "Sichtlinie", + "losLegendTerrain": "Gelände", + "losFrequencyLabel": "Frequenz", + "losFrequencyInfoTooltip": "Details zur Berechnung anzeigen", + "losFrequencyDialogTitle": "Berechnung des Funkhorizonts", + "losFrequencyDialogDescription": "Ausgehend von k={baselineK} bei {baselineFreq} MHz passt die Berechnung den k-Faktor für das aktuelle {frequencyMHz} MHz-Band an, das die gekrümmte Funkhorizontobergrenze definiert.", + "@losFrequencyDialogDescription": { + "description": "Explain how the calculation uses the baseline frequency and derived k-factor.", + "placeholders": { + "baselineK": { + "type": "double" + }, + "baselineFreq": { + "type": "double" + }, + "frequencyMHz": { + "type": "double" + }, + "kFactor": { + "type": "double" + } + } + } +} diff --git a/lib/l10n/app_en.arb b/lib/l10n/app_en.arb index 0e96e46..8f231e7 100644 --- a/lib/l10n/app_en.arb +++ b/lib/l10n/app_en.arb @@ -1667,6 +1667,30 @@ "losShowPanelTooltip": "Show LOS panel", "losHidePanelTooltip": "Hide LOS panel", "losElevationAttribution": "Elevation data: Open-Meteo (CC BY 4.0)", + "losLegendRadioHorizon": "Radio horizon", + "losLegendLosBeam": "LOS beam", + "losLegendTerrain": "Terrain", + "losFrequencyLabel": "Frequency", + "losFrequencyInfoTooltip": "View calculation details", + "losFrequencyDialogTitle": "Radio horizon calculation", + "losFrequencyDialogDescription": "Starting from k={baselineK} at {baselineFreq} MHz, the calculation adjusts the k-factor for the current {frequencyMHz} MHz band, which defines the curved radio horizon cap.", + "@losFrequencyDialogDescription": { + "description": "Explain how the calculation uses the baseline frequency and derived k-factor.", + "placeholders": { + "baselineK": { + "type": "double" + }, + "baselineFreq": { + "type": "double" + }, + "frequencyMHz": { + "type": "double" + }, + "kFactor": { + "type": "double" + } + } + }, "contacts_pathTrace": "Path Trace", "contacts_ping": "Ping", "contacts_repeaterPathTrace": "Path trace to repeater", @@ -1747,4 +1771,4 @@ "settings_gpxExportShareSubject": "meshcore-open GPX map data export", "snrIndicator_nearByRepeaters": "Nearby Repeaters", "snrIndicator_lastSeen": "Last seen" -} +} \ No newline at end of file diff --git a/lib/l10n/app_es.arb b/lib/l10n/app_es.arb index 99db15d..d194093 100644 --- a/lib/l10n/app_es.arb +++ b/lib/l10n/app_es.arb @@ -1746,5 +1746,29 @@ "losPointName": "Nombre del punto", "losShowPanelTooltip": "Mostrar panel LOS", "losHidePanelTooltip": "Ocultar panel LOS", - "losElevationAttribution": "Datos de elevación: Open-Meteo (CC BY 4.0)" -} \ No newline at end of file + "losElevationAttribution": "Datos de elevación: Open-Meteo (CC BY 4.0)", + "losLegendRadioHorizon": "Horizonte radioeléctrico", + "losLegendLosBeam": "Línea de visión", + "losLegendTerrain": "Terreno", + "losFrequencyLabel": "Frecuencia", + "losFrequencyInfoTooltip": "Ver detalles del cálculo", + "losFrequencyDialogTitle": "Cálculo del horizonte radioeléctrico", + "losFrequencyDialogDescription": "A partir de k={baselineK} en {baselineFreq} MHz, el cálculo ajusta el factor k para la banda actual de {frequencyMHz} MHz, que define el límite curvo del horizonte de radio.", + "@losFrequencyDialogDescription": { + "description": "Explain how the calculation uses the baseline frequency and derived k-factor.", + "placeholders": { + "baselineK": { + "type": "double" + }, + "baselineFreq": { + "type": "double" + }, + "frequencyMHz": { + "type": "double" + }, + "kFactor": { + "type": "double" + } + } + } +} diff --git a/lib/l10n/app_fr.arb b/lib/l10n/app_fr.arb index bc82195..f3e9ea8 100644 --- a/lib/l10n/app_fr.arb +++ b/lib/l10n/app_fr.arb @@ -1718,5 +1718,29 @@ "losPointName": "Nom du point", "losShowPanelTooltip": "Afficher le panneau LOS", "losHidePanelTooltip": "Masquer le panneau LOS", - "losElevationAttribution": "Données d'altitude : Open-Meteo (CC BY 4.0)" -} \ No newline at end of file + "losElevationAttribution": "Données d’altitude : Open-Meteo (CC BY 4.0)", + "losLegendRadioHorizon": "Horizon radio", + "losLegendLosBeam": "Ligne de visée", + "losLegendTerrain": "Terrain", + "losFrequencyLabel": "Fréquence", + "losFrequencyInfoTooltip": "Voir les détails du calcul", + "losFrequencyDialogTitle": "Calcul de l’horizon radio", + "losFrequencyDialogDescription": "À partir de k={baselineK} à {baselineFreq} MHz, le calcul ajuste le facteur k pour la bande actuelle de {frequencyMHz} MHz, ce qui définit la limite incurvée de l'horizon radio.", + "@losFrequencyDialogDescription": { + "description": "Explain how the calculation uses the baseline frequency and derived k-factor.", + "placeholders": { + "baselineK": { + "type": "double" + }, + "baselineFreq": { + "type": "double" + }, + "frequencyMHz": { + "type": "double" + }, + "kFactor": { + "type": "double" + } + } + } +} diff --git a/lib/l10n/app_it.arb b/lib/l10n/app_it.arb index fe4bffc..8b095f5 100644 --- a/lib/l10n/app_it.arb +++ b/lib/l10n/app_it.arb @@ -1718,5 +1718,29 @@ "losPointName": "Nome del punto", "losShowPanelTooltip": "Mostra il pannello LOS", "losHidePanelTooltip": "Nascondi il pannello LOS", - "losElevationAttribution": "Dati di elevazione: Open-Meteo (CC BY 4.0)" -} \ No newline at end of file + "losElevationAttribution": "Dati di elevazione: Open-Meteo (CC BY 4.0)", + "losLegendRadioHorizon": "Orizzonte radio", + "losLegendLosBeam": "Linea di vista", + "losLegendTerrain": "Terreno", + "losFrequencyLabel": "Frequenza", + "losFrequencyInfoTooltip": "Visualizza i dettagli del calcolo", + "losFrequencyDialogTitle": "Calcolo dell’orizzonte radio", + "losFrequencyDialogDescription": "Partendo da k={baselineK} a {baselineFreq} MHz, il calcolo regola il fattore k per l'attuale banda {frequencyMHz} MHz, che definisce il limite curvo dell'orizzonte radio.", + "@losFrequencyDialogDescription": { + "description": "Explain how the calculation uses the baseline frequency and derived k-factor.", + "placeholders": { + "baselineK": { + "type": "double" + }, + "baselineFreq": { + "type": "double" + }, + "frequencyMHz": { + "type": "double" + }, + "kFactor": { + "type": "double" + } + } + } +} diff --git a/lib/l10n/app_localizations.dart b/lib/l10n/app_localizations.dart index 6097f86..5f0cd5e 100644 --- a/lib/l10n/app_localizations.dart +++ b/lib/l10n/app_localizations.dart @@ -5004,6 +5004,53 @@ abstract class AppLocalizations { /// **'Elevation data: Open-Meteo (CC BY 4.0)'** String get losElevationAttribution; + /// No description provided for @losLegendRadioHorizon. + /// + /// In en, this message translates to: + /// **'Radio horizon'** + String get losLegendRadioHorizon; + + /// No description provided for @losLegendLosBeam. + /// + /// In en, this message translates to: + /// **'LOS beam'** + String get losLegendLosBeam; + + /// No description provided for @losLegendTerrain. + /// + /// In en, this message translates to: + /// **'Terrain'** + String get losLegendTerrain; + + /// No description provided for @losFrequencyLabel. + /// + /// In en, this message translates to: + /// **'Frequency'** + String get losFrequencyLabel; + + /// No description provided for @losFrequencyInfoTooltip. + /// + /// In en, this message translates to: + /// **'View calculation details'** + String get losFrequencyInfoTooltip; + + /// No description provided for @losFrequencyDialogTitle. + /// + /// In en, this message translates to: + /// **'Radio horizon calculation'** + String get losFrequencyDialogTitle; + + /// Explain how the calculation uses the baseline frequency and derived k-factor. + /// + /// In en, this message translates to: + /// **'Starting from k={baselineK} at {baselineFreq} MHz, the calculation adjusts the k-factor for the current {frequencyMHz} MHz band, which defines the curved radio horizon cap.'** + String losFrequencyDialogDescription( + double baselineK, + double baselineFreq, + double frequencyMHz, + double kFactor, + ); + /// No description provided for @contacts_pathTrace. /// /// In en, this message translates to: diff --git a/lib/l10n/app_localizations_bg.dart b/lib/l10n/app_localizations_bg.dart index 94f9f7a..91e5a94 100644 --- a/lib/l10n/app_localizations_bg.dart +++ b/lib/l10n/app_localizations_bg.dart @@ -2867,6 +2867,34 @@ class AppLocalizationsBg extends AppLocalizations { String get losElevationAttribution => 'Данни за надморска височина: Open-Meteo (CC BY 4.0)'; + @override + String get losLegendRadioHorizon => 'Радиохоризонт'; + + @override + String get losLegendLosBeam => 'Линия на видимост'; + + @override + String get losLegendTerrain => 'Терен'; + + @override + String get losFrequencyLabel => 'Честота'; + + @override + String get losFrequencyInfoTooltip => 'Преглед на детайли за изчислението'; + + @override + String get losFrequencyDialogTitle => 'Изчисляване на радиохоризонта'; + + @override + String losFrequencyDialogDescription( + double baselineK, + double baselineFreq, + double frequencyMHz, + double kFactor, + ) { + return 'Започвайки от k=$baselineK при $baselineFreq MHz, изчислението коригира k-фактора за текущата $frequencyMHz MHz лента, която определя границата на извития радиохоризонт.'; + } + @override String get contacts_pathTrace => 'Пътен проследяване'; diff --git a/lib/l10n/app_localizations_de.dart b/lib/l10n/app_localizations_de.dart index ba0f5da..4c591e5 100644 --- a/lib/l10n/app_localizations_de.dart +++ b/lib/l10n/app_localizations_de.dart @@ -2873,6 +2873,34 @@ class AppLocalizationsDe extends AppLocalizations { @override String get losElevationAttribution => 'Höhendaten: Open-Meteo (CC BY 4.0)'; + @override + String get losLegendRadioHorizon => 'Funkhorizont'; + + @override + String get losLegendLosBeam => 'Sichtlinie'; + + @override + String get losLegendTerrain => 'Gelände'; + + @override + String get losFrequencyLabel => 'Frequenz'; + + @override + String get losFrequencyInfoTooltip => 'Details zur Berechnung anzeigen'; + + @override + String get losFrequencyDialogTitle => 'Berechnung des Funkhorizonts'; + + @override + String losFrequencyDialogDescription( + double baselineK, + double baselineFreq, + double frequencyMHz, + double kFactor, + ) { + return 'Ausgehend von k=$baselineK bei $baselineFreq MHz passt die Berechnung den k-Faktor für das aktuelle $frequencyMHz MHz-Band an, das die gekrümmte Funkhorizontobergrenze definiert.'; + } + @override String get contacts_pathTrace => 'Pfadverfolgung'; diff --git a/lib/l10n/app_localizations_en.dart b/lib/l10n/app_localizations_en.dart index ce5b2f0..98fee85 100644 --- a/lib/l10n/app_localizations_en.dart +++ b/lib/l10n/app_localizations_en.dart @@ -2824,6 +2824,34 @@ class AppLocalizationsEn extends AppLocalizations { String get losElevationAttribution => 'Elevation data: Open-Meteo (CC BY 4.0)'; + @override + String get losLegendRadioHorizon => 'Radio horizon'; + + @override + String get losLegendLosBeam => 'LOS beam'; + + @override + String get losLegendTerrain => 'Terrain'; + + @override + String get losFrequencyLabel => 'Frequency'; + + @override + String get losFrequencyInfoTooltip => 'View calculation details'; + + @override + String get losFrequencyDialogTitle => 'Radio horizon calculation'; + + @override + String losFrequencyDialogDescription( + double baselineK, + double baselineFreq, + double frequencyMHz, + double kFactor, + ) { + return 'Starting from k=$baselineK at $baselineFreq MHz, the calculation adjusts the k-factor for the current $frequencyMHz MHz band, which defines the curved radio horizon cap.'; + } + @override String get contacts_pathTrace => 'Path Trace'; diff --git a/lib/l10n/app_localizations_es.dart b/lib/l10n/app_localizations_es.dart index 3c7838d..b868aad 100644 --- a/lib/l10n/app_localizations_es.dart +++ b/lib/l10n/app_localizations_es.dart @@ -2867,6 +2867,34 @@ class AppLocalizationsEs extends AppLocalizations { String get losElevationAttribution => 'Datos de elevación: Open-Meteo (CC BY 4.0)'; + @override + String get losLegendRadioHorizon => 'Horizonte radioeléctrico'; + + @override + String get losLegendLosBeam => 'Línea de visión'; + + @override + String get losLegendTerrain => 'Terreno'; + + @override + String get losFrequencyLabel => 'Frecuencia'; + + @override + String get losFrequencyInfoTooltip => 'Ver detalles del cálculo'; + + @override + String get losFrequencyDialogTitle => 'Cálculo del horizonte radioeléctrico'; + + @override + String losFrequencyDialogDescription( + double baselineK, + double baselineFreq, + double frequencyMHz, + double kFactor, + ) { + return 'A partir de k=$baselineK en $baselineFreq MHz, el cálculo ajusta el factor k para la banda actual de $frequencyMHz MHz, que define el límite curvo del horizonte de radio.'; + } + @override String get contacts_pathTrace => 'Rastreo de caminos'; diff --git a/lib/l10n/app_localizations_fr.dart b/lib/l10n/app_localizations_fr.dart index ee58fb4..a939e09 100644 --- a/lib/l10n/app_localizations_fr.dart +++ b/lib/l10n/app_localizations_fr.dart @@ -2880,7 +2880,35 @@ class AppLocalizationsFr extends AppLocalizations { @override String get losElevationAttribution => - 'Données d\'altitude : Open-Meteo (CC BY 4.0)'; + 'Données d’altitude : Open-Meteo (CC BY 4.0)'; + + @override + String get losLegendRadioHorizon => 'Horizon radio'; + + @override + String get losLegendLosBeam => 'Ligne de visée'; + + @override + String get losLegendTerrain => 'Terrain'; + + @override + String get losFrequencyLabel => 'Fréquence'; + + @override + String get losFrequencyInfoTooltip => 'Voir les détails du calcul'; + + @override + String get losFrequencyDialogTitle => 'Calcul de l’horizon radio'; + + @override + String losFrequencyDialogDescription( + double baselineK, + double baselineFreq, + double frequencyMHz, + double kFactor, + ) { + return 'À partir de k=$baselineK à $baselineFreq MHz, le calcul ajuste le facteur k pour la bande actuelle de $frequencyMHz MHz, ce qui définit la limite incurvée de l\'horizon radio.'; + } @override String get contacts_pathTrace => 'Traçage de chemin'; diff --git a/lib/l10n/app_localizations_it.dart b/lib/l10n/app_localizations_it.dart index b3c41e7..d4cda69 100644 --- a/lib/l10n/app_localizations_it.dart +++ b/lib/l10n/app_localizations_it.dart @@ -2867,6 +2867,34 @@ class AppLocalizationsIt extends AppLocalizations { String get losElevationAttribution => 'Dati di elevazione: Open-Meteo (CC BY 4.0)'; + @override + String get losLegendRadioHorizon => 'Orizzonte radio'; + + @override + String get losLegendLosBeam => 'Linea di vista'; + + @override + String get losLegendTerrain => 'Terreno'; + + @override + String get losFrequencyLabel => 'Frequenza'; + + @override + String get losFrequencyInfoTooltip => 'Visualizza i dettagli del calcolo'; + + @override + String get losFrequencyDialogTitle => 'Calcolo dell’orizzonte radio'; + + @override + String losFrequencyDialogDescription( + double baselineK, + double baselineFreq, + double frequencyMHz, + double kFactor, + ) { + return 'Partendo da k=$baselineK a $baselineFreq MHz, il calcolo regola il fattore k per l\'attuale banda $frequencyMHz MHz, che definisce il limite curvo dell\'orizzonte radio.'; + } + @override String get contacts_pathTrace => 'Traccia Percorso'; diff --git a/lib/l10n/app_localizations_nl.dart b/lib/l10n/app_localizations_nl.dart index 0ff00ad..5c38227 100644 --- a/lib/l10n/app_localizations_nl.dart +++ b/lib/l10n/app_localizations_nl.dart @@ -2857,6 +2857,34 @@ class AppLocalizationsNl extends AppLocalizations { String get losElevationAttribution => 'Hoogtegegevens: Open-Meteo (CC BY 4.0)'; + @override + String get losLegendRadioHorizon => 'Radiohorizon'; + + @override + String get losLegendLosBeam => 'Zichtlijn'; + + @override + String get losLegendTerrain => 'Terrein'; + + @override + String get losFrequencyLabel => 'Frequentie'; + + @override + String get losFrequencyInfoTooltip => 'Bekijk details van de berekening'; + + @override + String get losFrequencyDialogTitle => 'Berekening van de radiohorizon'; + + @override + String losFrequencyDialogDescription( + double baselineK, + double baselineFreq, + double frequencyMHz, + double kFactor, + ) { + return 'Beginnend met k=$baselineK bij $baselineFreq MHz, wordt bij de berekening de k-factor aangepast voor de huidige $frequencyMHz MHz-band, die de gebogen radiohorizonkap definieert.'; + } + @override String get contacts_pathTrace => 'Pad Traceren'; diff --git a/lib/l10n/app_localizations_pl.dart b/lib/l10n/app_localizations_pl.dart index d6c1e15..6f68786 100644 --- a/lib/l10n/app_localizations_pl.dart +++ b/lib/l10n/app_localizations_pl.dart @@ -2863,6 +2863,34 @@ class AppLocalizationsPl extends AppLocalizations { String get losElevationAttribution => 'Dane dotyczące wysokości: Open-Meteo (CC BY 4.0)'; + @override + String get losLegendRadioHorizon => 'Horyzont radiowy'; + + @override + String get losLegendLosBeam => 'Linia widoczności'; + + @override + String get losLegendTerrain => 'Teren'; + + @override + String get losFrequencyLabel => 'Częstotliwość'; + + @override + String get losFrequencyInfoTooltip => 'Zobacz szczegóły obliczenia'; + + @override + String get losFrequencyDialogTitle => 'Obliczanie horyzontu radiowego'; + + @override + String losFrequencyDialogDescription( + double baselineK, + double baselineFreq, + double frequencyMHz, + double kFactor, + ) { + return 'Zaczynając od k=$baselineK przy $baselineFreq MHz, obliczenia korygują współczynnik k dla bieżącego pasma $frequencyMHz MHz, które definiuje zakrzywiony limit horyzontu radiowego.'; + } + @override String get contacts_pathTrace => 'Śledzenie Ścieżek'; diff --git a/lib/l10n/app_localizations_pt.dart b/lib/l10n/app_localizations_pt.dart index a300ba9..cedf400 100644 --- a/lib/l10n/app_localizations_pt.dart +++ b/lib/l10n/app_localizations_pt.dart @@ -2866,6 +2866,34 @@ class AppLocalizationsPt extends AppLocalizations { String get losElevationAttribution => 'Dados de elevação: Open-Meteo (CC BY 4.0)'; + @override + String get losLegendRadioHorizon => 'Horizonte de rádio'; + + @override + String get losLegendLosBeam => 'Linha de visada'; + + @override + String get losLegendTerrain => 'Terreno'; + + @override + String get losFrequencyLabel => 'Frequência'; + + @override + String get losFrequencyInfoTooltip => 'Ver detalhes do cálculo'; + + @override + String get losFrequencyDialogTitle => 'Cálculo do horizonte de rádio'; + + @override + String losFrequencyDialogDescription( + double baselineK, + double baselineFreq, + double frequencyMHz, + double kFactor, + ) { + return 'Começando em k=$baselineK em $baselineFreq MHz, o cálculo ajusta o fator k para a banda atual de $frequencyMHz MHz, que define o limite do horizonte de rádio curvo.'; + } + @override String get contacts_pathTrace => 'Traçado de Caminho'; diff --git a/lib/l10n/app_localizations_ru.dart b/lib/l10n/app_localizations_ru.dart index c4c1633..d35b174 100644 --- a/lib/l10n/app_localizations_ru.dart +++ b/lib/l10n/app_localizations_ru.dart @@ -2869,6 +2869,34 @@ class AppLocalizationsRu extends AppLocalizations { String get losElevationAttribution => 'Данные о высоте: Open-Meteo (CC BY 4.0)'; + @override + String get losLegendRadioHorizon => 'Радиогоризонт'; + + @override + String get losLegendLosBeam => 'Линия прямой видимости'; + + @override + String get losLegendTerrain => 'Рельеф'; + + @override + String get losFrequencyLabel => 'Частота'; + + @override + String get losFrequencyInfoTooltip => 'Просмотреть детали расчёта'; + + @override + String get losFrequencyDialogTitle => 'Расчёт радиогоризонта'; + + @override + String losFrequencyDialogDescription( + double baselineK, + double baselineFreq, + double frequencyMHz, + double kFactor, + ) { + return 'Начиная с k=$baselineK на частоте $baselineFreq МГц, расчет корректирует коэффициент k для текущего диапазона $frequencyMHz МГц, который определяет изогнутую границу радиогоризонта.'; + } + @override String get contacts_pathTrace => 'Трассировка пути'; diff --git a/lib/l10n/app_localizations_sk.dart b/lib/l10n/app_localizations_sk.dart index 0df70a6..c4f9a92 100644 --- a/lib/l10n/app_localizations_sk.dart +++ b/lib/l10n/app_localizations_sk.dart @@ -2851,6 +2851,34 @@ class AppLocalizationsSk extends AppLocalizations { String get losElevationAttribution => 'Údaje o nadmorskej výške: Open-Meteo (CC BY 4.0)'; + @override + String get losLegendRadioHorizon => 'Rádiový horizont'; + + @override + String get losLegendLosBeam => 'Priama viditeľnosť'; + + @override + String get losLegendTerrain => 'Terén'; + + @override + String get losFrequencyLabel => 'Frekvencia'; + + @override + String get losFrequencyInfoTooltip => 'Zobraziť podrobnosti výpočtu'; + + @override + String get losFrequencyDialogTitle => 'Výpočet rádiového horizontu'; + + @override + String losFrequencyDialogDescription( + double baselineK, + double baselineFreq, + double frequencyMHz, + double kFactor, + ) { + return 'Počnúc od k=$baselineK pri $baselineFreq MHz výpočet upraví k-faktor pre aktuálne pásmo $frequencyMHz MHz, ktorý definuje zakrivený strop rádiového horizontu.'; + } + @override String get contacts_pathTrace => 'Sledovanie lúčov'; diff --git a/lib/l10n/app_localizations_sl.dart b/lib/l10n/app_localizations_sl.dart index 4be105e..a012ef1 100644 --- a/lib/l10n/app_localizations_sl.dart +++ b/lib/l10n/app_localizations_sl.dart @@ -2854,6 +2854,34 @@ class AppLocalizationsSl extends AppLocalizations { String get losElevationAttribution => 'Podatki o višini: Open-Meteo (CC BY 4.0)'; + @override + String get losLegendRadioHorizon => 'Radijski horizont'; + + @override + String get losLegendLosBeam => 'Linija vidnosti'; + + @override + String get losLegendTerrain => 'Teren'; + + @override + String get losFrequencyLabel => 'Frekvenca'; + + @override + String get losFrequencyInfoTooltip => 'Prikaži podrobnosti izračuna'; + + @override + String get losFrequencyDialogTitle => 'Izračun radijskega horizonta'; + + @override + String losFrequencyDialogDescription( + double baselineK, + double baselineFreq, + double frequencyMHz, + double kFactor, + ) { + return 'Začenši od k=$baselineK pri $baselineFreq MHz, izračun prilagodi k-faktor za trenutni pas $frequencyMHz MHz, ki določa ukrivljeno zgornjo mejo radijskega horizonta.'; + } + @override String get contacts_pathTrace => 'Sledenje poti'; diff --git a/lib/l10n/app_localizations_sv.dart b/lib/l10n/app_localizations_sv.dart index 52fa531..abfd89c 100644 --- a/lib/l10n/app_localizations_sv.dart +++ b/lib/l10n/app_localizations_sv.dart @@ -2837,6 +2837,34 @@ class AppLocalizationsSv extends AppLocalizations { @override String get losElevationAttribution => 'Höjddata: Open-Meteo (CC BY 4.0)'; + @override + String get losLegendRadioHorizon => 'Radiohorisont'; + + @override + String get losLegendLosBeam => 'Siktlinje'; + + @override + String get losLegendTerrain => 'Terräng'; + + @override + String get losFrequencyLabel => 'Frekvens'; + + @override + String get losFrequencyInfoTooltip => 'Visa detaljer om beräkningen'; + + @override + String get losFrequencyDialogTitle => 'Beräkning av radiohorisonten'; + + @override + String losFrequencyDialogDescription( + double baselineK, + double baselineFreq, + double frequencyMHz, + double kFactor, + ) { + return 'Med start från k=$baselineK vid $baselineFreq MHz, justerar beräkningen k-faktorn för det aktuella $frequencyMHz MHz-bandet, som definierar den böjda radiohorisonten.'; + } + @override String get contacts_pathTrace => 'Path Trace'; diff --git a/lib/l10n/app_localizations_uk.dart b/lib/l10n/app_localizations_uk.dart index 4847009..c6b8ff2 100644 --- a/lib/l10n/app_localizations_uk.dart +++ b/lib/l10n/app_localizations_uk.dart @@ -2877,6 +2877,34 @@ class AppLocalizationsUk extends AppLocalizations { String get losElevationAttribution => 'Дані про висоту: Open-Meteo (CC BY 4.0)'; + @override + String get losLegendRadioHorizon => 'Радіогоризонт'; + + @override + String get losLegendLosBeam => 'Лінія прямої видимості'; + + @override + String get losLegendTerrain => 'Рельєф'; + + @override + String get losFrequencyLabel => 'Частота'; + + @override + String get losFrequencyInfoTooltip => 'Переглянути деталі розрахунку'; + + @override + String get losFrequencyDialogTitle => 'Розрахунок радіогоризонту'; + + @override + String losFrequencyDialogDescription( + double baselineK, + double baselineFreq, + double frequencyMHz, + double kFactor, + ) { + return 'Починаючи з k=$baselineK на $baselineFreq МГц, обчислення коригує k-фактор для поточного діапазону $frequencyMHz МГц, який визначає викривлену межу радіогоризонту.'; + } + @override String get contacts_pathTrace => 'Трасування шляхів'; diff --git a/lib/l10n/app_localizations_zh.dart b/lib/l10n/app_localizations_zh.dart index 454f127..5677c09 100644 --- a/lib/l10n/app_localizations_zh.dart +++ b/lib/l10n/app_localizations_zh.dart @@ -2715,6 +2715,34 @@ class AppLocalizationsZh extends AppLocalizations { @override String get losElevationAttribution => '高程数据:Open-Meteo (CC BY 4.0)'; + @override + String get losLegendRadioHorizon => '无线电地平线'; + + @override + String get losLegendLosBeam => '视距波束'; + + @override + String get losLegendTerrain => '地形'; + + @override + String get losFrequencyLabel => '频率'; + + @override + String get losFrequencyInfoTooltip => '查看计算详情'; + + @override + String get losFrequencyDialogTitle => '无线电地平线计算'; + + @override + String losFrequencyDialogDescription( + double baselineK, + double baselineFreq, + double frequencyMHz, + double kFactor, + ) { + return '从 $baselineFreq MHz 处的 k=$baselineK 开始,计算调整当前 $frequencyMHz MHz 频段的 k 因子,该因子定义了弯曲的无线电范围上限。'; + } + @override String get contacts_pathTrace => '路径追踪'; diff --git a/lib/l10n/app_nl.arb b/lib/l10n/app_nl.arb index 2f39fdf..a94560b 100644 --- a/lib/l10n/app_nl.arb +++ b/lib/l10n/app_nl.arb @@ -1718,5 +1718,29 @@ "losPointName": "Puntnaam", "losShowPanelTooltip": "Toon LOS-paneel", "losHidePanelTooltip": "LOS-paneel verbergen", - "losElevationAttribution": "Hoogtegegevens: Open-Meteo (CC BY 4.0)" -} \ No newline at end of file + "losElevationAttribution": "Hoogtegegevens: Open-Meteo (CC BY 4.0)", + "losLegendRadioHorizon": "Radiohorizon", + "losLegendLosBeam": "Zichtlijn", + "losLegendTerrain": "Terrein", + "losFrequencyLabel": "Frequentie", + "losFrequencyInfoTooltip": "Bekijk details van de berekening", + "losFrequencyDialogTitle": "Berekening van de radiohorizon", + "losFrequencyDialogDescription": "Beginnend met k={baselineK} bij {baselineFreq} MHz, wordt bij de berekening de k-factor aangepast voor de huidige {frequencyMHz} MHz-band, die de gebogen radiohorizonkap definieert.", + "@losFrequencyDialogDescription": { + "description": "Explain how the calculation uses the baseline frequency and derived k-factor.", + "placeholders": { + "baselineK": { + "type": "double" + }, + "baselineFreq": { + "type": "double" + }, + "frequencyMHz": { + "type": "double" + }, + "kFactor": { + "type": "double" + } + } + } +} diff --git a/lib/l10n/app_pl.arb b/lib/l10n/app_pl.arb index 0432f8f..2af1058 100644 --- a/lib/l10n/app_pl.arb +++ b/lib/l10n/app_pl.arb @@ -1718,5 +1718,29 @@ "losPointName": "Nazwa punktu", "losShowPanelTooltip": "Pokaż panel LOS", "losHidePanelTooltip": "Ukryj panel LOS", - "losElevationAttribution": "Dane dotyczące wysokości: Open-Meteo (CC BY 4.0)" -} \ No newline at end of file + "losElevationAttribution": "Dane dotyczące wysokości: Open-Meteo (CC BY 4.0)", + "losLegendRadioHorizon": "Horyzont radiowy", + "losLegendLosBeam": "Linia widoczności", + "losLegendTerrain": "Teren", + "losFrequencyLabel": "Częstotliwość", + "losFrequencyInfoTooltip": "Zobacz szczegóły obliczenia", + "losFrequencyDialogTitle": "Obliczanie horyzontu radiowego", + "losFrequencyDialogDescription": "Zaczynając od k={baselineK} przy {baselineFreq} MHz, obliczenia korygują współczynnik k dla bieżącego pasma {frequencyMHz} MHz, które definiuje zakrzywiony limit horyzontu radiowego.", + "@losFrequencyDialogDescription": { + "description": "Explain how the calculation uses the baseline frequency and derived k-factor.", + "placeholders": { + "baselineK": { + "type": "double" + }, + "baselineFreq": { + "type": "double" + }, + "frequencyMHz": { + "type": "double" + }, + "kFactor": { + "type": "double" + } + } + } +} diff --git a/lib/l10n/app_pt.arb b/lib/l10n/app_pt.arb index 01c5a83..c9d3724 100644 --- a/lib/l10n/app_pt.arb +++ b/lib/l10n/app_pt.arb @@ -1718,5 +1718,29 @@ "losPointName": "Nome do ponto", "losShowPanelTooltip": "Mostrar painel LOS", "losHidePanelTooltip": "Ocultar painel LOS", - "losElevationAttribution": "Dados de elevação: Open-Meteo (CC BY 4.0)" -} \ No newline at end of file + "losElevationAttribution": "Dados de elevação: Open-Meteo (CC BY 4.0)", + "losLegendRadioHorizon": "Horizonte de rádio", + "losLegendLosBeam": "Linha de visada", + "losLegendTerrain": "Terreno", + "losFrequencyLabel": "Frequência", + "losFrequencyInfoTooltip": "Ver detalhes do cálculo", + "losFrequencyDialogTitle": "Cálculo do horizonte de rádio", + "losFrequencyDialogDescription": "Começando em k={baselineK} em {baselineFreq} MHz, o cálculo ajusta o fator k para a banda atual de {frequencyMHz} MHz, que define o limite do horizonte de rádio curvo.", + "@losFrequencyDialogDescription": { + "description": "Explain how the calculation uses the baseline frequency and derived k-factor.", + "placeholders": { + "baselineK": { + "type": "double" + }, + "baselineFreq": { + "type": "double" + }, + "frequencyMHz": { + "type": "double" + }, + "kFactor": { + "type": "double" + } + } + } +} diff --git a/lib/l10n/app_ru.arb b/lib/l10n/app_ru.arb index b8a20d9..e1a2066 100644 --- a/lib/l10n/app_ru.arb +++ b/lib/l10n/app_ru.arb @@ -958,5 +958,29 @@ "losPointName": "Имя точки", "losShowPanelTooltip": "Показать панель LOS", "losHidePanelTooltip": "Скрыть панель LOS", - "losElevationAttribution": "Данные о высоте: Open-Meteo (CC BY 4.0)" -} \ No newline at end of file + "losElevationAttribution": "Данные о высоте: Open-Meteo (CC BY 4.0)", + "losLegendRadioHorizon": "Радиогоризонт", + "losLegendLosBeam": "Линия прямой видимости", + "losLegendTerrain": "Рельеф", + "losFrequencyLabel": "Частота", + "losFrequencyInfoTooltip": "Просмотреть детали расчёта", + "losFrequencyDialogTitle": "Расчёт радиогоризонта", + "losFrequencyDialogDescription": "Начиная с k={baselineK} на частоте {baselineFreq} МГц, расчет корректирует коэффициент k для текущего диапазона {frequencyMHz} МГц, который определяет изогнутую границу радиогоризонта.", + "@losFrequencyDialogDescription": { + "description": "Explain how the calculation uses the baseline frequency and derived k-factor.", + "placeholders": { + "baselineK": { + "type": "double" + }, + "baselineFreq": { + "type": "double" + }, + "frequencyMHz": { + "type": "double" + }, + "kFactor": { + "type": "double" + } + } + } +} diff --git a/lib/l10n/app_sk.arb b/lib/l10n/app_sk.arb index 3245282..34e5933 100644 --- a/lib/l10n/app_sk.arb +++ b/lib/l10n/app_sk.arb @@ -1718,5 +1718,29 @@ "losPointName": "Názov bodu", "losShowPanelTooltip": "Zobraziť panel LOS", "losHidePanelTooltip": "Skryť panel LOS", - "losElevationAttribution": "Údaje o nadmorskej výške: Open-Meteo (CC BY 4.0)" -} \ No newline at end of file + "losElevationAttribution": "Údaje o nadmorskej výške: Open-Meteo (CC BY 4.0)", + "losLegendRadioHorizon": "Rádiový horizont", + "losLegendLosBeam": "Priama viditeľnosť", + "losLegendTerrain": "Terén", + "losFrequencyLabel": "Frekvencia", + "losFrequencyInfoTooltip": "Zobraziť podrobnosti výpočtu", + "losFrequencyDialogTitle": "Výpočet rádiového horizontu", + "losFrequencyDialogDescription": "Počnúc od k={baselineK} pri {baselineFreq} MHz výpočet upraví k-faktor pre aktuálne pásmo {frequencyMHz} MHz, ktorý definuje zakrivený strop rádiového horizontu.", + "@losFrequencyDialogDescription": { + "description": "Explain how the calculation uses the baseline frequency and derived k-factor.", + "placeholders": { + "baselineK": { + "type": "double" + }, + "baselineFreq": { + "type": "double" + }, + "frequencyMHz": { + "type": "double" + }, + "kFactor": { + "type": "double" + } + } + } +} diff --git a/lib/l10n/app_sl.arb b/lib/l10n/app_sl.arb index c560c31..1371f97 100644 --- a/lib/l10n/app_sl.arb +++ b/lib/l10n/app_sl.arb @@ -1718,5 +1718,29 @@ "losPointName": "Ime točke", "losShowPanelTooltip": "Pokaži ploščo LOS", "losHidePanelTooltip": "Skrij ploščo LOS", - "losElevationAttribution": "Podatki o višini: Open-Meteo (CC BY 4.0)" -} \ No newline at end of file + "losElevationAttribution": "Podatki o višini: Open-Meteo (CC BY 4.0)", + "losLegendRadioHorizon": "Radijski horizont", + "losLegendLosBeam": "Linija vidnosti", + "losLegendTerrain": "Teren", + "losFrequencyLabel": "Frekvenca", + "losFrequencyInfoTooltip": "Prikaži podrobnosti izračuna", + "losFrequencyDialogTitle": "Izračun radijskega horizonta", + "losFrequencyDialogDescription": "Začenši od k={baselineK} pri {baselineFreq} MHz, izračun prilagodi k-faktor za trenutni pas {frequencyMHz} MHz, ki določa ukrivljeno zgornjo mejo radijskega horizonta.", + "@losFrequencyDialogDescription": { + "description": "Explain how the calculation uses the baseline frequency and derived k-factor.", + "placeholders": { + "baselineK": { + "type": "double" + }, + "baselineFreq": { + "type": "double" + }, + "frequencyMHz": { + "type": "double" + }, + "kFactor": { + "type": "double" + } + } + } +} diff --git a/lib/l10n/app_sv.arb b/lib/l10n/app_sv.arb index b93c5ca..2bdaec4 100644 --- a/lib/l10n/app_sv.arb +++ b/lib/l10n/app_sv.arb @@ -1718,5 +1718,29 @@ "losPointName": "Punktnamn", "losShowPanelTooltip": "Visa LOS-panelen", "losHidePanelTooltip": "Dölj LOS-panelen", - "losElevationAttribution": "Höjddata: Open-Meteo (CC BY 4.0)" -} \ No newline at end of file + "losElevationAttribution": "Höjddata: Open-Meteo (CC BY 4.0)", + "losLegendRadioHorizon": "Radiohorisont", + "losLegendLosBeam": "Siktlinje", + "losLegendTerrain": "Terräng", + "losFrequencyLabel": "Frekvens", + "losFrequencyInfoTooltip": "Visa detaljer om beräkningen", + "losFrequencyDialogTitle": "Beräkning av radiohorisonten", + "losFrequencyDialogDescription": "Med start från k={baselineK} vid {baselineFreq} MHz, justerar beräkningen k-faktorn för det aktuella {frequencyMHz} MHz-bandet, som definierar den böjda radiohorisonten.", + "@losFrequencyDialogDescription": { + "description": "Explain how the calculation uses the baseline frequency and derived k-factor.", + "placeholders": { + "baselineK": { + "type": "double" + }, + "baselineFreq": { + "type": "double" + }, + "frequencyMHz": { + "type": "double" + }, + "kFactor": { + "type": "double" + } + } + } +} diff --git a/lib/l10n/app_uk.arb b/lib/l10n/app_uk.arb index 235e4ed..13d9362 100644 --- a/lib/l10n/app_uk.arb +++ b/lib/l10n/app_uk.arb @@ -1718,5 +1718,29 @@ "losPointName": "Назва точки", "losShowPanelTooltip": "Показати панель LOS", "losHidePanelTooltip": "Приховати панель LOS", - "losElevationAttribution": "Дані про висоту: Open-Meteo (CC BY 4.0)" -} \ No newline at end of file + "losElevationAttribution": "Дані про висоту: Open-Meteo (CC BY 4.0)", + "losLegendRadioHorizon": "Радіогоризонт", + "losLegendLosBeam": "Лінія прямої видимості", + "losLegendTerrain": "Рельєф", + "losFrequencyLabel": "Частота", + "losFrequencyInfoTooltip": "Переглянути деталі розрахунку", + "losFrequencyDialogTitle": "Розрахунок радіогоризонту", + "losFrequencyDialogDescription": "Починаючи з k={baselineK} на {baselineFreq} МГц, обчислення коригує k-фактор для поточного діапазону {frequencyMHz} МГц, який визначає викривлену межу радіогоризонту.", + "@losFrequencyDialogDescription": { + "description": "Explain how the calculation uses the baseline frequency and derived k-factor.", + "placeholders": { + "baselineK": { + "type": "double" + }, + "baselineFreq": { + "type": "double" + }, + "frequencyMHz": { + "type": "double" + }, + "kFactor": { + "type": "double" + } + } + } +} diff --git a/lib/l10n/app_zh.arb b/lib/l10n/app_zh.arb index 72f48ad..b2dc330 100644 --- a/lib/l10n/app_zh.arb +++ b/lib/l10n/app_zh.arb @@ -1718,5 +1718,29 @@ "losPointName": "点名称", "losShowPanelTooltip": "显示 LOS 面板", "losHidePanelTooltip": "隐藏 LOS 面板", - "losElevationAttribution": "高程数据:Open-Meteo (CC BY 4.0)" -} \ No newline at end of file + "losElevationAttribution": "高程数据:Open-Meteo (CC BY 4.0)", + "losLegendRadioHorizon": "无线电地平线", + "losLegendLosBeam": "视距波束", + "losLegendTerrain": "地形", + "losFrequencyLabel": "频率", + "losFrequencyInfoTooltip": "查看计算详情", + "losFrequencyDialogTitle": "无线电地平线计算", + "losFrequencyDialogDescription": "从 {baselineFreq} MHz 处的 k={baselineK} 开始,计算调整当前 {frequencyMHz} MHz 频段的 k 因子,该因子定义了弯曲的无线电范围上限。", + "@losFrequencyDialogDescription": { + "description": "Explain how the calculation uses the baseline frequency and derived k-factor.", + "placeholders": { + "baselineK": { + "type": "double" + }, + "baselineFreq": { + "type": "double" + }, + "frequencyMHz": { + "type": "double" + }, + "kFactor": { + "type": "double" + } + } + } +} diff --git a/lib/screens/line_of_sight_map_screen.dart b/lib/screens/line_of_sight_map_screen.dart index b073685..196cd2e 100644 --- a/lib/screens/line_of_sight_map_screen.dart +++ b/lib/screens/line_of_sight_map_screen.dart @@ -14,6 +14,7 @@ import '../services/app_settings_service.dart'; import '../services/line_of_sight_service.dart'; import '../services/map_tile_cache_service.dart'; import '../utils/route_transitions.dart'; +import '../connector/meshcore_connector.dart'; import '../widgets/app_bar.dart'; import '../widgets/quick_switch_bar.dart'; @@ -110,10 +111,13 @@ class _LineOfSightMapScreenState extends State { }); try { + final connector = context.watch(); + final frequencyMHz = _normalizeFrequencyMHz(connector.currentFreqHz); final result = await _lineOfSightService.analyzePath( [start.point, end.point], startAntennaHeightMeters: startAntenna, endAntennaHeightMeters: endAntenna, + frequencyMHz: frequencyMHz, ); if (!mounted) return; if (!_isRunRequestCurrent( @@ -424,6 +428,12 @@ class _LineOfSightMapScreenState extends State { Widget _buildControlPanel(bool isImperial) { _sanitizeSelection(); final segment = _primarySegmentResult(); + final connector = context.read(); + final reportedFrequencyMHz = _normalizeFrequencyMHz( + connector.currentFreqHz, + ); + final displayFrequencyMHz = segment?.frequencyMHz ?? reportedFrequencyMHz; + final kFactorUsed = segment?.usedKFactor; final endpoints = _visibleEndpoints(); final distanceUnit = isImperial ? 'mi' : 'km'; final heightUnit = isImperial ? 'ft' : 'm'; @@ -461,6 +471,9 @@ class _LineOfSightMapScreenState extends State { fontSize: 10, fontWeight: FontWeight.w600, ), + terrainLabel: context.l10n.losLegendTerrain, + losBeamLabel: context.l10n.losLegendLosBeam, + radioHorizonLabel: context.l10n.losLegendRadioHorizon, ), ), ) @@ -474,6 +487,14 @@ class _LineOfSightMapScreenState extends State { ), ), ), + if (segment != null) ...[ + const SizedBox(height: 8), + _LosLegend( + terrainLabel: context.l10n.losLegendTerrain, + losBeamLabel: context.l10n.losLegendLosBeam, + radioHorizonLabel: context.l10n.losLegendRadioHorizon, + ), + ], const SizedBox(height: 8), Text( segment != null @@ -488,6 +509,52 @@ class _LineOfSightMapScreenState extends State { ), ), const SizedBox(height: 4), + if (displayFrequencyMHz != null) + Padding( + padding: const EdgeInsets.only(top: 2, bottom: 4), + child: Row( + children: [ + Text( + context.l10n.losFrequencyLabel, + style: TextStyle( + fontSize: 11, + color: Colors.grey[700], + fontWeight: FontWeight.w600, + ), + ), + const SizedBox(width: 8), + Text( + '${displayFrequencyMHz.toStringAsFixed(3)} MHz', + style: TextStyle(fontSize: 11, color: Colors.grey[700]), + ), + if (kFactorUsed != null) ...[ + const SizedBox(width: 8), + Text( + 'k=${kFactorUsed.toStringAsFixed(3)}', + style: TextStyle( + fontSize: 11, + color: Colors.grey[700], + ), + ), + const SizedBox(width: 4), + IconButton( + padding: EdgeInsets.zero, + constraints: const BoxConstraints(), + icon: const Icon(Icons.info_outline, size: 16), + color: Colors.grey[600], + tooltip: context.l10n.losFrequencyInfoTooltip, + onPressed: () { + _showFrequencyInfoDialog( + context, + displayFrequencyMHz, + kFactorUsed, + ); + }, + ), + ], + ], + ), + ), Text( context.l10n.losElevationAttribution, style: TextStyle(fontSize: 10, color: Colors.grey[700]), @@ -896,6 +963,40 @@ class _LineOfSightMapScreenState extends State { break; } } + + void _showFrequencyInfoDialog( + BuildContext context, + double frequencyMHz, + double kFactor, + ) { + final baselineFreq = LineOfSightService.baselineFrequencyMHz; + final baselineK = LineOfSightService.baselineKFactor; + showDialog( + context: context, + builder: (dialogContext) => AlertDialog( + title: Text(context.l10n.losFrequencyDialogTitle), + content: Text( + context.l10n.losFrequencyDialogDescription( + baselineK, + baselineFreq, + frequencyMHz, + kFactor, + ), + ), + actions: [ + TextButton( + onPressed: () => Navigator.pop(dialogContext), + child: Text(context.l10n.common_ok), + ), + ], + ), + ); + } + + double? _normalizeFrequencyMHz(int? frequencyKHz) { + if (frequencyKHz == null || frequencyKHz <= 0) return null; + return frequencyKHz / 1000.0; + } } class _LosProfilePainter extends CustomPainter { @@ -903,12 +1004,18 @@ class _LosProfilePainter extends CustomPainter { final String distanceUnit; final String heightUnit; final TextStyle badgeTextStyle; + final String terrainLabel; + final String losBeamLabel; + final String radioHorizonLabel; const _LosProfilePainter({ required this.samples, required this.distanceUnit, required this.heightUnit, required this.badgeTextStyle, + required this.terrainLabel, + required this.losBeamLabel, + required this.radioHorizonLabel, }); @override @@ -920,44 +1027,108 @@ class _LosProfilePainter extends CustomPainter { if (samples.length < 2) return; final minY = samples - .map((s) => math.min(s.terrainMeters, s.lineHeightMeters)) + .map( + (s) => math.min( + math.min(s.terrainMeters, s.lineHeightMeters), + s.refractedHeightMeters, + ), + ) .reduce(math.min); final maxY = samples - .map((s) => math.max(s.terrainMeters, s.lineHeightMeters)) + .map( + (s) => math.max( + math.max(s.terrainMeters, s.lineHeightMeters), + s.refractedHeightMeters, + ), + ) .reduce(math.max); final ySpan = math.max(1.0, maxY - minY); final maxDist = math.max(1.0, samples.last.distanceMeters); + const horizontalPadding = 12.0; + const verticalPadding = 12.0; + final chartWidth = math.max(1.0, size.width - horizontalPadding * 2); + final chartHeight = math.max(1.0, size.height - verticalPadding * 2); Offset mapPoint(double x, double y) { - final px = (x / maxDist) * size.width; - final py = size.height - ((y - minY) / ySpan) * size.height; + final px = horizontalPadding + (x / maxDist) * chartWidth; + final py = + size.height - verticalPadding - ((y - minY) / ySpan) * chartHeight; return Offset(px, py); } - final terrainPath = ui.Path(); - terrainPath.moveTo(0, size.height); - for (final s in samples) { - final p = mapPoint(s.distanceMeters, s.terrainMeters); + final firstTerrainPoint = mapPoint( + samples.first.distanceMeters, + samples.first.terrainMeters, + ); + final lastTerrainPoint = mapPoint( + samples.last.distanceMeters, + samples.last.terrainMeters, + ); + + double distanceForCanvasX(double x) { + final normalized = ((x - horizontalPadding) / chartWidth).clamp(0.0, 1.0); + return normalized * maxDist; + } + + double elevationToPixel(double elevation) { + final normalized = ((elevation - minY) / ySpan).clamp(0.0, 1.0); + return size.height - verticalPadding - normalized * chartHeight; + } + + double extrapolateTerrain(double distance, bool isLeft) { + final samplesForSlope = isLeft + ? samples.sublist(0, math.min(2, samples.length)) + : samples.sublist(samples.length - math.min(2, samples.length)); + if (samplesForSlope.length < 2) { + return samplesForSlope.first.terrainMeters; + } + final a = samplesForSlope.first; + final b = samplesForSlope.last; + final dx = b.distanceMeters - a.distanceMeters; + if (dx.abs() < 1e-6) return a.terrainMeters; + final slope = (b.terrainMeters - a.terrainMeters) / dx; + return a.terrainMeters + slope * (distance - a.distanceMeters); + } + + final leftDistance = distanceForCanvasX(0.0); + final rightDistance = distanceForCanvasX(size.width); + final leftEdgeTerrain = extrapolateTerrain(leftDistance, true); + final rightEdgeTerrain = extrapolateTerrain(rightDistance, false); + final leftEdgePoint = Offset(0.0, elevationToPixel(leftEdgeTerrain)); + final rightEdgePoint = Offset( + size.width, + elevationToPixel(rightEdgeTerrain), + ); + + final terrainPath = ui.Path() + ..moveTo(0, size.height) + ..lineTo(leftEdgePoint.dx, leftEdgePoint.dy) + ..lineTo(firstTerrainPoint.dx, firstTerrainPoint.dy); + for (final sample in samples) { + final p = mapPoint(sample.distanceMeters, sample.terrainMeters); terrainPath.lineTo(p.dx, p.dy); } - terrainPath.lineTo(size.width, size.height); - terrainPath.close(); + terrainPath + ..lineTo(lastTerrainPoint.dx, lastTerrainPoint.dy) + ..lineTo(rightEdgePoint.dx, rightEdgePoint.dy) + ..lineTo(size.width, size.height) + ..close(); - canvas.drawPath(terrainPath, Paint()..color = const Color(0xCC7C6F5D)); + const terrainFillColor = Color(0xCC7C6F5D); + const terrainLineColor = Color(0xFF9FE870); + const losLineColor = Color(0xFFE0E7FF); + canvas.drawPath(terrainPath, Paint()..color = terrainFillColor); - final terrainLine = ui.Path(); - for (int i = 0; i < samples.length; i++) { - final p = mapPoint(samples[i].distanceMeters, samples[i].terrainMeters); - if (i == 0) { - terrainLine.moveTo(p.dx, p.dy); - } else { - terrainLine.lineTo(p.dx, p.dy); - } + final terrainLine = ui.Path()..moveTo(leftEdgePoint.dx, leftEdgePoint.dy); + for (final sample in samples) { + final p = mapPoint(sample.distanceMeters, sample.terrainMeters); + terrainLine.lineTo(p.dx, p.dy); } + terrainLine.lineTo(rightEdgePoint.dx, rightEdgePoint.dy); canvas.drawPath( terrainLine, Paint() - ..color = const Color(0xFF9FE870) + ..color = terrainLineColor ..style = PaintingStyle.stroke ..strokeWidth = 2, ); @@ -977,10 +1148,59 @@ class _LosProfilePainter extends CustomPainter { canvas.drawPath( losLine, Paint() - ..color = const Color(0xFFE0E7FF) + ..color = losLineColor ..style = PaintingStyle.stroke ..strokeWidth = 2, ); + + const refractedLineColor = Color(0xFFFFD57F); + final refractedLine = ui.Path(); + for (int i = 0; i < samples.length; i++) { + final p = mapPoint( + samples[i].distanceMeters, + samples[i].refractedHeightMeters, + ); + if (i == 0) { + refractedLine.moveTo(p.dx, p.dy); + } else { + refractedLine.lineTo(p.dx, p.dy); + } + } + canvas.drawPath( + refractedLine, + Paint() + ..color = refractedLineColor + ..style = PaintingStyle.stroke + ..strokeWidth = 1.5, + ); + + final capPath = ui.Path(); + for (int i = 0; i < samples.length; i++) { + final p = mapPoint( + samples[i].distanceMeters, + samples[i].refractedHeightMeters, + ); + if (i == 0) { + capPath.moveTo(p.dx, p.dy); + } else { + capPath.lineTo(p.dx, p.dy); + } + } + for (int i = samples.length - 1; i >= 0; i--) { + final p = mapPoint( + samples[i].distanceMeters, + samples[i].lineHeightMeters, + ); + capPath.lineTo(p.dx, p.dy); + } + capPath.close(); + const horizonFillColor = Color(0x40FFD57F); + canvas.drawPath( + capPath, + Paint() + ..color = horizonFillColor + ..style = PaintingStyle.fill, + ); } @override @@ -988,7 +1208,10 @@ class _LosProfilePainter extends CustomPainter { return oldDelegate.samples != samples || oldDelegate.distanceUnit != distanceUnit || oldDelegate.heightUnit != heightUnit || - oldDelegate.badgeTextStyle != badgeTextStyle; + oldDelegate.badgeTextStyle != badgeTextStyle || + oldDelegate.terrainLabel != terrainLabel || + oldDelegate.losBeamLabel != losBeamLabel || + oldDelegate.radioHorizonLabel != radioHorizonLabel; } void _drawUnitBadge(Canvas canvas, Size size) { @@ -1001,3 +1224,73 @@ class _LosProfilePainter extends CustomPainter { painter.paint(canvas, Offset(size.width - painter.width - 8, 8)); } } + +class _LosLegend extends StatelessWidget { + static const _terrainColor = Color(0xFF9FE870); + static const _losColor = Color(0xFFE0E7FF); + static const _radioColor = Color(0xFFFFD57F); + + final String terrainLabel; + final String losBeamLabel; + final String radioHorizonLabel; + + const _LosLegend({ + required this.terrainLabel, + required this.losBeamLabel, + required this.radioHorizonLabel, + }); + + @override + Widget build(BuildContext context) { + final textStyle = + Theme.of(context).textTheme.labelSmall?.copyWith( + color: Colors.white70, + fontSize: 11, + fontWeight: FontWeight.w500, + ) ?? + const TextStyle( + color: Colors.white70, + fontSize: 11, + fontWeight: FontWeight.w500, + ); + + final entries = [ + _LegendEntry(terrainLabel, _terrainColor), + _LegendEntry(losBeamLabel, _losColor), + _LegendEntry(radioHorizonLabel, _radioColor), + ]; + + const swatchSize = 10.0; + + return Wrap( + spacing: 16, + runSpacing: 6, + children: entries + .map( + (entry) => Row( + mainAxisSize: MainAxisSize.min, + children: [ + Container( + width: swatchSize, + height: swatchSize, + decoration: BoxDecoration( + color: entry.color, + borderRadius: BorderRadius.circular(2), + ), + ), + const SizedBox(width: 6), + Text(entry.label, style: textStyle), + ], + ), + ) + .toList(), + ); + } +} + +class _LegendEntry { + final String label; + final Color color; + + const _LegendEntry(this.label, this.color); +} diff --git a/lib/services/line_of_sight_service.dart b/lib/services/line_of_sight_service.dart index e9f9f7b..7f056c8 100644 --- a/lib/services/line_of_sight_service.dart +++ b/lib/services/line_of_sight_service.dart @@ -12,12 +12,14 @@ class LineOfSightSample { final double distanceMeters; final double terrainMeters; final double lineHeightMeters; + final double refractedHeightMeters; final double clearanceMeters; const LineOfSightSample({ required this.distanceMeters, required this.terrainMeters, required this.lineHeightMeters, + required this.refractedHeightMeters, required this.clearanceMeters, }); } @@ -30,6 +32,8 @@ class LineOfSightResult { final double? firstObstructionDistanceMeters; final List samples; final String? errorMessage; + final double usedKFactor; + final double? frequencyMHz; const LineOfSightResult({ required this.hasData, @@ -38,12 +42,16 @@ class LineOfSightResult { required this.maxObstructionMeters, required this.firstObstructionDistanceMeters, required this.samples, + required this.usedKFactor, + this.frequencyMHz, this.errorMessage, }); const LineOfSightResult.error({ required this.totalDistanceMeters, required this.errorMessage, + this.usedKFactor = 4.0 / 3.0, + this.frequencyMHz, }) : hasData = false, isClear = false, maxObstructionMeters = 0, @@ -89,6 +97,11 @@ class LineOfSightService { static const Duration _cacheTtl = Duration(hours: 24); static const int _maxFetchAttempts = 4; // initial try + 3 retries static const Duration _initialBackoff = Duration(milliseconds: 300); + static const double _baselineFrequencyMHz = 915.0; + static const double _baselineKFactor = 4.0 / 3.0; + + static double get baselineFrequencyMHz => _baselineFrequencyMHz; + static double get baselineKFactor => _baselineKFactor; final http.Client _httpClient; final bool _ownsHttpClient; @@ -106,7 +119,7 @@ class LineOfSightService { List points, { double startAntennaHeightMeters = 1.5, double endAntennaHeightMeters = 1.5, - double kFactor = 4.0 / 3.0, + double? frequencyMHz, double obstructionToleranceMeters = 0.0, }) async { if (points.length < 2) { @@ -123,6 +136,7 @@ class LineOfSightService { var blockedSegments = 0; var unknownSegments = 0; + final kFactor = _kFactorForFrequency(frequencyMHz); for (int i = 0; i < points.length - 1; i++) { final result = await analyzeLink( points[i], @@ -130,6 +144,7 @@ class LineOfSightService { startAntennaHeightMeters: startAntennaHeightMeters, endAntennaHeightMeters: endAntennaHeightMeters, kFactor: kFactor, + frequencyMHz: frequencyMHz, obstructionToleranceMeters: obstructionToleranceMeters, ); segments.add( @@ -163,7 +178,8 @@ class LineOfSightService { LatLng end, { double startAntennaHeightMeters = 1.5, double endAntennaHeightMeters = 1.5, - double kFactor = 4.0 / 3.0, + required double kFactor, + double? frequencyMHz, double obstructionToleranceMeters = 0.0, }) async { final totalDistanceMeters = _distance.as(LengthUnit.Meter, start, end); @@ -175,6 +191,8 @@ class LineOfSightService { maxObstructionMeters: 0, firstObstructionDistanceMeters: null, samples: const [], + usedKFactor: kFactor, + frequencyMHz: frequencyMHz, ); } @@ -185,6 +203,8 @@ class LineOfSightService { return LineOfSightResult.error( totalDistanceMeters: totalDistanceMeters, errorMessage: errorElevationUnavailable, + usedKFactor: kFactor, + frequencyMHz: frequencyMHz, ); } @@ -194,6 +214,7 @@ class LineOfSightService { startAntennaHeightMeters: startAntennaHeightMeters, endAntennaHeightMeters: endAntennaHeightMeters, kFactor: kFactor, + frequencyMHz: frequencyMHz, obstructionToleranceMeters: obstructionToleranceMeters, ); } @@ -203,13 +224,16 @@ class LineOfSightService { required List elevations, double startAntennaHeightMeters = 1.5, double endAntennaHeightMeters = 1.5, - double kFactor = 4.0 / 3.0, + required double kFactor, + double? frequencyMHz, double obstructionToleranceMeters = 0.0, }) { if (points.length < 2 || elevations.length != points.length) { - return const LineOfSightResult.error( + return LineOfSightResult.error( totalDistanceMeters: 0, errorMessage: errorInvalidInput, + usedKFactor: kFactor, + frequencyMHz: frequencyMHz, ); } @@ -238,6 +262,10 @@ class LineOfSightService { (2 * effectiveEarthRadius); final terrainHeight = elevations[i] + earthBulge; final clearance = lineHeight - terrainHeight; + final unrefBulge = + (distanceFromStart * (totalDistanceMeters - distanceFromStart)) / + (2 * _earthRadiusMeters); + final refractedHeight = lineHeight + (unrefBulge - earthBulge); if (clearance < -obstructionToleranceMeters) { isClear = false; @@ -253,6 +281,7 @@ class LineOfSightService { distanceMeters: distanceFromStart, terrainMeters: terrainHeight, lineHeightMeters: lineHeight, + refractedHeightMeters: refractedHeight, clearanceMeters: clearance, ), ); @@ -265,9 +294,20 @@ class LineOfSightService { maxObstructionMeters: maxObstructionMeters, firstObstructionDistanceMeters: firstObstructionDistanceMeters, samples: samples, + usedKFactor: kFactor, + frequencyMHz: frequencyMHz, ); } + static double _kFactorForFrequency(double? frequencyMHz) { + if (frequencyMHz == null) return _baselineKFactor; + final delta = + (frequencyMHz - _baselineFrequencyMHz) / _baselineFrequencyMHz; + final adjustment = delta * 0.15; + final scaled = _baselineKFactor * (1 + adjustment); + return scaled.clamp(1.1, 1.6).toDouble(); + } + List _buildSamplePoints( LatLng start, LatLng end, diff --git a/pubspec.lock b/pubspec.lock index ed84c40..22cad80 100644 --- a/pubspec.lock +++ b/pubspec.lock @@ -347,6 +347,14 @@ packages: url: "https://pub.dev" source: hosted version: "8.2.2" + flutter_svg: + dependency: "direct main" + description: + name: flutter_svg + sha256: "87fbd7c534435b6c5d9d98b01e1fd527812b82e68ddd8bd35fc45ed0fa8f0a95" + url: "https://pub.dev" + source: hosted + version: "2.2.3" flutter_test: dependency: "direct dev" description: flutter @@ -597,6 +605,14 @@ packages: url: "https://pub.dev" source: hosted version: "1.9.1" + path_parsing: + dependency: transitive + description: + name: path_parsing + sha256: "883402936929eac138ee0a45da5b0f2c80f89913e6dc3bf77eb65b84b409c6ca" + url: "https://pub.dev" + source: hosted + version: "1.1.0" path_provider: dependency: "direct main" description: @@ -1010,6 +1026,30 @@ packages: url: "https://pub.dev" source: hosted version: "4.5.2" + vector_graphics: + dependency: transitive + description: + name: vector_graphics + sha256: a4f059dc26fc8295b5921376600a194c4ec7d55e72f2fe4c7d2831e103d461e6 + url: "https://pub.dev" + source: hosted + version: "1.1.19" + vector_graphics_codec: + dependency: transitive + description: + name: vector_graphics_codec + sha256: "99fd9fbd34d9f9a32efd7b6a6aae14125d8237b10403b422a6a6dfeac2806146" + url: "https://pub.dev" + source: hosted + version: "1.1.13" + vector_graphics_compiler: + dependency: transitive + description: + name: vector_graphics_compiler + sha256: "5a88dd14c0954a5398af544651c7fb51b457a2a556949bfb25369b210ef73a74" + url: "https://pub.dev" + source: hosted + version: "1.2.0" vector_math: dependency: transitive description: @@ -1043,7 +1083,7 @@ packages: source: hosted version: "1.3.0" web: - dependency: transitive + dependency: "direct main" description: name: web sha256: "868d88a33d8a87b18ffc05f9f030ba328ffefba92d6c127917a2ba740f9cfe4a" diff --git a/test/services/line_of_sight_service_test.dart b/test/services/line_of_sight_service_test.dart index 987ee6c..267a70b 100644 --- a/test/services/line_of_sight_service_test.dart +++ b/test/services/line_of_sight_service_test.dart @@ -16,6 +16,7 @@ void main() { elevations: elevations, startAntennaHeightMeters: 2, endAntennaHeightMeters: 2, + kFactor: 4.0 / 3.0, ); expect(result.hasData, isTrue); @@ -36,6 +37,7 @@ void main() { elevations: elevations, startAntennaHeightMeters: 1.5, endAntennaHeightMeters: 1.5, + kFactor: 4.0 / 3.0, ); expect(result.hasData, isTrue);