GPS-filter mot jitter: fartgrind (Doppler), stillastående-filter, baslinje-reset
All checks were successful
release / build-release (push) Successful in 4m19s
All checks were successful
release / build-release (push) Successful in 4m19s
Användarens idé i sensorriktig variant: GPS:ens egen Doppler-fartmätning (stabilare än positionerna) används som referens — - Fartgrind: positionshopp som kräver > faktor × Doppler-fart förkastas (t.ex. 100 m-hopp vid gångfart). Efter 4 förkastade i rad sätts ny baslinje (tunnel/underfart) utan att distans räknas. - Stillastående-filter: vandring < max(2 m, halva noggrannheten) räknas varken som distans eller spårpunkt — ingen fejkdistans när man står still. - Allt loggas i tekniska loggen (hopp, tillåten fart, förkastanden). Justerbart i Inställningar → GPS-spårning: noggrannhetsgräns (35 m), fartfaktor (2,0×), fartgolv (12 km/h). Version 0.8.3. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01C2GA94E1f3cmrvdQLQhYdg
This commit is contained in:
@@ -34,6 +34,12 @@ data class AppSettings(
|
||||
val isFemale: Boolean? = null,
|
||||
/** Visa teknisk logg på spårskärmen (felsökning) */
|
||||
val trackLogEnabled: Boolean = false,
|
||||
/** GPS-filter: förkasta fixar med sämre noggrannhet än detta (meter) */
|
||||
val gpsAccuracyLimitM: Int = 35,
|
||||
/** GPS-filter: max tillåten positionsfart som multipel av Doppler-farten */
|
||||
val gpsSpeedFactor: Double = 2.0,
|
||||
/** GPS-filter: fartgolv (km/h) när Doppler-fart saknas/är noll */
|
||||
val gpsSpeedFloorKmh: Int = 12,
|
||||
)
|
||||
|
||||
class SettingsStore(private val context: Context) {
|
||||
@@ -61,9 +67,24 @@ class SettingsStore(private val context: Context) {
|
||||
birthYear = prefs[KEY_BIRTH_YEAR],
|
||||
isFemale = prefs[KEY_IS_FEMALE]?.toBooleanStrictOrNull(),
|
||||
trackLogEnabled = prefs[KEY_TRACK_LOG]?.toBooleanStrictOrNull() ?: false,
|
||||
gpsAccuracyLimitM = prefs[KEY_GPS_ACC] ?: 35,
|
||||
gpsSpeedFactor = prefs[KEY_GPS_FACTOR]?.toDoubleOrNull() ?: 2.0,
|
||||
gpsSpeedFloorKmh = prefs[KEY_GPS_FLOOR] ?: 12,
|
||||
)
|
||||
}
|
||||
|
||||
suspend fun setGpsAccuracyLimit(value: Int) {
|
||||
context.settingsDataStore.edit { it[KEY_GPS_ACC] = value.coerceIn(10, 100) }
|
||||
}
|
||||
|
||||
suspend fun setGpsSpeedFactor(value: Double) {
|
||||
context.settingsDataStore.edit { it[KEY_GPS_FACTOR] = value.coerceIn(1.2, 5.0).toString() }
|
||||
}
|
||||
|
||||
suspend fun setGpsSpeedFloor(value: Int) {
|
||||
context.settingsDataStore.edit { it[KEY_GPS_FLOOR] = value.coerceIn(4, 40) }
|
||||
}
|
||||
|
||||
suspend fun setTrackLogEnabled(value: Boolean) {
|
||||
context.settingsDataStore.edit { it[KEY_TRACK_LOG] = value.toString() }
|
||||
}
|
||||
@@ -140,5 +161,8 @@ class SettingsStore(private val context: Context) {
|
||||
private val KEY_BIRTH_YEAR = intPreferencesKey("birth_year")
|
||||
private val KEY_IS_FEMALE = stringPreferencesKey("is_female")
|
||||
private val KEY_TRACK_LOG = stringPreferencesKey("track_log_enabled")
|
||||
private val KEY_GPS_ACC = intPreferencesKey("gps_accuracy_limit_m")
|
||||
private val KEY_GPS_FACTOR = stringPreferencesKey("gps_speed_factor")
|
||||
private val KEY_GPS_FLOOR = intPreferencesKey("gps_speed_floor_kmh")
|
||||
}
|
||||
}
|
||||
|
||||
@@ -69,6 +69,13 @@ class TrackingService : Service(), LocationListener {
|
||||
private var weightKg: Double? = null
|
||||
private var activeSinceElapsed = 0L
|
||||
private var lastLocation: Location? = null
|
||||
// GPS-filter (justerbara i inställningarna, skickas med vid start)
|
||||
private var accuracyLimitM = 35f
|
||||
private var speedFactor = 2.0
|
||||
private var speedFloorMs = 12.0 / 3.6
|
||||
// Glidande Doppler-fart (m/s) — GPS:ens egen fartmätning, stabilare än positionerna
|
||||
private var dopplerEmaMs = 0.0
|
||||
private var rejectStreak = 0
|
||||
// Glidande medel av höjd för att filtrera GPS-brus innan höjdmeter summeras
|
||||
private val altitudeWindow = ArrayDeque<Double>()
|
||||
private var smoothedAltitude: Double? = null
|
||||
@@ -99,6 +106,11 @@ class TrackingService : Service(), LocationListener {
|
||||
private fun startTracking(intent: Intent) {
|
||||
val distanceBased = intent.getBooleanExtra(EXTRA_DISTANCE_BASED, false)
|
||||
weightKg = intent.getDoubleExtra(EXTRA_WEIGHT_KG, -1.0).takeIf { it > 0 }
|
||||
accuracyLimitM = intent.getIntExtra(EXTRA_GPS_ACC_LIMIT, 35).toFloat()
|
||||
speedFactor = intent.getDoubleExtra(EXTRA_GPS_SPEED_FACTOR, 2.0)
|
||||
speedFloorMs = intent.getIntExtra(EXTRA_GPS_SPEED_FLOOR, 12) / 3.6
|
||||
dopplerEmaMs = 0.0
|
||||
rejectStreak = 0
|
||||
|
||||
_state.value = TrackingState(
|
||||
isActive = true,
|
||||
@@ -116,6 +128,12 @@ class TrackingService : Service(), LocationListener {
|
||||
"start: ${_state.value.typeName} (distans=$distanceBased, vikt=${weightKg ?: "?"} kg)" +
|
||||
if (distanceBased) " — väntar på GPS-fix" else ""
|
||||
)
|
||||
if (distanceBased) {
|
||||
TrackLog.log(
|
||||
"filter: acc≤${accuracyLimitM.toInt()} m, fartgrind ${speedFactor}× Doppler " +
|
||||
"(golv ${"%.0f".format(speedFloorMs * 3.6)} km/h)"
|
||||
)
|
||||
}
|
||||
|
||||
val type = if (distanceBased) {
|
||||
ServiceInfo.FOREGROUND_SERVICE_TYPE_LOCATION
|
||||
@@ -217,11 +235,45 @@ class TrackingService : Service(), LocationListener {
|
||||
// Position för kartan uppdateras alltid, oavsett kvalitet
|
||||
_state.value = _state.value.copy(currentPosition = location.latitude to location.longitude)
|
||||
|
||||
// Spårpunkter kräver hygglig noggrannhet
|
||||
if (location.hasAccuracy() && location.accuracy > 35f) {
|
||||
TrackLog.log("${location.provider}: förkastad för spåret, acc=${"%.0f".format(acc)} m (>35)")
|
||||
// Grind 1: spårpunkter kräver hygglig noggrannhet
|
||||
if (location.hasAccuracy() && location.accuracy > accuracyLimitM) {
|
||||
TrackLog.log("${location.provider}: förkastad, acc=${"%.0f".format(acc)} m (>${accuracyLimitM.toInt()})")
|
||||
return
|
||||
}
|
||||
|
||||
// Uppdatera glidande Doppler-fart (GPS:ens fartmätning via frekvensskift)
|
||||
if (location.hasSpeed()) {
|
||||
dopplerEmaMs = if (dopplerEmaMs == 0.0) location.speed.toDouble()
|
||||
else 0.7 * dopplerEmaMs + 0.3 * location.speed
|
||||
}
|
||||
|
||||
// Grind 2 (fartgrinden): en ny position som skulle kräva högre fart än
|
||||
// speedFactor × Doppler-farten är orimlig — GPS-hopp, inte förflyttning.
|
||||
lastLocation?.let { prev ->
|
||||
val dtSec = (location.elapsedRealtimeNanos - prev.elapsedRealtimeNanos) / 1e9
|
||||
if (dtSec > 0.3) {
|
||||
val jump = prev.distanceTo(location).toDouble()
|
||||
val impliedMs = jump / dtSec
|
||||
val allowedMs = maxOf(speedFloorMs, dopplerEmaMs * speedFactor)
|
||||
if (impliedMs > allowedMs) {
|
||||
rejectStreak++
|
||||
TrackLog.log(
|
||||
"${location.provider}: fartgrind — hopp ${"%.0f".format(jump)} m på ${"%.1f".format(dtSec)} s " +
|
||||
"= ${"%.1f".format(impliedMs * 3.6)} km/h (tillåtet ${"%.1f".format(allowedMs * 3.6)}), förkastad ($rejectStreak)"
|
||||
)
|
||||
// Många orimliga i rad = det är NYA positionen som är sann
|
||||
// (t.ex. efter tunnel) — sätt ny baslinje utan att räkna distans.
|
||||
if (rejectStreak >= 4) {
|
||||
TrackLog.log("fartgrind: ny baslinje efter $rejectStreak förkastade")
|
||||
lastLocation = location
|
||||
rejectStreak = 0
|
||||
}
|
||||
return
|
||||
}
|
||||
}
|
||||
}
|
||||
rejectStreak = 0
|
||||
|
||||
TrackLog.log(
|
||||
"${location.provider}: fix acc=${"%.0f".format(acc)} m" +
|
||||
" alt=${if (location.hasAltitude()) "%.0f".format(location.altitude) else "?"}" +
|
||||
@@ -232,10 +284,13 @@ class TrackingService : Service(), LocationListener {
|
||||
var distance = s.distanceMeters
|
||||
var elevGain = s.elevationGainMeters
|
||||
|
||||
var acceptedAsMovement = true
|
||||
lastLocation?.let { prev ->
|
||||
val d = prev.distanceTo(location).toDouble()
|
||||
// Ignorera mikrobrus när man står stilla
|
||||
if (d >= 2.0) distance += d
|
||||
// Grind 3 (stillastående): vandring mindre än halva noggrannheten
|
||||
// är brus, inte förflyttning — räkna varken distans eller spårpunkt.
|
||||
val minMove = maxOf(2.0, (if (location.hasAccuracy()) location.accuracy else 5f) * 0.5)
|
||||
if (d >= minMove) distance += d else acceptedAsMovement = false
|
||||
}
|
||||
|
||||
if (location.hasAltitude()) {
|
||||
@@ -257,7 +312,9 @@ class TrackingService : Service(), LocationListener {
|
||||
_state.value = cur.copy(
|
||||
distanceMeters = distance,
|
||||
elevationGainMeters = elevGain,
|
||||
points = cur.points + (location.latitude to location.longitude),
|
||||
points = if (acceptedAsMovement || cur.points.isEmpty()) {
|
||||
cur.points + (location.latitude to location.longitude)
|
||||
} else cur.points,
|
||||
currentSpeedKmh = if (location.hasSpeed()) location.speed * 3.6 else null,
|
||||
gpsFix = true,
|
||||
)
|
||||
@@ -333,12 +390,22 @@ class TrackingService : Service(), LocationListener {
|
||||
const val EXTRA_MET = "met"
|
||||
const val EXTRA_DISTANCE_BASED = "distanceBased"
|
||||
const val EXTRA_WEIGHT_KG = "weightKg"
|
||||
const val EXTRA_GPS_ACC_LIMIT = "gpsAccLimit"
|
||||
const val EXTRA_GPS_SPEED_FACTOR = "gpsSpeedFactor"
|
||||
const val EXTRA_GPS_SPEED_FLOOR = "gpsSpeedFloor"
|
||||
|
||||
private val _state = MutableStateFlow(TrackingState())
|
||||
/** Läses av UI:t — servicen äger sanningen. */
|
||||
val state: StateFlow<TrackingState> = _state.asStateFlow()
|
||||
|
||||
fun start(context: Context, type: ActivityType, weightKg: Double?) {
|
||||
fun start(
|
||||
context: Context,
|
||||
type: ActivityType,
|
||||
weightKg: Double?,
|
||||
gpsAccuracyLimitM: Int = 35,
|
||||
gpsSpeedFactor: Double = 2.0,
|
||||
gpsSpeedFloorKmh: Int = 12,
|
||||
) {
|
||||
val intent = Intent(context, TrackingService::class.java).apply {
|
||||
putExtra(EXTRA_TYPE_ID, type.id)
|
||||
putExtra(EXTRA_TYPE_KEY, type.key)
|
||||
@@ -347,6 +414,9 @@ class TrackingService : Service(), LocationListener {
|
||||
putExtra(EXTRA_MET, type.met)
|
||||
putExtra(EXTRA_DISTANCE_BASED, type.isDistanceBased)
|
||||
weightKg?.let { putExtra(EXTRA_WEIGHT_KG, it) }
|
||||
putExtra(EXTRA_GPS_ACC_LIMIT, gpsAccuracyLimitM)
|
||||
putExtra(EXTRA_GPS_SPEED_FACTOR, gpsSpeedFactor)
|
||||
putExtra(EXTRA_GPS_SPEED_FLOOR, gpsSpeedFloorKmh)
|
||||
}
|
||||
context.startForegroundService(intent)
|
||||
}
|
||||
|
||||
@@ -73,6 +73,16 @@ class SettingsViewModel(
|
||||
|
||||
fun setTrackLog(value: Boolean) = viewModelScope.launch { store.setTrackLogEnabled(value) }
|
||||
|
||||
fun adjustGpsAccuracy(delta: Int) = viewModelScope.launch {
|
||||
store.setGpsAccuracyLimit(settings.value.gpsAccuracyLimitM + delta)
|
||||
}
|
||||
fun adjustGpsSpeedFactor(delta: Double) = viewModelScope.launch {
|
||||
store.setGpsSpeedFactor(settings.value.gpsSpeedFactor + delta)
|
||||
}
|
||||
fun adjustGpsSpeedFloor(delta: Int) = viewModelScope.launch {
|
||||
store.setGpsSpeedFloor(settings.value.gpsSpeedFloorKmh + delta)
|
||||
}
|
||||
|
||||
fun setAutoRelogin(value: Boolean) = viewModelScope.launch {
|
||||
store.setAutoRelogin(value)
|
||||
// Stängs funktionen av slängs de sparade uppgifterna direkt.
|
||||
@@ -147,6 +157,41 @@ fun SettingsScreen(
|
||||
}
|
||||
}
|
||||
|
||||
SettingsCard(title = "GPS-spårning") {
|
||||
Text(
|
||||
"Filter mot GPS-brus i aktivitetsspårningen. Fartgrinden förkastar " +
|
||||
"positionshopp som skulle kräva högre fart än faktorn × GPS:ens " +
|
||||
"egen fartmätning (Doppler).",
|
||||
style = MaterialTheme.typography.bodySmall,
|
||||
color = MaterialTheme.colorScheme.onSurfaceVariant,
|
||||
modifier = Modifier.padding(horizontal = 16.dp, vertical = 4.dp),
|
||||
)
|
||||
StepperRow(
|
||||
label = "Noggrannhetsgräns",
|
||||
value = "${settings.gpsAccuracyLimitM} m",
|
||||
onMinus = { viewModel.adjustGpsAccuracy(-5) },
|
||||
onPlus = { viewModel.adjustGpsAccuracy(5) },
|
||||
)
|
||||
StepperRow(
|
||||
label = "Fartgrind (× uppmätt fart)",
|
||||
value = "${settings.gpsSpeedFactor}×",
|
||||
onMinus = { viewModel.adjustGpsSpeedFactor(-0.5) },
|
||||
onPlus = { viewModel.adjustGpsSpeedFactor(0.5) },
|
||||
)
|
||||
StepperRow(
|
||||
label = "Fartgolv",
|
||||
value = "${settings.gpsSpeedFloorKmh} km/h",
|
||||
onMinus = { viewModel.adjustGpsSpeedFloor(-2) },
|
||||
onPlus = { viewModel.adjustGpsSpeedFloor(2) },
|
||||
)
|
||||
Text(
|
||||
"Gäller från nästa startade aktivitet. Standard: 35 m / 2,0× / 12 km/h.",
|
||||
style = MaterialTheme.typography.bodySmall,
|
||||
color = MaterialTheme.colorScheme.onSurfaceVariant,
|
||||
modifier = Modifier.padding(horizontal = 16.dp, vertical = 4.dp),
|
||||
)
|
||||
}
|
||||
|
||||
SettingsCard(title = "Felsökning") {
|
||||
Row(
|
||||
modifier = Modifier
|
||||
@@ -378,6 +423,30 @@ private fun SettingsCard(title: String, content: @Composable () -> Unit) {
|
||||
}
|
||||
}
|
||||
|
||||
@Composable
|
||||
private fun StepperRow(
|
||||
label: String,
|
||||
value: String,
|
||||
onMinus: () -> Unit,
|
||||
onPlus: () -> Unit,
|
||||
) {
|
||||
Row(
|
||||
modifier = Modifier
|
||||
.fillMaxWidth()
|
||||
.padding(horizontal = 16.dp, vertical = 2.dp),
|
||||
verticalAlignment = Alignment.CenterVertically,
|
||||
) {
|
||||
Text(label, modifier = Modifier.weight(1f), style = MaterialTheme.typography.bodyLarge)
|
||||
FilledTonalButton(onClick = onMinus) { Text("−") }
|
||||
Text(
|
||||
value,
|
||||
modifier = Modifier.padding(horizontal = 10.dp),
|
||||
style = MaterialTheme.typography.titleSmall,
|
||||
)
|
||||
FilledTonalButton(onClick = onPlus) { Text("+") }
|
||||
}
|
||||
}
|
||||
|
||||
@Composable
|
||||
private fun RadioRow(label: String, description: String, selected: Boolean, onClick: () -> Unit) {
|
||||
Row(
|
||||
|
||||
@@ -67,6 +67,7 @@ import eu.brassepc.fitnessdroid.ui.appContainer
|
||||
import eu.brassepc.fitnessdroid.ui.common.compact
|
||||
import eu.brassepc.fitnessdroid.ui.common.openAppSettings
|
||||
import kotlinx.coroutines.flow.MutableStateFlow
|
||||
import kotlinx.coroutines.flow.first
|
||||
import kotlinx.coroutines.flow.map
|
||||
import kotlinx.coroutines.launch
|
||||
import org.osmdroid.config.Configuration
|
||||
@@ -80,7 +81,7 @@ import java.time.Instant
|
||||
class TrackViewModel(
|
||||
private val gymApi: GymApi,
|
||||
private val repo: GymRepository,
|
||||
settingsStore: eu.brassepc.fitnessdroid.data.SettingsStore,
|
||||
private val settingsStore: eu.brassepc.fitnessdroid.data.SettingsStore,
|
||||
) : ViewModel() {
|
||||
val tracking = TrackingService.state
|
||||
/** Teknisk logg visas bara om den slagits på i inställningarna. */
|
||||
@@ -110,7 +111,13 @@ class TrackViewModel(
|
||||
fun start(context: android.content.Context) {
|
||||
val type = pendingType.value ?: return
|
||||
viewModelScope.launch {
|
||||
TrackingService.start(context, type, repo.bodyWeightKg())
|
||||
val cfg = settingsStore.settings.first()
|
||||
TrackingService.start(
|
||||
context, type, repo.bodyWeightKg(),
|
||||
gpsAccuracyLimitM = cfg.gpsAccuracyLimitM,
|
||||
gpsSpeedFactor = cfg.gpsSpeedFactor,
|
||||
gpsSpeedFloorKmh = cfg.gpsSpeedFloorKmh,
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
Reference in New Issue
Block a user