Merge pull request #412 from just-stuff-tm/enhancement/los-obstruction-pinning-411

add selectable LOS obstruction pinning for repeater placement Enhancement #411
This commit is contained in:
zjs81
2026-04-27 13:13:40 -07:00
committed by GitHub
42 changed files with 1817 additions and 106 deletions
+1
View File
@@ -87,6 +87,7 @@ keystore.properties
# IDE
.vscode/launch.json
.vscode/settings.json
.contextstream/
# Cloudflare Wrangler
.wrangler
+41 -1
View File
@@ -2067,5 +2067,45 @@
"room_guest": "Информация за сървъра на стаята",
"repeater_guest": "Информация за ретранслаторите",
"repeater_guestTools": "Инструменти за гости",
"settings_multiAck": "Множество потвърждения"
"settings_multiAck": "Множество потвърждения",
"@losBlockedSpotChip": {
"placeholders": {
"distance": {
"type": "String"
},
"distanceUnit": {
"type": "String"
},
"obstruction": {
"type": "String"
},
"heightUnit": {
"type": "String"
}
}
},
"@losSelectedObstructionDetails": {
"placeholders": {
"obstruction": {
"type": "String"
},
"heightUnit": {
"type": "String"
},
"distanceFromA": {
"type": "String"
},
"distanceUnit": {
"type": "String"
},
"distanceFromB": {
"type": "String"
}
}
},
"losSelectedObstructionTitle": "Избрано препятствие",
"losBlockedSpotChip": "{distance} {distanceUnit} • {obstruction} {heightUnit}",
"losBlockedSpotsHint": "Кликнете върху блокираната точка, за да я отбележите на картата.",
"losBlockedSpotsTitle": "Ограничени места",
"losSelectedObstructionDetails": "Blocked by {obstruction} {heightUnit}, {distanceFromA} from A and {distanceFromB} from B ({distanceUnit})."
}
+41 -1
View File
@@ -2095,5 +2095,45 @@
"repeater_guestTools": "Gastwerkzeuge",
"chat_sendMessage": "Nachricht senden",
"room_guest": "Informationen zum Room Server",
"settings_multiAck": "Mehrere Bestätigungen"
"settings_multiAck": "Mehrere Bestätigungen",
"@losBlockedSpotChip": {
"placeholders": {
"distance": {
"type": "String"
},
"distanceUnit": {
"type": "String"
},
"obstruction": {
"type": "String"
},
"heightUnit": {
"type": "String"
}
}
},
"@losSelectedObstructionDetails": {
"placeholders": {
"obstruction": {
"type": "String"
},
"heightUnit": {
"type": "String"
},
"distanceFromA": {
"type": "String"
},
"distanceUnit": {
"type": "String"
},
"distanceFromB": {
"type": "String"
}
}
},
"losBlockedSpotsTitle": "Reservierte Plätze",
"losSelectedObstructionTitle": "Ausgewählte Behinderung",
"losBlockedSpotChip": "{distance} • {distanceUnit} • {obstruction} {heightUnit}",
"losBlockedSpotsHint": "Klicken Sie auf einen blockierten Bereich, um ihn auf der Karte hervorzuheben.",
"losSelectedObstructionDetails": "Blockiert durch {obstruction} in einer Höhe von {heightUnit}, {distanceFromA} von A und {distanceFromB} von B ({distanceUnit})."
}
+40
View File
@@ -1878,6 +1878,46 @@
"losLegendRadioHorizon": "Radio horizon",
"losLegendLosBeam": "LOS beam",
"losLegendTerrain": "Terrain",
"losBlockedSpotsTitle": "Blocked spots",
"losBlockedSpotsHint": "Tap a blocked spot to highlight it on the map.",
"losBlockedSpotChip": "{distance} {distanceUnit} • {obstruction} {heightUnit}",
"@losBlockedSpotChip": {
"placeholders": {
"distance": {
"type": "String"
},
"distanceUnit": {
"type": "String"
},
"obstruction": {
"type": "String"
},
"heightUnit": {
"type": "String"
}
}
},
"losSelectedObstructionTitle": "Selected obstruction",
"losSelectedObstructionDetails": "Blocked by {obstruction} {heightUnit}, {distanceFromA} from A and {distanceFromB} from B ({distanceUnit}).",
"@losSelectedObstructionDetails": {
"placeholders": {
"obstruction": {
"type": "String"
},
"heightUnit": {
"type": "String"
},
"distanceFromA": {
"type": "String"
},
"distanceUnit": {
"type": "String"
},
"distanceFromB": {
"type": "String"
}
}
},
"losFrequencyLabel": "Frequency",
"losFrequencyInfoTooltip": "View calculation details",
"losFrequencyDialogTitle": "Radio horizon calculation",
+41 -1
View File
@@ -2095,5 +2095,45 @@
"chat_sendMessage": "Enviar mensaje",
"repeater_guestTools": "Herramientas para invitados",
"room_guest": "Información del servidor",
"settings_multiAck": "Múltiples respuestas de confirmación"
"settings_multiAck": "Múltiples respuestas de confirmación",
"@losBlockedSpotChip": {
"placeholders": {
"distance": {
"type": "String"
},
"distanceUnit": {
"type": "String"
},
"obstruction": {
"type": "String"
},
"heightUnit": {
"type": "String"
}
}
},
"@losSelectedObstructionDetails": {
"placeholders": {
"obstruction": {
"type": "String"
},
"heightUnit": {
"type": "String"
},
"distanceFromA": {
"type": "String"
},
"distanceUnit": {
"type": "String"
},
"distanceFromB": {
"type": "String"
}
}
},
"losBlockedSpotsTitle": "Espacios ocupados",
"losBlockedSpotsHint": "Seleccione un punto bloqueado para resaltarlo en el mapa.",
"losSelectedObstructionTitle": "Obstrucción seleccionada",
"losSelectedObstructionDetails": "Bloqueado por {obstruction} a una altura de {heightUnit}, a {distanceFromA} metros de A y a {distanceFromB} metros de B ({distanceUnit}).",
"losBlockedSpotChip": "{distance} {distanceUnit} • {obstruction} {heightUnit}"
}
+41 -1
View File
@@ -2067,5 +2067,45 @@
"chat_sendMessage": "Envoyer un message",
"room_guest": "Informations sur le serveur",
"repeater_guest": "Informations sur les répéteurs",
"settings_multiAck": "Plusieurs accusés de réception"
"settings_multiAck": "Plusieurs accusés de réception",
"@losBlockedSpotChip": {
"placeholders": {
"distance": {
"type": "String"
},
"distanceUnit": {
"type": "String"
},
"obstruction": {
"type": "String"
},
"heightUnit": {
"type": "String"
}
}
},
"@losSelectedObstructionDetails": {
"placeholders": {
"obstruction": {
"type": "String"
},
"heightUnit": {
"type": "String"
},
"distanceFromA": {
"type": "String"
},
"distanceUnit": {
"type": "String"
},
"distanceFromB": {
"type": "String"
}
}
},
"losSelectedObstructionTitle": "Obstruction sélectionnée",
"losBlockedSpotsTitle": "Places occupés",
"losBlockedSpotsHint": "Sélectionnez un emplacement bloqué pour le mettre en évidence sur la carte.",
"losSelectedObstructionDetails": "Bloqué par {obstruction}, à une hauteur de {heightUnit}, à une distance de {distanceFromA} par rapport à A et à une distance de {distanceFromB} par rapport à B ({distanceUnit}).",
"losBlockedSpotChip": "{distance} {distanceUnit} • {obstruction} {heightUnit}"
}
+41 -1
View File
@@ -2105,5 +2105,45 @@
"room_guest": "Szoba szerver információk",
"chat_sendMessage": "Üzenet küldése",
"repeater_guest": "Adatok a repeaterről",
"settings_multiAck": "Többszörös visszaigazolások"
"settings_multiAck": "Többszörös visszaigazolások",
"@losBlockedSpotChip": {
"placeholders": {
"distance": {
"type": "String"
},
"distanceUnit": {
"type": "String"
},
"obstruction": {
"type": "String"
},
"heightUnit": {
"type": "String"
}
}
},
"@losSelectedObstructionDetails": {
"placeholders": {
"obstruction": {
"type": "String"
},
"heightUnit": {
"type": "String"
},
"distanceFromA": {
"type": "String"
},
"distanceUnit": {
"type": "String"
},
"distanceFromB": {
"type": "String"
}
}
},
"losSelectedObstructionTitle": "Kiválasztott akadály",
"losBlockedSpotChip": "{distance} {distanceUnit} • {obstruction} {heightUnit}",
"losBlockedSpotsHint": "A blokkolt területet megjelölve, hogy a térképen kiemeljük.",
"losBlockedSpotsTitle": "Foglalhatatlan területek",
"losSelectedObstructionDetails": "Elakadt a {obstruction} miatt, {heightUnit} magasságban, {distanceFromA} méterrel A-tól és {distanceFromB} méterrel B-től ({distanceUnit})."
}
+41 -1
View File
@@ -2067,5 +2067,45 @@
"repeater_guestTools": "Strumenti per gli ospiti",
"chat_sendMessage": "Invia messaggio",
"room_guest": "Informazioni sul server",
"settings_multiAck": "ACK multipli"
"settings_multiAck": "ACK multipli",
"@losBlockedSpotChip": {
"placeholders": {
"distance": {
"type": "String"
},
"distanceUnit": {
"type": "String"
},
"obstruction": {
"type": "String"
},
"heightUnit": {
"type": "String"
}
}
},
"@losSelectedObstructionDetails": {
"placeholders": {
"obstruction": {
"type": "String"
},
"heightUnit": {
"type": "String"
},
"distanceFromA": {
"type": "String"
},
"distanceUnit": {
"type": "String"
},
"distanceFromB": {
"type": "String"
}
}
},
"losSelectedObstructionTitle": "Ostacolo selezionato",
"losBlockedSpotsHint": "Tocca un punto bloccato sulla mappa per evidenziarlo.",
"losBlockedSpotsTitle": "Posti occupati",
"losBlockedSpotChip": "{distance} {distanceUnit} • {obstruction} {heightUnit}",
"losSelectedObstructionDetails": "Blocked by {obstruction} {heightUnit}, {distanceFromA} from A and {distanceFromB} from B ({distanceUnit})."
}
+41 -1
View File
@@ -2105,5 +2105,45 @@
"chat_sendMessage": "メッセージを送信する",
"repeater_guest": "繰り返し送信に関する情報",
"repeater_guestTools": "ゲスト向けツール",
"settings_multiAck": "複数のACK(応答)"
"settings_multiAck": "複数のACK(応答)",
"@losBlockedSpotChip": {
"placeholders": {
"distance": {
"type": "String"
},
"distanceUnit": {
"type": "String"
},
"obstruction": {
"type": "String"
},
"heightUnit": {
"type": "String"
}
}
},
"@losSelectedObstructionDetails": {
"placeholders": {
"obstruction": {
"type": "String"
},
"heightUnit": {
"type": "String"
},
"distanceFromA": {
"type": "String"
},
"distanceUnit": {
"type": "String"
},
"distanceFromB": {
"type": "String"
}
}
},
"losBlockedSpotsHint": "地図上で、特定された場所を強調するために、該当する場所をタップしてください。",
"losSelectedObstructionTitle": "選択された障害",
"losBlockedSpotsTitle": "利用できない場所",
"losSelectedObstructionDetails": "{obstruction} によって {heightUnit} の高さで、A地点から {distanceFromA}、B地点から {distanceFromB} ({distanceUnit}) の距離で塞がれています。",
"losBlockedSpotChip": "{distance} {distanceUnit} • {obstruction} {heightUnit}"
}
+41 -1
View File
@@ -2105,5 +2105,45 @@
"chat_sendMessage": "메시지를 보내기",
"repeater_guest": "반복 장비 정보",
"room_guest": "서버 정보",
"settings_multiAck": "다중 ACK"
"settings_multiAck": "다중 ACK",
"@losBlockedSpotChip": {
"placeholders": {
"distance": {
"type": "String"
},
"distanceUnit": {
"type": "String"
},
"obstruction": {
"type": "String"
},
"heightUnit": {
"type": "String"
}
}
},
"@losSelectedObstructionDetails": {
"placeholders": {
"obstruction": {
"type": "String"
},
"heightUnit": {
"type": "String"
},
"distanceFromA": {
"type": "String"
},
"distanceUnit": {
"type": "String"
},
"distanceFromB": {
"type": "String"
}
}
},
"losBlockedSpotsHint": "지도에서 특정 위치를 강조하려면 해당 위치를 클릭하세요.",
"losBlockedSpotsTitle": "차단된 공간",
"losSelectedObstructionTitle": "선택된 장애물",
"losBlockedSpotChip": "{distance} {distanceUnit} • {obstruction} {heightUnit}",
"losSelectedObstructionDetails": "Blocked by {obstruction} {heightUnit}, {distanceFromA} from A and {distanceFromB} from B ({distanceUnit})."
}
+41
View File
@@ -5658,6 +5658,47 @@ abstract class AppLocalizations {
/// **'Terrain'**
String get losLegendTerrain;
/// No description provided for @losBlockedSpotsTitle.
///
/// In en, this message translates to:
/// **'Blocked spots'**
String get losBlockedSpotsTitle;
/// No description provided for @losBlockedSpotsHint.
///
/// In en, this message translates to:
/// **'Tap a blocked spot to highlight it on the map.'**
String get losBlockedSpotsHint;
/// No description provided for @losBlockedSpotChip.
///
/// In en, this message translates to:
/// **'{distance} {distanceUnit} • {obstruction} {heightUnit}'**
String losBlockedSpotChip(
String distance,
String distanceUnit,
String obstruction,
String heightUnit,
);
/// No description provided for @losSelectedObstructionTitle.
///
/// In en, this message translates to:
/// **'Selected obstruction'**
String get losSelectedObstructionTitle;
/// No description provided for @losSelectedObstructionDetails.
///
/// In en, this message translates to:
/// **'Blocked by {obstruction} {heightUnit}, {distanceFromA} from A and {distanceFromB} from B ({distanceUnit}).'**
String losSelectedObstructionDetails(
String obstruction,
String heightUnit,
String distanceFromA,
String distanceUnit,
String distanceFromB,
);
/// No description provided for @losFrequencyLabel.
///
/// In en, this message translates to:
+31
View File
@@ -3245,6 +3245,37 @@ class AppLocalizationsBg extends AppLocalizations {
@override
String get losLegendTerrain => 'Терен';
@override
String get losBlockedSpotsTitle => 'Ограничени места';
@override
String get losBlockedSpotsHint =>
'Кликнете върху блокираната точка, за да я отбележите на картата.';
@override
String losBlockedSpotChip(
String distance,
String distanceUnit,
String obstruction,
String heightUnit,
) {
return '$distance $distanceUnit$obstruction $heightUnit';
}
@override
String get losSelectedObstructionTitle => 'Избрано препятствие';
@override
String losSelectedObstructionDetails(
String obstruction,
String heightUnit,
String distanceFromA,
String distanceUnit,
String distanceFromB,
) {
return 'Blocked by $obstruction $heightUnit, $distanceFromA from A and $distanceFromB from B ($distanceUnit).';
}
@override
String get losFrequencyLabel => 'Честота';
+31
View File
@@ -3250,6 +3250,37 @@ class AppLocalizationsDe extends AppLocalizations {
@override
String get losLegendTerrain => 'Gelände';
@override
String get losBlockedSpotsTitle => 'Reservierte Plätze';
@override
String get losBlockedSpotsHint =>
'Klicken Sie auf einen blockierten Bereich, um ihn auf der Karte hervorzuheben.';
@override
String losBlockedSpotChip(
String distance,
String distanceUnit,
String obstruction,
String heightUnit,
) {
return '$distance$distanceUnit$obstruction $heightUnit';
}
@override
String get losSelectedObstructionTitle => 'Ausgewählte Behinderung';
@override
String losSelectedObstructionDetails(
String obstruction,
String heightUnit,
String distanceFromA,
String distanceUnit,
String distanceFromB,
) {
return 'Blockiert durch $obstruction in einer Höhe von $heightUnit, $distanceFromA von A und $distanceFromB von B ($distanceUnit).';
}
@override
String get losFrequencyLabel => 'Frequenz';
+31
View File
@@ -3189,6 +3189,37 @@ class AppLocalizationsEn extends AppLocalizations {
@override
String get losLegendTerrain => 'Terrain';
@override
String get losBlockedSpotsTitle => 'Blocked spots';
@override
String get losBlockedSpotsHint =>
'Tap a blocked spot to highlight it on the map.';
@override
String losBlockedSpotChip(
String distance,
String distanceUnit,
String obstruction,
String heightUnit,
) {
return '$distance $distanceUnit$obstruction $heightUnit';
}
@override
String get losSelectedObstructionTitle => 'Selected obstruction';
@override
String losSelectedObstructionDetails(
String obstruction,
String heightUnit,
String distanceFromA,
String distanceUnit,
String distanceFromB,
) {
return 'Blocked by $obstruction $heightUnit, $distanceFromA from A and $distanceFromB from B ($distanceUnit).';
}
@override
String get losFrequencyLabel => 'Frequency';
+31
View File
@@ -3244,6 +3244,37 @@ class AppLocalizationsEs extends AppLocalizations {
@override
String get losLegendTerrain => 'Terreno';
@override
String get losBlockedSpotsTitle => 'Espacios ocupados';
@override
String get losBlockedSpotsHint =>
'Seleccione un punto bloqueado para resaltarlo en el mapa.';
@override
String losBlockedSpotChip(
String distance,
String distanceUnit,
String obstruction,
String heightUnit,
) {
return '$distance $distanceUnit$obstruction $heightUnit';
}
@override
String get losSelectedObstructionTitle => 'Obstrucción seleccionada';
@override
String losSelectedObstructionDetails(
String obstruction,
String heightUnit,
String distanceFromA,
String distanceUnit,
String distanceFromB,
) {
return 'Bloqueado por $obstruction a una altura de $heightUnit, a $distanceFromA metros de A y a $distanceFromB metros de B ($distanceUnit).';
}
@override
String get losFrequencyLabel => 'Frecuencia';
+31
View File
@@ -3262,6 +3262,37 @@ class AppLocalizationsFr extends AppLocalizations {
@override
String get losLegendTerrain => 'Terrain';
@override
String get losBlockedSpotsTitle => 'Places occupés';
@override
String get losBlockedSpotsHint =>
'Sélectionnez un emplacement bloqué pour le mettre en évidence sur la carte.';
@override
String losBlockedSpotChip(
String distance,
String distanceUnit,
String obstruction,
String heightUnit,
) {
return '$distance $distanceUnit$obstruction $heightUnit';
}
@override
String get losSelectedObstructionTitle => 'Obstruction sélectionnée';
@override
String losSelectedObstructionDetails(
String obstruction,
String heightUnit,
String distanceFromA,
String distanceUnit,
String distanceFromB,
) {
return 'Bloqué par $obstruction, à une hauteur de $heightUnit, à une distance de $distanceFromA par rapport à A et à une distance de $distanceFromB par rapport à B ($distanceUnit).';
}
@override
String get losFrequencyLabel => 'Fréquence';
+31
View File
@@ -3258,6 +3258,37 @@ class AppLocalizationsHu extends AppLocalizations {
@override
String get losLegendTerrain => 'Terület';
@override
String get losBlockedSpotsTitle => 'Foglalhatatlan területek';
@override
String get losBlockedSpotsHint =>
'A blokkolt területet megjelölve, hogy a térképen kiemeljük.';
@override
String losBlockedSpotChip(
String distance,
String distanceUnit,
String obstruction,
String heightUnit,
) {
return '$distance $distanceUnit$obstruction $heightUnit';
}
@override
String get losSelectedObstructionTitle => 'Kiválasztott akadály';
@override
String losSelectedObstructionDetails(
String obstruction,
String heightUnit,
String distanceFromA,
String distanceUnit,
String distanceFromB,
) {
return 'Elakadt a $obstruction miatt, $heightUnit magasságban, $distanceFromA méterrel A-tól és $distanceFromB méterrel B-től ($distanceUnit).';
}
@override
String get losFrequencyLabel => 'Hatósság';
+31
View File
@@ -3247,6 +3247,37 @@ class AppLocalizationsIt extends AppLocalizations {
@override
String get losLegendTerrain => 'Terreno';
@override
String get losBlockedSpotsTitle => 'Posti occupati';
@override
String get losBlockedSpotsHint =>
'Tocca un punto bloccato sulla mappa per evidenziarlo.';
@override
String losBlockedSpotChip(
String distance,
String distanceUnit,
String obstruction,
String heightUnit,
) {
return '$distance $distanceUnit$obstruction $heightUnit';
}
@override
String get losSelectedObstructionTitle => 'Ostacolo selezionato';
@override
String losSelectedObstructionDetails(
String obstruction,
String heightUnit,
String distanceFromA,
String distanceUnit,
String distanceFromB,
) {
return 'Blocked by $obstruction $heightUnit, $distanceFromA from A and $distanceFromB from B ($distanceUnit).';
}
@override
String get losFrequencyLabel => 'Frequenza';
+30
View File
@@ -3099,6 +3099,36 @@ class AppLocalizationsJa extends AppLocalizations {
@override
String get losLegendTerrain => '地形';
@override
String get losBlockedSpotsTitle => '利用できない場所';
@override
String get losBlockedSpotsHint => '地図上で、特定された場所を強調するために、該当する場所をタップしてください。';
@override
String losBlockedSpotChip(
String distance,
String distanceUnit,
String obstruction,
String heightUnit,
) {
return '$distance $distanceUnit$obstruction $heightUnit';
}
@override
String get losSelectedObstructionTitle => '選択された障害';
@override
String losSelectedObstructionDetails(
String obstruction,
String heightUnit,
String distanceFromA,
String distanceUnit,
String distanceFromB,
) {
return '$obstruction によって $heightUnit の高さで、A地点から $distanceFromA、B地点から $distanceFromB ($distanceUnit) の距離で塞がれています。';
}
@override
String get losFrequencyLabel => '周波数';
+30
View File
@@ -3099,6 +3099,36 @@ class AppLocalizationsKo extends AppLocalizations {
@override
String get losLegendTerrain => '지형';
@override
String get losBlockedSpotsTitle => '차단된 공간';
@override
String get losBlockedSpotsHint => '지도에서 특정 위치를 강조하려면 해당 위치를 클릭하세요.';
@override
String losBlockedSpotChip(
String distance,
String distanceUnit,
String obstruction,
String heightUnit,
) {
return '$distance $distanceUnit$obstruction $heightUnit';
}
@override
String get losSelectedObstructionTitle => '선택된 장애물';
@override
String losSelectedObstructionDetails(
String obstruction,
String heightUnit,
String distanceFromA,
String distanceUnit,
String distanceFromB,
) {
return 'Blocked by $obstruction $heightUnit, $distanceFromA from A and $distanceFromB from B ($distanceUnit).';
}
@override
String get losFrequencyLabel => '빈도';
+31
View File
@@ -3229,6 +3229,37 @@ class AppLocalizationsNl extends AppLocalizations {
@override
String get losLegendTerrain => 'Terrein';
@override
String get losBlockedSpotsTitle => 'Geplande plaatsen';
@override
String get losBlockedSpotsHint =>
'Tik op een geblokkeerd gebied om het op de kaart te markeren.';
@override
String losBlockedSpotChip(
String distance,
String distanceUnit,
String obstruction,
String heightUnit,
) {
return '$distance $distanceUnit$obstruction $heightUnit';
}
@override
String get losSelectedObstructionTitle => 'Geselecteerde obstakel';
@override
String losSelectedObstructionDetails(
String obstruction,
String heightUnit,
String distanceFromA,
String distanceUnit,
String distanceFromB,
) {
return 'Blocked by $obstruction $heightUnit, $distanceFromA from A and $distanceFromB from B ($distanceUnit).';
}
@override
String get losFrequencyLabel => 'Frequentie';
+31
View File
@@ -3254,6 +3254,37 @@ class AppLocalizationsPl extends AppLocalizations {
@override
String get losLegendTerrain => 'Teren';
@override
String get losBlockedSpotsTitle => 'Zablokowane miejsca';
@override
String get losBlockedSpotsHint =>
'Kliknij zablokowane miejsce, aby je zaznaczyć na mapie.';
@override
String losBlockedSpotChip(
String distance,
String distanceUnit,
String obstruction,
String heightUnit,
) {
return '$distance $distanceUnit$obstruction $heightUnit';
}
@override
String get losSelectedObstructionTitle => 'Wybór przeszkody';
@override
String losSelectedObstructionDetails(
String obstruction,
String heightUnit,
String distanceFromA,
String distanceUnit,
String distanceFromB,
) {
return 'Blocked by $obstruction $heightUnit, $distanceFromA from A and $distanceFromB from B ($distanceUnit).';
}
@override
String get losFrequencyLabel => 'Częstotliwość';
+31
View File
@@ -3243,6 +3243,37 @@ class AppLocalizationsPt extends AppLocalizations {
@override
String get losLegendTerrain => 'Terreno';
@override
String get losBlockedSpotsTitle => 'Locais ocupados';
@override
String get losBlockedSpotsHint =>
'Toque em um ponto bloqueado para destacá-lo no mapa.';
@override
String losBlockedSpotChip(
String distance,
String distanceUnit,
String obstruction,
String heightUnit,
) {
return '$distance $distanceUnit$obstruction $heightUnit';
}
@override
String get losSelectedObstructionTitle => 'Obstrução selecionada';
@override
String losSelectedObstructionDetails(
String obstruction,
String heightUnit,
String distanceFromA,
String distanceUnit,
String distanceFromB,
) {
return 'Blocked by $obstruction $heightUnit, $distanceFromA from A and $distanceFromB from B ($distanceUnit).';
}
@override
String get losFrequencyLabel => 'Frequência';
+32
View File
@@ -3248,6 +3248,38 @@ class AppLocalizationsRu extends AppLocalizations {
@override
String get losLegendTerrain => 'Рельеф';
@override
String get losBlockedSpotsTitle => 'Зарезервированные места';
@override
String get losBlockedSpotsHint =>
'Щелкните по заблокированной области, чтобы выделить ее на карте.';
@override
String losBlockedSpotChip(
String distance,
String distanceUnit,
String obstruction,
String heightUnit,
) {
return '$distance $distanceUnit$obstruction $heightUnit';
}
@override
String get losSelectedObstructionTitle =>
'Выбранный объект, препятствующий движению';
@override
String losSelectedObstructionDetails(
String obstruction,
String heightUnit,
String distanceFromA,
String distanceUnit,
String distanceFromB,
) {
return 'Blocked by $obstruction $heightUnit, $distanceFromA from A and $distanceFromB from B ($distanceUnit).';
}
@override
String get losFrequencyLabel => 'Частота';
+31
View File
@@ -3223,6 +3223,37 @@ class AppLocalizationsSk extends AppLocalizations {
@override
String get losLegendTerrain => 'Terén';
@override
String get losBlockedSpotsTitle => 'Zablokované miesta';
@override
String get losBlockedSpotsHint =>
'Kliknite na zablokované miesto, aby ste ho zvýraznili na mape.';
@override
String losBlockedSpotChip(
String distance,
String distanceUnit,
String obstruction,
String heightUnit,
) {
return '$distance $distanceUnit$obstruction $heightUnit';
}
@override
String get losSelectedObstructionTitle => 'Vybraná prekážka';
@override
String losSelectedObstructionDetails(
String obstruction,
String heightUnit,
String distanceFromA,
String distanceUnit,
String distanceFromB,
) {
return 'Blocked by $obstruction $heightUnit, $distanceFromA from A and $distanceFromB from B ($distanceUnit).';
}
@override
String get losFrequencyLabel => 'Frekvencia';
+31
View File
@@ -3224,6 +3224,37 @@ class AppLocalizationsSl extends AppLocalizations {
@override
String get losLegendTerrain => 'Teren';
@override
String get losBlockedSpotsTitle => 'Zasedena parkirišča';
@override
String get losBlockedSpotsHint =>
'Dotaknite blokirano točko, da jo označite na zemljeplati.';
@override
String losBlockedSpotChip(
String distance,
String distanceUnit,
String obstruction,
String heightUnit,
) {
return '$distance $distanceUnit$obstruction $heightUnit';
}
@override
String get losSelectedObstructionTitle => 'Izbrano ovire';
@override
String losSelectedObstructionDetails(
String obstruction,
String heightUnit,
String distanceFromA,
String distanceUnit,
String distanceFromB,
) {
return 'Blocked by $obstruction $heightUnit, $distanceFromA from A and $distanceFromB from B ($distanceUnit).';
}
@override
String get losFrequencyLabel => 'Frekvenca';
+31
View File
@@ -3206,6 +3206,37 @@ class AppLocalizationsSv extends AppLocalizations {
@override
String get losLegendTerrain => 'Terräng';
@override
String get losBlockedSpotsTitle => 'Reserverade platser';
@override
String get losBlockedSpotsHint =>
'Klicka på en markerad plats för att framhäva den på kartan.';
@override
String losBlockedSpotChip(
String distance,
String distanceUnit,
String obstruction,
String heightUnit,
) {
return '$distance $distanceUnit$obstruction $heightUnit';
}
@override
String get losSelectedObstructionTitle => 'Vald hinder';
@override
String losSelectedObstructionDetails(
String obstruction,
String heightUnit,
String distanceFromA,
String distanceUnit,
String distanceFromB,
) {
return 'Blocked by $obstruction $heightUnit, $distanceFromA from A and $distanceFromB from B ($distanceUnit).';
}
@override
String get losFrequencyLabel => 'Frekvens';
+31
View File
@@ -3251,6 +3251,37 @@ class AppLocalizationsUk extends AppLocalizations {
@override
String get losLegendTerrain => 'Рельєф';
@override
String get losBlockedSpotsTitle => 'Заблоковані місця';
@override
String get losBlockedSpotsHint =>
'Натисніть на заблоковане місце, щоб виділити його на карті.';
@override
String losBlockedSpotChip(
String distance,
String distanceUnit,
String obstruction,
String heightUnit,
) {
return '$distance $distanceUnit$obstruction $heightUnit';
}
@override
String get losSelectedObstructionTitle => 'Вибраний об\'єкт перешкоди';
@override
String losSelectedObstructionDetails(
String obstruction,
String heightUnit,
String distanceFromA,
String distanceUnit,
String distanceFromB,
) {
return 'Blocked by $obstruction $heightUnit, $distanceFromA from A and $distanceFromB from B ($distanceUnit).';
}
@override
String get losFrequencyLabel => 'Частота';
+30
View File
@@ -3023,6 +3023,36 @@ class AppLocalizationsZh extends AppLocalizations {
@override
String get losLegendTerrain => '地形';
@override
String get losBlockedSpotsTitle => '被占用区域';
@override
String get losBlockedSpotsHint => '点击地图上的某个被遮盖的区域,以突出显示该区域。';
@override
String losBlockedSpotChip(
String distance,
String distanceUnit,
String obstruction,
String heightUnit,
) {
return '$distance $distanceUnit$obstruction $heightUnit';
}
@override
String get losSelectedObstructionTitle => '选择性阻碍';
@override
String losSelectedObstructionDetails(
String obstruction,
String heightUnit,
String distanceFromA,
String distanceUnit,
String distanceFromB,
) {
return 'Blocked by $obstruction $heightUnit, $distanceFromA from A and $distanceFromB from B ($distanceUnit).';
}
@override
String get losFrequencyLabel => '频率';
+41 -1
View File
@@ -2067,5 +2067,45 @@
"room_guest": "Informatie over de server",
"chat_sendMessage": "Verzend bericht",
"repeater_guest": "Informatie over herhalingsapparatuur",
"settings_multiAck": "Meerdere bevestigingen"
"settings_multiAck": "Meerdere bevestigingen",
"@losBlockedSpotChip": {
"placeholders": {
"distance": {
"type": "String"
},
"distanceUnit": {
"type": "String"
},
"obstruction": {
"type": "String"
},
"heightUnit": {
"type": "String"
}
}
},
"@losSelectedObstructionDetails": {
"placeholders": {
"obstruction": {
"type": "String"
},
"heightUnit": {
"type": "String"
},
"distanceFromA": {
"type": "String"
},
"distanceUnit": {
"type": "String"
},
"distanceFromB": {
"type": "String"
}
}
},
"losSelectedObstructionTitle": "Geselecteerde obstakel",
"losBlockedSpotsHint": "Tik op een geblokkeerd gebied om het op de kaart te markeren.",
"losBlockedSpotsTitle": "Geplande plaatsen",
"losBlockedSpotChip": "{distance} {distanceUnit} • {obstruction} {heightUnit}",
"losSelectedObstructionDetails": "Blocked by {obstruction} {heightUnit}, {distanceFromA} from A and {distanceFromB} from B ({distanceUnit})."
}
+41 -1
View File
@@ -2105,5 +2105,45 @@
"repeater_guestTools": "Narzędzia dla gości",
"repeater_guest": "Informacje dotyczące urządzenia powtarzającego",
"room_guest": "Informacje o serwerze",
"settings_multiAck": "Wielokrotne potwierdzenia odbioru"
"settings_multiAck": "Wielokrotne potwierdzenia odbioru",
"@losBlockedSpotChip": {
"placeholders": {
"distance": {
"type": "String"
},
"distanceUnit": {
"type": "String"
},
"obstruction": {
"type": "String"
},
"heightUnit": {
"type": "String"
}
}
},
"@losSelectedObstructionDetails": {
"placeholders": {
"obstruction": {
"type": "String"
},
"heightUnit": {
"type": "String"
},
"distanceFromA": {
"type": "String"
},
"distanceUnit": {
"type": "String"
},
"distanceFromB": {
"type": "String"
}
}
},
"losSelectedObstructionTitle": "Wybór przeszkody",
"losBlockedSpotsTitle": "Zablokowane miejsca",
"losBlockedSpotsHint": "Kliknij zablokowane miejsce, aby je zaznaczyć na mapie.",
"losBlockedSpotChip": "{distance} {distanceUnit} • {obstruction} {heightUnit}",
"losSelectedObstructionDetails": "Blocked by {obstruction} {heightUnit}, {distanceFromA} from A and {distanceFromB} from B ({distanceUnit})."
}
+41 -1
View File
@@ -2067,5 +2067,45 @@
"room_guest": "Informações do Servidor",
"chat_sendMessage": "Enviar mensagem",
"repeater_guest": "Informações sobre repetidores",
"repeater_guestTools": "Ferramentas para hóspedes"
"repeater_guestTools": "Ferramentas para hóspedes",
"@losBlockedSpotChip": {
"placeholders": {
"distance": {
"type": "String"
},
"distanceUnit": {
"type": "String"
},
"obstruction": {
"type": "String"
},
"heightUnit": {
"type": "String"
}
}
},
"@losSelectedObstructionDetails": {
"placeholders": {
"obstruction": {
"type": "String"
},
"heightUnit": {
"type": "String"
},
"distanceFromA": {
"type": "String"
},
"distanceUnit": {
"type": "String"
},
"distanceFromB": {
"type": "String"
}
}
},
"losBlockedSpotsTitle": "Locais ocupados",
"losBlockedSpotsHint": "Toque em um ponto bloqueado para destacá-lo no mapa.",
"losSelectedObstructionTitle": "Obstrução selecionada",
"losBlockedSpotChip": "{distance} {distanceUnit} • {obstruction} {heightUnit}",
"losSelectedObstructionDetails": "Blocked by {obstruction} {heightUnit}, {distanceFromA} from A and {distanceFromB} from B ({distanceUnit})."
}
+41 -1
View File
@@ -1307,5 +1307,45 @@
"repeater_guest": "Информация о ретрансляторе",
"room_guest": "Информация о сервере",
"repeater_guestTools": "Инструменты для гостей",
"settings_multiAck": "Несколько подтверждений"
"settings_multiAck": "Несколько подтверждений",
"@losBlockedSpotChip": {
"placeholders": {
"distance": {
"type": "String"
},
"distanceUnit": {
"type": "String"
},
"obstruction": {
"type": "String"
},
"heightUnit": {
"type": "String"
}
}
},
"@losSelectedObstructionDetails": {
"placeholders": {
"obstruction": {
"type": "String"
},
"heightUnit": {
"type": "String"
},
"distanceFromA": {
"type": "String"
},
"distanceUnit": {
"type": "String"
},
"distanceFromB": {
"type": "String"
}
}
},
"losBlockedSpotsHint": "Щелкните по заблокированной области, чтобы выделить ее на карте.",
"losBlockedSpotsTitle": "Зарезервированные места",
"losSelectedObstructionTitle": "Выбранный объект, препятствующий движению",
"losBlockedSpotChip": "{distance} {distanceUnit} • {obstruction} {heightUnit}",
"losSelectedObstructionDetails": "Blocked by {obstruction} {heightUnit}, {distanceFromA} from A and {distanceFromB} from B ({distanceUnit})."
}
+41 -1
View File
@@ -2067,5 +2067,45 @@
"chat_sendMessage": "Odoslať správu",
"repeater_guest": "Informácie o opakovači",
"room_guest": "Informácie o serveri",
"repeater_guestTools": "Nástroje pre hostí"
"repeater_guestTools": "Nástroje pre hostí",
"@losBlockedSpotChip": {
"placeholders": {
"distance": {
"type": "String"
},
"distanceUnit": {
"type": "String"
},
"obstruction": {
"type": "String"
},
"heightUnit": {
"type": "String"
}
}
},
"@losSelectedObstructionDetails": {
"placeholders": {
"obstruction": {
"type": "String"
},
"heightUnit": {
"type": "String"
},
"distanceFromA": {
"type": "String"
},
"distanceUnit": {
"type": "String"
},
"distanceFromB": {
"type": "String"
}
}
},
"losBlockedSpotsTitle": "Zablokované miesta",
"losBlockedSpotsHint": "Kliknite na zablokované miesto, aby ste ho zvýraznili na mape.",
"losSelectedObstructionTitle": "Vybraná prekážka",
"losBlockedSpotChip": "{distance} {distanceUnit} • {obstruction} {heightUnit}",
"losSelectedObstructionDetails": "Blocked by {obstruction} {heightUnit}, {distanceFromA} from A and {distanceFromB} from B ({distanceUnit})."
}
+41 -1
View File
@@ -2067,5 +2067,45 @@
"chat_sendMessage": "Pošlji sporočilo",
"room_guest": "Informacije o strežniku",
"repeater_guestTools": "Naložila za goste",
"settings_multiAck": "Več potrdil"
"settings_multiAck": "Več potrdil",
"@losBlockedSpotChip": {
"placeholders": {
"distance": {
"type": "String"
},
"distanceUnit": {
"type": "String"
},
"obstruction": {
"type": "String"
},
"heightUnit": {
"type": "String"
}
}
},
"@losSelectedObstructionDetails": {
"placeholders": {
"obstruction": {
"type": "String"
},
"heightUnit": {
"type": "String"
},
"distanceFromA": {
"type": "String"
},
"distanceUnit": {
"type": "String"
},
"distanceFromB": {
"type": "String"
}
}
},
"losBlockedSpotsHint": "Dotaknite blokirano točko, da jo označite na zemljeplati.",
"losSelectedObstructionTitle": "Izbrano ovire",
"losBlockedSpotsTitle": "Zasedena parkirišča",
"losBlockedSpotChip": "{distance} {distanceUnit} • {obstruction} {heightUnit}",
"losSelectedObstructionDetails": "Blocked by {obstruction} {heightUnit}, {distanceFromA} from A and {distanceFromB} from B ({distanceUnit})."
}
+41 -1
View File
@@ -2067,5 +2067,45 @@
"chat_sendMessage": "Skicka meddelande",
"repeater_guestTools": "Gästverktyg",
"room_guest": "Information om servern",
"settings_multiAck": "Flera bekräftelser"
"settings_multiAck": "Flera bekräftelser",
"@losBlockedSpotChip": {
"placeholders": {
"distance": {
"type": "String"
},
"distanceUnit": {
"type": "String"
},
"obstruction": {
"type": "String"
},
"heightUnit": {
"type": "String"
}
}
},
"@losSelectedObstructionDetails": {
"placeholders": {
"obstruction": {
"type": "String"
},
"heightUnit": {
"type": "String"
},
"distanceFromA": {
"type": "String"
},
"distanceUnit": {
"type": "String"
},
"distanceFromB": {
"type": "String"
}
}
},
"losBlockedSpotChip": "{distance} {distanceUnit} • {obstruction} {heightUnit}",
"losBlockedSpotsTitle": "Reserverade platser",
"losSelectedObstructionTitle": "Vald hinder",
"losBlockedSpotsHint": "Klicka på en markerad plats för att framhäva den på kartan.",
"losSelectedObstructionDetails": "Blocked by {obstruction} {heightUnit}, {distanceFromA} from A and {distanceFromB} from B ({distanceUnit})."
}
+41 -1
View File
@@ -2067,5 +2067,45 @@
"repeater_guest": "Інформація про ретранслятор",
"room_guest": "Інформація про сервер кімнати",
"chat_sendMessage": "Надіслати повідомлення",
"settings_multiAck": "Багато підтверджень"
"settings_multiAck": "Багато підтверджень",
"@losBlockedSpotChip": {
"placeholders": {
"distance": {
"type": "String"
},
"distanceUnit": {
"type": "String"
},
"obstruction": {
"type": "String"
},
"heightUnit": {
"type": "String"
}
}
},
"@losSelectedObstructionDetails": {
"placeholders": {
"obstruction": {
"type": "String"
},
"heightUnit": {
"type": "String"
},
"distanceFromA": {
"type": "String"
},
"distanceUnit": {
"type": "String"
},
"distanceFromB": {
"type": "String"
}
}
},
"losBlockedSpotsHint": "Натисніть на заблоковане місце, щоб виділити його на карті.",
"losBlockedSpotsTitle": "Заблоковані місця",
"losSelectedObstructionTitle": "Вибраний об'єкт перешкоди",
"losBlockedSpotChip": "{distance} {distanceUnit} • {obstruction} {heightUnit}",
"losSelectedObstructionDetails": "Blocked by {obstruction} {heightUnit}, {distanceFromA} from A and {distanceFromB} from B ({distanceUnit})."
}
+41 -1
View File
@@ -2072,5 +2072,45 @@
"repeater_guestTools": "访客工具",
"repeater_guest": "重复器信息",
"chat_sendMessage": "发送消息",
"room_guest": "服务器信息"
"room_guest": "服务器信息",
"@losBlockedSpotChip": {
"placeholders": {
"distance": {
"type": "String"
},
"distanceUnit": {
"type": "String"
},
"obstruction": {
"type": "String"
},
"heightUnit": {
"type": "String"
}
}
},
"@losSelectedObstructionDetails": {
"placeholders": {
"obstruction": {
"type": "String"
},
"heightUnit": {
"type": "String"
},
"distanceFromA": {
"type": "String"
},
"distanceUnit": {
"type": "String"
},
"distanceFromB": {
"type": "String"
}
}
},
"losBlockedSpotsTitle": "被占用区域",
"losBlockedSpotsHint": "点击地图上的某个被遮盖的区域,以突出显示该区域。",
"losSelectedObstructionTitle": "选择性阻碍",
"losBlockedSpotChip": "{distance} {distanceUnit} • {obstruction} {heightUnit}",
"losSelectedObstructionDetails": "Blocked by {obstruction} {heightUnit}, {distanceFromA} from A and {distanceFromB} from B ({distanceUnit})."
}
+398 -28
View File
@@ -62,6 +62,7 @@ class _LineOfSightMapScreenState extends State<LineOfSightMapScreen> {
bool _loading = false;
String? _error;
LineOfSightPathResult? _result;
LineOfSightObstruction? _selectedObstruction;
LineOfSightEndpoint? _start;
LineOfSightEndpoint? _end;
final List<LineOfSightEndpoint> _customEndpoints = [];
@@ -111,6 +112,7 @@ class _LineOfSightMapScreenState extends State<LineOfSightMapScreen> {
if (start == null || end == null) {
setState(() {
_result = null;
_selectedObstruction = null;
_error = _errorSelectStartEnd;
});
return;
@@ -142,6 +144,7 @@ class _LineOfSightMapScreenState extends State<LineOfSightMapScreen> {
}
setState(() {
_result = result;
_selectedObstruction = _defaultObstructionFor(result);
});
} catch (e) {
if (!mounted) return;
@@ -156,6 +159,7 @@ class _LineOfSightMapScreenState extends State<LineOfSightMapScreen> {
}
setState(() {
_result = null;
_selectedObstruction = null;
_error = context.l10n.losRunFailed(e.toString());
});
} finally {
@@ -184,6 +188,7 @@ class _LineOfSightMapScreenState extends State<LineOfSightMapScreen> {
void _selectFromMap(LineOfSightEndpoint endpoint) {
setState(() {
_result = null;
_selectedObstruction = null;
_error = null;
if (_start == null || (_start != null && _end != null)) {
_start = endpoint;
@@ -241,6 +246,7 @@ class _LineOfSightMapScreenState extends State<LineOfSightMapScreen> {
_start = null;
_end = null;
_result = null;
_selectedObstruction = null;
_error = _errorSelectStartEnd;
});
}
@@ -251,6 +257,7 @@ class _LineOfSightMapScreenState extends State<LineOfSightMapScreen> {
if (identical(_start, endpoint)) _start = null;
if (identical(_end, endpoint)) _end = null;
_result = null;
_selectedObstruction = null;
});
}
@@ -377,7 +384,9 @@ class _LineOfSightMapScreenState extends State<LineOfSightMapScreen> {
),
if (_result != null && _result!.segments.isNotEmpty)
PolylineLayer(polylines: _buildSegmentPolylines(_result!)),
MarkerLayer(markers: _buildMarkers(endpoints)),
MarkerLayer(
markers: _buildMarkers(endpoints, _primaryObstructions()),
),
],
),
if (_showHud)
@@ -445,6 +454,8 @@ class _LineOfSightMapScreenState extends State<LineOfSightMapScreen> {
);
final displayFrequencyMHz = segment?.frequencyMHz ?? reportedFrequencyMHz;
final kFactorUsed = segment?.usedKFactor;
final obstructions =
segment?.obstructions ?? const <LineOfSightObstruction>[];
final endpoints = _visibleEndpoints();
final distanceUnit = isImperial ? 'mi' : 'km';
final heightUnit = isImperial ? 'ft' : 'm';
@@ -463,31 +474,7 @@ class _LineOfSightMapScreenState extends State<LineOfSightMapScreen> {
crossAxisAlignment: CrossAxisAlignment.start,
children: [
if (segment != null)
SizedBox(
height: 160,
width: double.infinity,
child: CustomPaint(
painter: _LosProfilePainter(
samples: segment.samples,
distanceUnit: distanceUnit,
heightUnit: heightUnit,
badgeTextStyle:
Theme.of(context).textTheme.labelSmall?.copyWith(
color: Colors.white70,
fontSize: 10,
fontWeight: FontWeight.w600,
) ??
const TextStyle(
color: Colors.white70,
fontSize: 10,
fontWeight: FontWeight.w600,
),
terrainLabel: context.l10n.losLegendTerrain,
losBeamLabel: context.l10n.losLegendLosBeam,
radioHorizonLabel: context.l10n.losLegendRadioHorizon,
),
),
)
_buildProfileView(segment, distanceUnit, heightUnit, isImperial)
else
SizedBox(
height: 44,
@@ -519,6 +506,96 @@ class _LineOfSightMapScreenState extends State<LineOfSightMapScreen> {
fontWeight: FontWeight.w600,
),
),
if (obstructions.isNotEmpty) ...[
const SizedBox(height: 8),
Text(
context.l10n.losBlockedSpotsTitle,
style: const TextStyle(
fontSize: 12,
fontWeight: FontWeight.w700,
),
),
const SizedBox(height: 4),
Text(
context.l10n.losBlockedSpotsHint,
style: TextStyle(fontSize: 11, color: Colors.grey[700]),
),
const SizedBox(height: 6),
Wrap(
spacing: 6,
runSpacing: 6,
children: [
for (final obstruction in obstructions)
ChoiceChip(
label: Text(
_obstructionChipLabel(obstruction, isImperial),
style: const TextStyle(fontSize: 11),
),
selected:
_selectedObstruction?.sampleIndex ==
obstruction.sampleIndex,
onSelected: (_) => _selectObstruction(obstruction),
),
],
),
if (_selectedObstruction != null) ...[
const SizedBox(height: 8),
DecoratedBox(
decoration: BoxDecoration(
color: Colors.orange.withValues(alpha: 0.1),
borderRadius: BorderRadius.circular(10),
border: Border.all(
color: Colors.deepOrangeAccent.withValues(alpha: 0.45),
),
),
child: Padding(
padding: const EdgeInsets.all(10),
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Text(
context.l10n.losSelectedObstructionTitle,
style: const TextStyle(
fontSize: 12,
fontWeight: FontWeight.w700,
),
),
const SizedBox(height: 4),
Text(
context.l10n.losSelectedObstructionDetails(
_formatHeightValue(
_selectedObstruction!.obstructionMeters,
isImperial,
),
heightUnit,
_formatDistanceValue(
_selectedObstruction!.distanceMeters,
isImperial,
),
distanceUnit,
_formatDistanceValue(
segment!.totalDistanceMeters -
_selectedObstruction!.distanceMeters,
isImperial,
),
),
style: const TextStyle(fontSize: 11),
),
const SizedBox(height: 4),
Text(
'${_selectedObstruction!.point.latitude.toStringAsFixed(5)}, '
'${_selectedObstruction!.point.longitude.toStringAsFixed(5)}',
style: TextStyle(
fontSize: 11,
color: Colors.grey[700],
),
),
],
),
),
),
],
],
const SizedBox(height: 4),
if (displayFrequencyMHz != null)
Padding(
@@ -605,6 +682,7 @@ class _LineOfSightMapScreenState extends State<LineOfSightMapScreen> {
_showDisplayNodes = value;
_sanitizeSelection();
_result = null;
_selectedObstruction = null;
});
},
),
@@ -655,6 +733,7 @@ class _LineOfSightMapScreenState extends State<LineOfSightMapScreen> {
setState(() {
_start = value;
_result = null;
_selectedObstruction = null;
});
if (_start != null && _end != null) {
_runLos();
@@ -670,6 +749,7 @@ class _LineOfSightMapScreenState extends State<LineOfSightMapScreen> {
setState(() {
_end = value;
_result = null;
_selectedObstruction = null;
});
if (_start != null && _end != null) {
_runLos();
@@ -769,6 +849,179 @@ class _LineOfSightMapScreenState extends State<LineOfSightMapScreen> {
return _result!.segments.first.result;
}
List<LineOfSightObstruction> _primaryObstructions() {
return _primarySegmentResult()?.obstructions ?? const [];
}
LineOfSightObstruction? _defaultObstructionFor(
LineOfSightPathResult? result,
) {
if (result == null || result.segments.isEmpty) return null;
final obstructions = result.segments.first.result.obstructions;
if (obstructions.isEmpty) return null;
return obstructions.reduce(
(current, next) =>
next.obstructionMeters > current.obstructionMeters ? next : current,
);
}
void _selectObstruction(LineOfSightObstruction obstruction) {
setState(() {
_selectedObstruction = obstruction;
});
}
String _formatDistanceValue(double meters, bool isImperial) {
final value = isImperial ? (meters / 1000.0) * _kmToMiles : meters / 1000.0;
return value.toStringAsFixed(2);
}
String _formatHeightValue(double meters, bool isImperial) {
final value = isImperial ? meters * _metersToFeet : meters;
return value.toStringAsFixed(1);
}
String _obstructionChipLabel(
LineOfSightObstruction obstruction,
bool isImperial,
) {
final distanceUnit = isImperial ? 'mi' : 'km';
final heightUnit = isImperial ? 'ft' : 'm';
return context.l10n.losBlockedSpotChip(
_formatDistanceValue(obstruction.distanceMeters, isImperial),
distanceUnit,
_formatHeightValue(obstruction.obstructionMeters, isImperial),
heightUnit,
);
}
Widget _buildProfileView(
LineOfSightResult segment,
String distanceUnit,
String heightUnit,
bool isImperial,
) {
if (segment.samples.length < 2) {
return SizedBox(
height: 160,
width: double.infinity,
child: CustomPaint(
painter: _LosProfilePainter(
samples: segment.samples,
distanceUnit: distanceUnit,
heightUnit: heightUnit,
badgeTextStyle:
Theme.of(context).textTheme.labelSmall?.copyWith(
color: Colors.white70,
fontSize: 10,
fontWeight: FontWeight.w600,
) ??
const TextStyle(
color: Colors.white70,
fontSize: 10,
fontWeight: FontWeight.w600,
),
terrainLabel: context.l10n.losLegendTerrain,
losBeamLabel: context.l10n.losLegendLosBeam,
radioHorizonLabel: context.l10n.losLegendRadioHorizon,
selectedSampleIndex: _selectedObstruction?.sampleIndex,
),
),
);
}
return SizedBox(
height: 160,
width: double.infinity,
child: LayoutBuilder(
builder: (context, constraints) {
final size = Size(constraints.maxWidth, 160);
final geometry = _LosProfileGeometry(
samples: segment.samples,
size: size,
);
return Stack(
clipBehavior: Clip.none,
children: [
Positioned.fill(
child: CustomPaint(
painter: _LosProfilePainter(
samples: segment.samples,
distanceUnit: distanceUnit,
heightUnit: heightUnit,
badgeTextStyle:
Theme.of(context).textTheme.labelSmall?.copyWith(
color: Colors.white70,
fontSize: 10,
fontWeight: FontWeight.w600,
) ??
const TextStyle(
color: Colors.white70,
fontSize: 10,
fontWeight: FontWeight.w600,
),
terrainLabel: context.l10n.losLegendTerrain,
losBeamLabel: context.l10n.losLegendLosBeam,
radioHorizonLabel: context.l10n.losLegendRadioHorizon,
selectedSampleIndex: _selectedObstruction?.sampleIndex,
),
),
),
for (final obstruction in segment.obstructions)
Builder(
builder: (context) {
final sample = segment.samples[obstruction.sampleIndex];
final position = geometry.mapPoint(
sample.distanceMeters,
sample.terrainMeters,
);
final isSelected =
_selectedObstruction?.sampleIndex ==
obstruction.sampleIndex;
final markerSize = isSelected ? 18.0 : 14.0;
final left = (position.dx - markerSize / 2)
.clamp(0.0, math.max(0.0, size.width - markerSize))
.toDouble();
final top = (position.dy - markerSize / 2)
.clamp(0.0, math.max(0.0, size.height - markerSize))
.toDouble();
return Positioned(
left: left,
top: top,
child: Tooltip(
message: _obstructionChipLabel(obstruction, isImperial),
child: GestureDetector(
onTap: () => _selectObstruction(obstruction),
child: Container(
width: markerSize,
height: markerSize,
decoration: BoxDecoration(
color: isSelected
? Colors.amberAccent
: Colors.deepOrangeAccent,
shape: BoxShape.circle,
border: Border.all(
color: isSelected
? Colors.white
: Colors.black87,
width: isSelected ? 2 : 1.5,
),
boxShadow: const [
BoxShadow(color: Colors.black45, blurRadius: 4),
],
),
),
),
),
);
},
),
],
);
},
),
);
}
String _profileStats(LineOfSightResult result, bool isImperial) {
final distance = isImperial
? (result.totalDistanceMeters / 1000.0) * _kmToMiles
@@ -820,8 +1073,51 @@ class _LineOfSightMapScreenState extends State<LineOfSightMapScreen> {
return polylines;
}
List<Marker> _buildMarkers(List<LineOfSightEndpoint> endpoints) {
List<Marker> _buildMarkers(
List<LineOfSightEndpoint> endpoints,
List<LineOfSightObstruction> obstructions,
) {
return [
for (final obstruction in obstructions)
Marker(
point: obstruction.point,
width: 52,
height: 52,
child: GestureDetector(
onTap: () => _selectObstruction(obstruction),
child: Center(
child: Container(
width:
_selectedObstruction?.sampleIndex == obstruction.sampleIndex
? 36
: 24,
height:
_selectedObstruction?.sampleIndex == obstruction.sampleIndex
? 36
: 24,
decoration: BoxDecoration(
shape: BoxShape.circle,
color: Colors.transparent,
border: Border.all(
color:
_selectedObstruction?.sampleIndex ==
obstruction.sampleIndex
? Colors.amberAccent
: Colors.deepOrangeAccent,
width:
_selectedObstruction?.sampleIndex ==
obstruction.sampleIndex
? 4
: 3,
),
boxShadow: const [
BoxShadow(color: Colors.black26, blurRadius: 6),
],
),
),
),
),
),
for (final endpoint in endpoints)
Marker(
point: endpoint.point,
@@ -1010,6 +1306,51 @@ class _LineOfSightMapScreenState extends State<LineOfSightMapScreen> {
}
}
class _LosProfileGeometry {
static const horizontalPadding = 12.0;
static const verticalPadding = 12.0;
final List<LineOfSightSample> samples;
final Size size;
late final double minY = samples
.map(
(s) => math.min(
math.min(s.terrainMeters, s.lineHeightMeters),
s.refractedHeightMeters,
),
)
.reduce(math.min);
late final double maxY = samples
.map(
(s) => math.max(
math.max(s.terrainMeters, s.lineHeightMeters),
s.refractedHeightMeters,
),
)
.reduce(math.max);
late final double ySpan = math.max(1.0, maxY - minY);
late final double maxDist = math.max(1.0, samples.last.distanceMeters);
late final double chartWidth = math.max(
1.0,
size.width - horizontalPadding * 2,
);
late final double chartHeight = math.max(
1.0,
size.height - verticalPadding * 2,
);
_LosProfileGeometry({required this.samples, required this.size});
Offset mapPoint(double distanceMeters, double elevationMeters) {
final px = horizontalPadding + (distanceMeters / maxDist) * chartWidth;
final py =
size.height -
verticalPadding -
((elevationMeters - minY) / ySpan) * chartHeight;
return Offset(px, py);
}
}
class _LosProfilePainter extends CustomPainter {
final List<LineOfSightSample> samples;
final String distanceUnit;
@@ -1018,6 +1359,7 @@ class _LosProfilePainter extends CustomPainter {
final String terrainLabel;
final String losBeamLabel;
final String radioHorizonLabel;
final int? selectedSampleIndex;
const _LosProfilePainter({
required this.samples,
@@ -1027,6 +1369,7 @@ class _LosProfilePainter extends CustomPainter {
required this.terrainLabel,
required this.losBeamLabel,
required this.radioHorizonLabel,
this.selectedSampleIndex,
});
@override
@@ -1212,6 +1555,32 @@ class _LosProfilePainter extends CustomPainter {
..color = horizonFillColor
..style = PaintingStyle.fill,
);
if (selectedSampleIndex != null &&
selectedSampleIndex! >= 0 &&
selectedSampleIndex! < samples.length) {
final selectedSample = samples[selectedSampleIndex!];
final selectedPoint = mapPoint(
selectedSample.distanceMeters,
selectedSample.terrainMeters,
);
canvas.drawLine(
Offset(selectedPoint.dx, verticalPadding),
Offset(selectedPoint.dx, size.height - verticalPadding),
Paint()
..color = Colors.amberAccent.withValues(alpha: 0.7)
..strokeWidth = 1.5,
);
canvas.drawCircle(selectedPoint, 7, Paint()..color = Colors.amberAccent);
canvas.drawCircle(
selectedPoint,
8.5,
Paint()
..color = Colors.white
..style = PaintingStyle.stroke
..strokeWidth = 1.5,
);
}
}
@override
@@ -1222,7 +1591,8 @@ class _LosProfilePainter extends CustomPainter {
oldDelegate.badgeTextStyle != badgeTextStyle ||
oldDelegate.terrainLabel != terrainLabel ||
oldDelegate.losBeamLabel != losBeamLabel ||
oldDelegate.radioHorizonLabel != radioHorizonLabel;
oldDelegate.radioHorizonLabel != radioHorizonLabel ||
oldDelegate.selectedSampleIndex != selectedSampleIndex;
}
void _drawUnitBadge(Canvas canvas, Size size) {
+48 -1
View File
@@ -24,6 +24,26 @@ class LineOfSightSample {
});
}
class LineOfSightObstruction {
final int sampleIndex;
final LatLng point;
final double distanceMeters;
final double clearanceMeters;
final double obstructionMeters;
final double terrainMeters;
final double lineHeightMeters;
const LineOfSightObstruction({
required this.sampleIndex,
required this.point,
required this.distanceMeters,
required this.clearanceMeters,
required this.obstructionMeters,
required this.terrainMeters,
required this.lineHeightMeters,
});
}
class LineOfSightResult {
final bool hasData;
final bool isClear;
@@ -31,6 +51,7 @@ class LineOfSightResult {
final double maxObstructionMeters;
final double? firstObstructionDistanceMeters;
final List<LineOfSightSample> samples;
final List<LineOfSightObstruction> obstructions;
final String? errorMessage;
final double usedKFactor;
final double? frequencyMHz;
@@ -42,6 +63,7 @@ class LineOfSightResult {
required this.maxObstructionMeters,
required this.firstObstructionDistanceMeters,
required this.samples,
required this.obstructions,
required this.usedKFactor,
this.frequencyMHz,
this.errorMessage,
@@ -56,7 +78,8 @@ class LineOfSightResult {
isClear = false,
maxObstructionMeters = 0,
firstObstructionDistanceMeters = null,
samples = const [];
samples = const [],
obstructions = const [];
}
class LineOfSightPathSegment {
@@ -191,6 +214,7 @@ class LineOfSightService {
maxObstructionMeters: 0,
firstObstructionDistanceMeters: null,
samples: const [],
obstructions: const [],
usedKFactor: kFactor,
frequencyMHz: frequencyMHz,
);
@@ -249,7 +273,9 @@ class LineOfSightService {
var maxObstructionMeters = 0.0;
double? firstObstructionDistanceMeters;
final samples = <LineOfSightSample>[];
final obstructions = <LineOfSightObstruction>[];
var isClear = true;
LineOfSightObstruction? clusterWorstObstruction;
for (int i = 0; i < points.length; i++) {
final fraction = points.length == 1 ? 0.0 : i / (points.length - 1);
@@ -274,6 +300,23 @@ class LineOfSightService {
maxObstructionMeters = obstruction;
}
firstObstructionDistanceMeters ??= distanceFromStart;
final candidate = LineOfSightObstruction(
sampleIndex: i,
point: points[i],
distanceMeters: distanceFromStart,
clearanceMeters: clearance,
obstructionMeters: obstruction,
terrainMeters: terrainHeight,
lineHeightMeters: lineHeight,
);
if (clusterWorstObstruction == null ||
candidate.obstructionMeters >
clusterWorstObstruction.obstructionMeters) {
clusterWorstObstruction = candidate;
}
} else if (clusterWorstObstruction != null) {
obstructions.add(clusterWorstObstruction);
clusterWorstObstruction = null;
}
samples.add(
@@ -286,6 +329,9 @@ class LineOfSightService {
),
);
}
if (clusterWorstObstruction != null) {
obstructions.add(clusterWorstObstruction);
}
return LineOfSightResult(
hasData: true,
@@ -294,6 +340,7 @@ class LineOfSightService {
maxObstructionMeters: maxObstructionMeters,
firstObstructionDistanceMeters: firstObstructionDistanceMeters,
samples: samples,
obstructions: obstructions,
usedKFactor: kFactor,
frequencyMHz: frequencyMHz,
);
@@ -23,6 +23,7 @@ void main() {
expect(result.isClear, isTrue);
expect(result.maxObstructionMeters, equals(0));
expect(result.firstObstructionDistanceMeters, isNull);
expect(result.obstructions, isEmpty);
});
test(
@@ -44,9 +45,32 @@ void main() {
expect(result.isClear, isFalse);
expect(result.maxObstructionMeters, greaterThan(0));
expect(result.firstObstructionDistanceMeters, isNotNull);
expect(result.obstructions, hasLength(1));
expect(result.obstructions.single.sampleIndex, equals(10));
expect(result.obstructions.single.point, equals(points[10]));
},
);
test('computeFromElevations groups contiguous blocked samples', () {
final points = makePoints(21);
final elevations = List<double>.filled(points.length, 100);
elevations[9] = 220;
elevations[10] = 320;
elevations[11] = 240;
final result = LineOfSightService.computeFromElevations(
points: points,
elevations: elevations,
startAntennaHeightMeters: 1.5,
endAntennaHeightMeters: 1.5,
kFactor: 4.0 / 3.0,
);
expect(result.obstructions, hasLength(1));
expect(result.obstructions.single.sampleIndex, equals(10));
expect(result.obstructions.single.obstructionMeters, greaterThan(0));
});
test('analyzePath summarizes clear and blocked segments', () async {
final service = LineOfSightService(
elevationDataSource: (points) async {
+12 -60
View File
@@ -1,86 +1,38 @@
{
"bg": [
"chat_markAsUnread",
"chat_newMessages"
"losSelectedObstructionDetails"
],
"de": [
"chat_markAsUnread",
"chat_newMessages"
],
"es": [
"chat_markAsUnread",
"chat_newMessages"
],
"fr": [
"chat_markAsUnread",
"chat_newMessages"
],
"hu": [
"chat_markAsUnread",
"chat_newMessages"
],
"it": [
"chat_markAsUnread",
"chat_newMessages"
"losSelectedObstructionDetails"
],
"ja": [
"chat_markAsUnread",
"chat_newMessages"
],
"ko": [
"chat_markAsUnread",
"chat_newMessages"
"losSelectedObstructionDetails"
],
"nl": [
"chat_markAsUnread",
"chat_newMessages"
"losSelectedObstructionDetails"
],
"pl": [
"chat_markAsUnread",
"chat_newMessages"
"losSelectedObstructionDetails"
],
"pt": [
"chat_markAsUnread",
"chat_newMessages"
"losSelectedObstructionDetails"
],
"ru": [
"chat_markAsUnread",
"chat_newMessages"
"losSelectedObstructionDetails"
],
"sk": [
"chat_markAsUnread",
"chat_newMessages"
"losSelectedObstructionDetails"
],
"sl": [
"chat_markAsUnread",
"chat_newMessages"
"losSelectedObstructionDetails"
],
"sv": [
"chat_markAsUnread",
"chat_newMessages"
"losSelectedObstructionDetails"
],
"uk": [
"chat_markAsUnread",
"chat_newMessages"
"losSelectedObstructionDetails"
],
"zh": [
"chat_markAsUnread",
"chat_newMessages"
"losSelectedObstructionDetails"
]
}