Merge remote-tracking branch 'origin/calculate-refrac-los' into combined-prs

This commit is contained in:
just_stuff_tm 2026-02-24 12:40:26 -05:00
commit de63733bb9
35 changed files with 1259 additions and 57 deletions

View file

@ -1718,5 +1718,29 @@
"losPointName": "Име на точката",
"losShowPanelTooltip": "Показване на LOS панел",
"losHidePanelTooltip": "Скриване на LOS панела",
"losElevationAttribution": "Данни за надморска височина: Open-Meteo (CC BY 4.0)"
}
"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"
}
}
}
}

View file

@ -1746,5 +1746,29 @@
"losPointName": "Punktname",
"losShowPanelTooltip": "LOS-Panel anzeigen",
"losHidePanelTooltip": "LOS-Panel ausblenden",
"losElevationAttribution": "Höhendaten: Open-Meteo (CC BY 4.0)"
}
"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"
}
}
}
}

View file

@ -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"
}
}

View file

@ -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)"
}
"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"
}
}
}
}

View file

@ -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)"
}
"losElevationAttribution": "Données daltitude : 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 lhorizon 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"
}
}
}
}

View file

@ -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)"
}
"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 dellorizzonte 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"
}
}
}
}

View file

@ -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:

View file

@ -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 => 'Пътен проследяване';

View file

@ -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';

View file

@ -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';

View file

@ -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';

View file

@ -2880,7 +2880,35 @@ class AppLocalizationsFr extends AppLocalizations {
@override
String get losElevationAttribution =>
'Données d\'altitude : Open-Meteo (CC BY 4.0)';
'Données daltitude : 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 lhorizon 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';

View file

@ -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 dellorizzonte 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';

View file

@ -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';

View file

@ -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';

View file

@ -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';

View file

@ -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 => 'Трассировка пути';

View file

@ -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';

View file

@ -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';

View file

@ -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';

View file

@ -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 => 'Трасування шляхів';

View file

@ -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 => '路径追踪';

View file

@ -1718,5 +1718,29 @@
"losPointName": "Puntnaam",
"losShowPanelTooltip": "Toon LOS-paneel",
"losHidePanelTooltip": "LOS-paneel verbergen",
"losElevationAttribution": "Hoogtegegevens: Open-Meteo (CC BY 4.0)"
}
"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"
}
}
}
}

View file

@ -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)"
}
"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"
}
}
}
}

View file

@ -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)"
}
"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"
}
}
}
}

View file

@ -958,5 +958,29 @@
"losPointName": "Имя точки",
"losShowPanelTooltip": "Показать панель LOS",
"losHidePanelTooltip": "Скрыть панель LOS",
"losElevationAttribution": "Данные о высоте: Open-Meteo (CC BY 4.0)"
}
"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"
}
}
}
}

View file

@ -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)"
}
"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"
}
}
}
}

View file

@ -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)"
}
"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"
}
}
}
}

View file

@ -1718,5 +1718,29 @@
"losPointName": "Punktnamn",
"losShowPanelTooltip": "Visa LOS-panelen",
"losHidePanelTooltip": "Dölj LOS-panelen",
"losElevationAttribution": "Höjddata: Open-Meteo (CC BY 4.0)"
}
"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"
}
}
}
}

View file

@ -1718,5 +1718,29 @@
"losPointName": "Назва точки",
"losShowPanelTooltip": "Показати панель LOS",
"losHidePanelTooltip": "Приховати панель LOS",
"losElevationAttribution": "Дані про висоту: Open-Meteo (CC BY 4.0)"
}
"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"
}
}
}
}

View file

@ -1718,5 +1718,29 @@
"losPointName": "点名称",
"losShowPanelTooltip": "显示 LOS 面板",
"losHidePanelTooltip": "隐藏 LOS 面板",
"losElevationAttribution": "高程数据Open-Meteo (CC BY 4.0)"
}
"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"
}
}
}
}

View file

@ -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<LineOfSightMapScreen> {
});
try {
final connector = context.watch<MeshCoreConnector>();
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<LineOfSightMapScreen> {
Widget _buildControlPanel(bool isImperial) {
_sanitizeSelection();
final segment = _primarySegmentResult();
final connector = context.read<MeshCoreConnector>();
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<LineOfSightMapScreen> {
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<LineOfSightMapScreen> {
),
),
),
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<LineOfSightMapScreen> {
),
),
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<LineOfSightMapScreen> {
break;
}
}
void _showFrequencyInfoDialog(
BuildContext context,
double frequencyMHz,
double kFactor,
) {
final baselineFreq = LineOfSightService.baselineFrequencyMHz;
final baselineK = LineOfSightService.baselineKFactor;
showDialog<void>(
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);
}

View file

@ -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<LineOfSightSample> 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<LatLng> 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<double> 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<LatLng> _buildSamplePoints(
LatLng start,
LatLng end,

View file

@ -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"

View file

@ -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);