mirror of
https://github.com/praktimarc/kst4contest.git
synced 2026-10-26 16:15:51 +01:00
Path analysis: own ground height ASL and highest point of the remote locator square (#133)
* Path analysis: own ground height ASL and highest point of the remote square Two new settings in the path-analysis section of the Station tab: - Own ground height ASL (stn_pathAnalysisOwnHeightAslMeters). Empty keeps the elevation model value at the own locator centre; a value from -500 to 9000 m replaces the elevation of the first profile sample. The antenna height AGL is still added on top. The field commits on Enter or focus loss into the settings draft and reports a refused entry; it keeps the operator's spelling while that names the drafted number, because the draft is Compose state and would otherwise swap "25" for "25.0" under the cursor right after Enter. - Target station on the highest point of its locator square (stn_pathAnalysisDxHighestPointInSquare, on by default). The remote square is sampled on a 5 x 5 grid of cell centres in one batched request against the Open-Meteo provider, the highest sample becomes the path end with its position and elevation. LocatorHighestPointResolver caches the result per locator for the session and backs off five minutes after a failure (failure list pruned and bounded); the analysis then falls back to the square centre and that result is not kept. Both settings are drafted like the other path-analysis settings and take effect with Apply/Close or Save; the reachability service reads the stored values only. The lookup runs on the reachability thread only. Building a request peeks the resolver cache without blocking, so a cached square moves the endpoint right away. The calculation key now includes the own height and the remote ground elevation at full precision, and the switch, so changing either recalculates. Also assign the result of sanitizeEffectiveEarthRadiusFactor in the PathAnalysisRequest constructor; it was called and dropped. No behaviour change: the sanitizer is the identity for valid factors, and the analysis service sanitizes again. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> * Document own ground height and highest-point setting of the path analysis Changelog v1.50 (New), configuration and features pages in English and German, and the terrain section of PROJECT_CONTEXT. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> --------- Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
1 parent
ee4f1fe02e
commit
92c2576a81
29 files changed
+1789
-23
No files matched your search
@@ -223,7 +223,7 @@ fun buildSettingsTabs(
|
||||
* so a tidier order or a shorter title would be a regression, not an improvement.
|
||||
*/
|
||||
return listOf(
|
||||
SettingsTab({ kst4contest.view.i18n.CurrentStrings.get().tabStation }) { StationTab(stationState) },
|
||||
SettingsTab({ kst4contest.view.i18n.CurrentStrings.get().tabStation }) { StationTab(stationState, reportRefusal) },
|
||||
SettingsTab({ kst4contest.view.i18n.CurrentStrings.get().tabLogSynch }) { LogSynchTab(logSynchState) },
|
||||
SettingsTab({ kst4contest.view.i18n.CurrentStrings.get().tabTrxSynch }) { TrxSynchTab(TrxSynchTabState(prefs, draft, host::applyOwnQrgFollower)) },
|
||||
SettingsTab({ kst4contest.view.i18n.CurrentStrings.get().tabAirscout }) { AirscoutTab(AirscoutTabState(prefs, draft)) },
|
||||
|
||||
@@ -13,9 +13,11 @@ import kst4contest.view.compose.Form
|
||||
*
|
||||
* The controls stage their edits in the window's draft (see StationTabState); only the
|
||||
* login credentials write through at once.
|
||||
*
|
||||
* @param onRefused shows why a committed entry was refused (the own ground height)
|
||||
*/
|
||||
@Composable
|
||||
fun StationTab(state: StationTabState) {
|
||||
fun StationTab(state: StationTabState, onRefused: (String) -> Unit = {}) {
|
||||
val strings = LocalStrings.current
|
||||
|
||||
Column(
|
||||
@@ -98,8 +100,22 @@ fun StationTab(state: StationTabState) {
|
||||
Form.decimal(strings.stationOwnTxPower, state.stn_pathAnalysisOwnTxPowerWatts) { state.stn_pathAnalysisOwnTxPowerWatts = it }
|
||||
Form.decimal(strings.stationOwnAntennaGain, state.stn_pathAnalysisOwnAntennaGainDbi) { state.stn_pathAnalysisOwnAntennaGainDbi = it }
|
||||
Form.decimal(strings.stationOwnAntennaHeight, state.stn_pathAnalysisOwnAntennaHeightMeters) { state.stn_pathAnalysisOwnAntennaHeightMeters = it }
|
||||
/*
|
||||
* Committed on Enter or on leaving the field, not per keystroke: the stored
|
||||
* value reads back as "250.0", and a per-keystroke write would feed that back
|
||||
* into the field while the operator is still typing.
|
||||
*/
|
||||
Form.committed(
|
||||
label = strings.stationOwnHeightAsl,
|
||||
stored = state.stn_pathAnalysisOwnHeightAslText,
|
||||
commit = state::commitOwnHeightAsl,
|
||||
onRefused = onRefused,
|
||||
isUnchanged = StationTabState::isOwnHeightAslUnchanged,
|
||||
current = { state.stn_pathAnalysisOwnHeightAslText },
|
||||
)
|
||||
Form.decimal(strings.stationTargetTxPower, state.stn_pathAnalysisDefaultTargetTxPowerWatts) { state.stn_pathAnalysisDefaultTargetTxPowerWatts = it }
|
||||
Form.decimal(strings.stationTargetAntennaGain, state.stn_pathAnalysisDefaultTargetAntennaGainDbi) { state.stn_pathAnalysisDefaultTargetAntennaGainDbi = it }
|
||||
Form.check(strings.stationTargetHighestPoint, state.stn_pathAnalysisDxHighestPointInSquare) { state.stn_pathAnalysisDxHighestPointInSquare = it }
|
||||
Form.text(strings.stationDemDirectory, state.stn_pathAnalysisDemRootDirectory) { state.stn_pathAnalysisDemRootDirectory = it }
|
||||
}
|
||||
}
|
||||
|
||||
@@ -3,8 +3,13 @@ package kst4contest.view.compose.tabs
|
||||
import kst4contest.controller.On4KstConnectionState
|
||||
import kst4contest.model.ChatCategory
|
||||
import kst4contest.model.ChatPreferences
|
||||
import androidx.compose.runtime.getValue
|
||||
import androidx.compose.runtime.mutableStateOf
|
||||
import androidx.compose.runtime.setValue
|
||||
import kst4contest.view.compose.Form
|
||||
import kst4contest.view.compose.SettingsDraft
|
||||
import kst4contest.view.compose.setting
|
||||
import kst4contest.view.i18n.CurrentStrings
|
||||
|
||||
/**
|
||||
* The Station tab as a typed facade over ChatPreferences.
|
||||
@@ -103,6 +108,58 @@ class StationTabState(
|
||||
|
||||
var stn_pathAnalysisOwnTxPowerWatts: Double by draft.setting({ prefs.getStn_pathAnalysisOwnTxPowerWatts() }) { prefs.setStn_pathAnalysisOwnTxPowerWatts(it) }
|
||||
|
||||
/**
|
||||
* Own ground elevation above sea level in metres, NaN while the elevation model decides.
|
||||
* Staged like every path-analysis setting; the reachability service reads the stored
|
||||
* value, so a changed height reaches the analysis only once it is applied.
|
||||
*/
|
||||
var stn_pathAnalysisOwnHeightAslMeters: Double by draft.setting({ prefs.getStn_pathAnalysisOwnHeightAslMeters() }) { prefs.setStn_pathAnalysisOwnHeightAslMeters(it) }
|
||||
|
||||
/** Whether the path ends at the highest point of the remote locator square. */
|
||||
var stn_pathAnalysisDxHighestPointInSquare: Boolean by draft.setting({ prefs.isStn_pathAnalysisDxHighestPointInSquare() }) { prefs.setStn_pathAnalysisDxHighestPointInSquare(it) }
|
||||
|
||||
/**
|
||||
* The entry last committed to the own-height field, as the operator typed it.
|
||||
*
|
||||
* [Form.committed] keys its draft text on the stored text, and the staged number reads
|
||||
* back in its own spelling ("25.0" for "25"). The settings draft is Compose state, so
|
||||
* the commit itself recomposes the field with the new key, and without this the field
|
||||
* would swap "25" for "25.0" under the cursor right after Enter, or drop keys typed
|
||||
* since. Handing back the committed spelling while it still names the same number
|
||||
* keeps key and text in agreement.
|
||||
*/
|
||||
private var committedOwnHeightAslText by mutableStateOf<String?>(null)
|
||||
|
||||
/**
|
||||
* The own ground elevation as its field shows it: empty while the elevation model
|
||||
* decides. The spelling of the last commit while it still names the drafted number,
|
||||
* otherwise that number itself (discarded, applied elsewhere, or never committed here).
|
||||
* Written only through [commitOwnHeightAsl].
|
||||
*/
|
||||
val stn_pathAnalysisOwnHeightAslText: String
|
||||
get() {
|
||||
val drafted = stn_pathAnalysisOwnHeightAslMeters.let { if (it.isFinite()) it.toString() else "" }
|
||||
val committed = committedOwnHeightAslText
|
||||
|
||||
return if (committed != null && isOwnHeightAslUnchanged(committed, drafted)) committed else drafted
|
||||
}
|
||||
|
||||
/**
|
||||
* Commits an own ground elevation entry into the draft. Empty hands the elevation back
|
||||
* to the model; a number in range is staged; anything else is refused and nothing is
|
||||
* staged.
|
||||
*
|
||||
* @return null when staged, otherwise the reason shown to the operator
|
||||
*/
|
||||
fun commitOwnHeightAsl(entered: String): String? {
|
||||
val meters = parseOwnHeightAsl(entered)
|
||||
?: return CurrentStrings.get().stationInvalidOwnHeightAsl(entered.trim())
|
||||
|
||||
stn_pathAnalysisOwnHeightAslMeters = meters
|
||||
committedOwnHeightAslText = entered.trim()
|
||||
return null
|
||||
}
|
||||
|
||||
var stn_pstRotatorEnabled: Boolean by draft.setting({ prefs.isStn_pstRotatorEnabled() }) { prefs.setStn_pstRotatorEnabled(it) }
|
||||
|
||||
var stn_pstRotatorHost: String by draft.setting({ prefs.getStn_pstRotatorHost() }) { prefs.setStn_pstRotatorHost(it) }
|
||||
@@ -176,4 +233,31 @@ class StationTabState(
|
||||
loginChatCategorySecond = category
|
||||
applySecondCategory(category)
|
||||
}
|
||||
|
||||
companion object {
|
||||
/**
|
||||
* Reads an own ground elevation entry: empty means "elevation model" (NaN), a
|
||||
* number in the accepted range is taken, anything else is refused with null.
|
||||
* A comma is read as the decimal separator, as in the other number fields.
|
||||
*/
|
||||
fun parseOwnHeightAsl(text: String): Double? {
|
||||
val trimmed = text.trim()
|
||||
if (trimmed.isEmpty()) {
|
||||
return Double.NaN
|
||||
}
|
||||
val meters = trimmed.replace(',', '.').toDoubleOrNull() ?: return null
|
||||
return if (ChatPreferences.isValidOwnHeightAslMeters(meters)) meters else null
|
||||
}
|
||||
|
||||
/**
|
||||
* Whether a draft names the stored height. Compared as numbers, so "250", "250.0"
|
||||
* and "250,0" all count as the stored 250.0, and an empty draft as the unset value;
|
||||
* a draft that does not parse is never unchanged, so leaving the field refuses it.
|
||||
*/
|
||||
fun isOwnHeightAslUnchanged(draft: String, stored: String): Boolean {
|
||||
val typed = parseOwnHeightAsl(draft) ?: return false
|
||||
val now = parseOwnHeightAsl(stored) ?: return false
|
||||
return if (typed.isNaN() || now.isNaN()) typed.isNaN() && now.isNaN() else typed == now
|
||||
}
|
||||
}
|
||||
}
|
||||
Reference in new issue
Block a user