1
0
mirror of https://github.com/oliexdev/openScale.git synced 2025-08-24 17:23:03 +02:00

When calculating derived values, the code now fetches the unit from the MeasurementType and converts the input values (e.g., weight to KG, waist to CM) before passing them to the calculation functions. This ensures calculations are performed with consistent units.

This commit is contained in:
oliexdev
2025-08-14 12:02:28 +02:00
parent 994361ad5c
commit 28725e9ba3
2 changed files with 176 additions and 82 deletions

View File

@@ -19,13 +19,17 @@ package com.health.openscale.core.database
import com.health.openscale.core.data.ActivityLevel
import com.health.openscale.core.data.GenderType
import com.health.openscale.core.data.MeasureUnit
import com.health.openscale.core.data.Measurement
import com.health.openscale.core.data.MeasurementType
import com.health.openscale.core.data.MeasurementTypeKey
import com.health.openscale.core.data.MeasurementValue
import com.health.openscale.core.data.UnitType
import com.health.openscale.core.data.User
import com.health.openscale.core.data.WeightUnit
import com.health.openscale.core.model.MeasurementWithValues
import com.health.openscale.core.utils.CalculationUtil
import com.health.openscale.core.utils.Converters
import com.health.openscale.core.utils.LogManager
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.flow.Flow
@@ -220,8 +224,9 @@ class DatabaseRepository(
}
val userId = measurement.userId
// Fetch all current values for this specific measurement and all global MeasurementType definitions
val currentMeasurementValues = measurementValueDao.getValuesForMeasurement(measurementId).first()
val allGlobalTypes = measurementTypeDao.getAll().first()
val allGlobalTypes = measurementTypeDao.getAll().first() // These are MeasurementType objects, containing unit info
val user = userDao.getById(userId).first() ?: run {
LogManager.w(DERIVED_VALUES_TAG, "User with ID $userId not found for measurement $measurementId. Cannot recalculate derived values.")
return
@@ -230,16 +235,22 @@ class DatabaseRepository(
LogManager.d(DERIVED_VALUES_TAG, "Fetched ${currentMeasurementValues.size} current values, " +
"${allGlobalTypes.size} global types, and user '${user.name}' for measurement $measurementId.")
val findValue = { key: MeasurementTypeKey ->
val type = allGlobalTypes.find { it.key == key }
if (type == null) {
// Helper to find a raw value and its unit from the persisted MeasurementValues and MeasurementTypes
val findValueAndUnit = { key: MeasurementTypeKey ->
val measurementTypeObject = allGlobalTypes.find { it.key == key }
if (measurementTypeObject == null) {
LogManager.w(DERIVED_VALUES_TAG, "MeasurementType for key '$key' not found in global types list.")
Pair(null, null) // Return nulls if the type definition is missing
} else {
val valueObject = currentMeasurementValues.find { it.typeId == measurementTypeObject.id }
val value = valueObject?.floatValue
val unit = measurementTypeObject.unit // The unit is defined in the MeasurementType object
LogManager.v(DERIVED_VALUES_TAG, "findValueAndUnit for $key (typeId: ${measurementTypeObject.id}, unit: $unit): ${value ?: "not found"}")
Pair(value, unit)
}
val value = currentMeasurementValues.find { it.typeId == type?.id }?.floatValue
LogManager.v(DERIVED_VALUES_TAG, "findValue for $key (typeId: ${type?.id}): ${value ?: "not found"}")
value
}
// Helper to save or update a derived measurement value
val saveOrUpdateDerivedValue: suspend (value: Float?, typeKey: MeasurementTypeKey) -> Unit =
save@{ derivedValue, derivedValueTypeKey ->
val derivedTypeObject = allGlobalTypes.find { it.key == derivedValueTypeKey }
@@ -252,6 +263,7 @@ class DatabaseRepository(
val existingDerivedValueObject = currentMeasurementValues.find { it.typeId == derivedTypeObject.id }
if (derivedValue == null) {
// If derived value is null, delete any existing persisted value for it
if (existingDerivedValueObject != null) {
measurementValueDao.deleteById(existingDerivedValueObject.id)
LogManager.d(DERIVED_VALUES_TAG, "Derived value for key ${derivedTypeObject.key} is null. Deleted existing value (ID: ${existingDerivedValueObject.id}).")
@@ -259,7 +271,8 @@ class DatabaseRepository(
LogManager.v(DERIVED_VALUES_TAG, "Derived value for key ${derivedTypeObject.key} is null. No existing value to delete.")
}
} else {
val roundedValue = roundTo(derivedValue)
// If derived value is not null, insert or update it
val roundedValue = roundTo(derivedValue) // Apply rounding
if (existingDerivedValueObject != null) {
if (existingDerivedValueObject.floatValue != roundedValue) {
measurementValueDao.update(existingDerivedValueObject.copy(floatValue = roundedValue))
@@ -280,20 +293,93 @@ class DatabaseRepository(
}
}
val weightKg = findValue(MeasurementTypeKey.WEIGHT)
val bodyFatPercentage = findValue(MeasurementTypeKey.BODY_FAT)
val waistCm = findValue(MeasurementTypeKey.WAIST)
val hipsCm = findValue(MeasurementTypeKey.HIPS)
val caliper1Cm = findValue(MeasurementTypeKey.CALIPER_1)
val caliper2Cm = findValue(MeasurementTypeKey.CALIPER_2)
val caliper3Cm = findValue(MeasurementTypeKey.CALIPER_3)
// Fetch raw values and their original units
val (weightValue, weightUnitType) = findValueAndUnit(MeasurementTypeKey.WEIGHT)
val (bodyFatValue, _) = findValueAndUnit(MeasurementTypeKey.BODY_FAT) // Unit usually % (UnitType.PERCENT)
val (waistValue, waistUnitType) = findValueAndUnit(MeasurementTypeKey.WAIST)
val (hipsValue, hipsUnitType) = findValueAndUnit(MeasurementTypeKey.HIPS)
val (caliper1Value, caliper1UnitType) = findValueAndUnit(MeasurementTypeKey.CALIPER_1)
val (caliper2Value, caliper2UnitType) = findValueAndUnit(MeasurementTypeKey.CALIPER_2)
val (caliper3Value, caliper3UnitType) = findValueAndUnit(MeasurementTypeKey.CALIPER_3)
processBmiCalculation(weightKg, user.heightCm).also { saveOrUpdateDerivedValue(it, MeasurementTypeKey.BMI) }
// --- CONVERT VALUES TO REQUIRED UNITS FOR CALCULATIONS ---
// Convert weight to Kilograms (KG)
val weightKg: Float? = if (weightValue != null && weightUnitType != null) {
when (weightUnitType) {
UnitType.KG -> weightValue
UnitType.LB -> Converters.toKilogram(weightValue, WeightUnit.LB)
UnitType.ST -> Converters.toKilogram(weightValue, WeightUnit.ST)
else -> {
LogManager.w(DERIVED_VALUES_TAG, "Unsupported unit $weightUnitType for weight conversion. Assuming KG if value present for ${MeasurementTypeKey.WEIGHT}.")
weightValue // Fallback or handle error appropriately
}
}
} else null
// Body fat is typically already in percentage
val bodyFatPercentage: Float? = bodyFatValue
// Convert waist circumference to Centimeters (CM)
val waistCm: Float? = if (waistValue != null && waistUnitType != null) {
when (waistUnitType) {
UnitType.CM -> waistValue
UnitType.INCH -> Converters.toCentimeter(waistValue, MeasureUnit.INCH)
else -> {
LogManager.w(DERIVED_VALUES_TAG, "Unsupported unit $waistUnitType for waist conversion. Assuming CM if value present for ${MeasurementTypeKey.WAIST}.")
waistValue
}
}
} else null
// Convert hips circumference to Centimeters (CM)
val hipsCm: Float? = if (hipsValue != null && hipsUnitType != null) {
when (hipsUnitType) {
UnitType.CM -> hipsValue
UnitType.INCH -> Converters.toCentimeter(hipsValue, MeasureUnit.INCH)
else -> {
LogManager.w(DERIVED_VALUES_TAG, "Unsupported unit $hipsUnitType for hips conversion. Assuming CM if value present for ${MeasurementTypeKey.HIPS}.")
hipsValue
}
}
} else null
// Convert caliper measurements to Centimeters (CM)
val caliper1Cm: Float? = if (caliper1Value != null && caliper1UnitType != null) {
when (caliper1UnitType) {
UnitType.CM -> caliper1Value
UnitType.INCH -> Converters.toCentimeter(caliper1Value, MeasureUnit.INCH)
else -> caliper1Value // Fallback
}
} else null
val caliper2Cm: Float? = if (caliper2Value != null && caliper2UnitType != null) {
when (caliper2UnitType) {
UnitType.CM -> caliper2Value
UnitType.INCH -> Converters.toCentimeter(caliper2Value, MeasureUnit.INCH)
else -> caliper2Value
}
} else null
val caliper3Cm: Float? = if (caliper3Value != null && caliper3UnitType != null) {
when (caliper3UnitType) {
UnitType.CM -> caliper3Value
UnitType.INCH -> Converters.toCentimeter(caliper3Value, MeasureUnit.INCH)
else -> caliper3Value
}
} else null
// User's height is assumed to be stored in CM in the User object
val userHeightCm = user.heightCm
// --- PERFORM DERIVED VALUE CALCULATIONS ---
// Pass the converted values (e.g., weightKg, waistCm) to the processing functions
processBmiCalculation(weightKg, userHeightCm).also { saveOrUpdateDerivedValue(it, MeasurementTypeKey.BMI) }
processLbmCalculation(weightKg, bodyFatPercentage).also { saveOrUpdateDerivedValue(it, MeasurementTypeKey.LBM) }
processWhrCalculation(waistCm, hipsCm).also { saveOrUpdateDerivedValue(it, MeasurementTypeKey.WHR) }
processWhtrCalculation(waistCm, user.heightCm).also { saveOrUpdateDerivedValue(it, MeasurementTypeKey.WHTR) }
processBmrCalculation(weightKg, user).also { bmr ->
processWhtrCalculation(waistCm, userHeightCm).also { saveOrUpdateDerivedValue(it, MeasurementTypeKey.WHTR) }
processBmrCalculation(weightKg, user).also { bmr -> // user object contains heightCm and other necessary details
saveOrUpdateDerivedValue(bmr, MeasurementTypeKey.BMR)
// TDEE calculation depends on the BMR result
processTDEECalculation(bmr, user.activityLevel).also { saveOrUpdateDerivedValue(it, MeasurementTypeKey.TDEE) }
}
processFatCaliperCalculation(caliper1Cm, caliper2Cm, caliper3Cm, user)

View File

@@ -1158,7 +1158,7 @@ class SettingsViewModel(
*/
fun updateMeasurementTypeAndConvertDataViewModelCentric(
originalType: MeasurementType,
updatedType: MeasurementType,
updatedType: MeasurementType, // This contains the new unit and other proposed changes from the UI
showSnackbarMaster: Boolean = true
) {
viewModelScope.launch {
@@ -1170,14 +1170,54 @@ class SettingsViewModel(
var conversionErrorOccurred = false
var valuesConvertedCount = 0
if (originalType.unit != updatedType.unit && originalType.inputType == InputFieldType.FLOAT && updatedType.inputType == InputFieldType.FLOAT) {
// 1. First, update the MeasurementType definition in the database.
// This ensures that any subsequent recalculations of derived values
// will use the correct (new) unit for this type.
val finalTypeToUpdate = MeasurementType(
id = originalType.id,
key = originalType.key, // Key should be immutable for an existing type
name = updatedType.name,
color = updatedType.color,
icon = updatedType.icon,
unit = updatedType.unit, // Crucial: The new unit
inputType = updatedType.inputType,
displayOrder = originalType.displayOrder,
isDerived = originalType.isDerived,
isEnabled = updatedType.isEnabled,
isPinned = updatedType.isPinned,
isOnRightYAxis = updatedType.isOnRightYAxis
)
try {
repository.updateMeasurementType(finalTypeToUpdate)
LogManager.i(
TAG,
"Unit changed for FLOAT type ID ${originalType.id} from ${originalType.unit} to ${updatedType.unit}. Converting values."
"MeasurementType (ID: ${originalType.id}) definition updated successfully to new unit '${finalTypeToUpdate.unit}'."
)
} catch (e: Exception) {
LogManager.e(TAG, "Error updating MeasurementType (ID: ${originalType.id}) definition itself", e)
sharedViewModel.showSnackbar(
messageResId = R.string.measurement_type_updated_error,
// Consider using context.getString for display names if not available in originalType
formatArgs = listOf(originalType.name ?: originalType.key.toString())
)
conversionErrorOccurred = true // Prevent further steps if this critical update fails
}
// 2. If the type definition was updated successfully AND the unit has changed for a FLOAT type,
// convert the associated MeasurementValue entries.
if (!conversionErrorOccurred &&
originalType.unit != updatedType.unit &&
originalType.inputType == InputFieldType.FLOAT &&
updatedType.inputType == InputFieldType.FLOAT
) {
LogManager.i(
TAG,
"Unit changed for FLOAT type ID ${originalType.id}. Converting values AFTER type definition update."
)
try {
val valuesToConvert = repository.getValuesForType(originalType.id).first() // .first() um den aktuellen Wert des Flows zu erhalten
// Fetch all values belonging to this type
val valuesToConvert = repository.getValuesForType(originalType.id).first()
if (valuesToConvert.isNotEmpty()) {
LogManager.d(TAG, "Found ${valuesToConvert.size} values of type ID ${originalType.id} to potentially convert.")
@@ -1185,13 +1225,14 @@ class SettingsViewModel(
valuesToConvert.forEach { valueToConvert ->
valueToConvert.floatValue?.let { currentFloatVal ->
// Convert from the original unit of the values to the new target unit
val convertedFloat = Converters.convertFloatValueUnit(
value = currentFloatVal,
fromUnit = originalType.unit,
toUnit = updatedType.unit
fromUnit = originalType.unit, // Important: Use the unit the values currently have
toUnit = updatedType.unit // The new target unit
)
if (convertedFloat != currentFloatVal) {
if (convertedFloat != currentFloatVal) { // Add only if the value actually changes
updatedValuesBatch.add(valueToConvert.copy(floatValue = convertedFloat))
valuesConvertedCount++
}
@@ -1200,9 +1241,8 @@ class SettingsViewModel(
if (updatedValuesBatch.isNotEmpty()) {
LogManager.d(TAG, "Updating ${updatedValuesBatch.size} values in batch for type ID ${originalType.id}.")
// Aktualisiere jeden Wert einzeln. Dein Repository.updateMeasurementValue
// stößt die Neuberechnung der abgeleiteten Werte an.
updatedValuesBatch.forEach { repository.updateMeasurementValue(it) }
// Consider a repository.justUpdateMeasurementValueData(it)
LogManager.d(TAG, "Batch update of ${updatedValuesBatch.size} values completed for type ID ${originalType.id}.")
} else {
LogManager.i(TAG, "No values required actual conversion or update for type ID ${originalType.id} after checking.")
@@ -1213,74 +1253,42 @@ class SettingsViewModel(
} catch (e: Exception) {
LogManager.e(TAG, "Error during value conversion/update for type ID ${originalType.id}", e)
conversionErrorOccurred = true
// Verwende hier getDisplayName vom originalType, da updatedType möglicherweise noch nicht committet wurde.
sharedViewModel.showSnackbar(
messageResId = R.string.measurement_type_update_error_conversion_failed,
formatArgs = listOf(originalType.name ?: originalType.key.toString())
)
// Optional: Consider reverting the MeasurementType update if value conversion fails (adds complexity).
}
} else if (originalType.unit != updatedType.unit) {
} else if (!conversionErrorOccurred && originalType.unit != updatedType.unit) {
LogManager.i(
TAG,
"Unit changed for type ID ${originalType.id}, but InputType is not FLOAT (Original: ${originalType.inputType}, Updated: ${updatedType.inputType}). No value conversion performed."
"Unit changed for type ID ${originalType.id}, but InputType is not FLOAT or previous type update failed. " +
"No direct value conversion, but affected measurements will be flagged for recalculation."
)
}
if (!conversionErrorOccurred) {
try {
val finalTypeToUpdate = MeasurementType(
id = originalType.id,
key = originalType.key,
name = updatedType.name,
color = updatedType.color,
icon = updatedType.icon,
unit = updatedType.unit,
inputType = updatedType.inputType,
displayOrder = originalType.displayOrder,
isDerived = originalType.isDerived,
isEnabled = updatedType.isEnabled,
isPinned = updatedType.isPinned,
isOnRightYAxis = updatedType.isOnRightYAxis
)
repository.updateMeasurementType(finalTypeToUpdate)
LogManager.i(
TAG,
"MeasurementType (ID: ${originalType.id}) updated successfully to new unit '${finalTypeToUpdate.unit}'."
)
if (showSnackbarMaster) {
if (valuesConvertedCount > 0) {
sharedViewModel.showSnackbar(
messageResId = R.string.measurement_type_updated_and_values_converted_successfully,
formatArgs = listOf(updatedType.name ?: updatedType.key.toString(), valuesConvertedCount.toString()) // Context für getDisplayName wäre besser
)
} else if (originalType.unit != updatedType.unit && originalType.inputType == InputFieldType.FLOAT) {
sharedViewModel.showSnackbar(
messageResId = R.string.measurement_type_updated_unit_changed_no_values_converted,
formatArgs = listOf(updatedType.name ?: updatedType.key.toString()) // Context für getDisplayName wäre besser
)
}
else {
sharedViewModel.showSnackbar(
messageResId = R.string.measurement_type_updated_successfully,
formatArgs = listOf(updatedType.name ?: updatedType.key.toString()) // Context für getDisplayName wäre besser
)
}
}
// sharedViewModel.refreshMeasurementTypes() // Optional, wenn die Liste sich nicht automatisch aktualisiert
} catch (e: Exception) {
LogManager.e(TAG, "Error updating MeasurementType (ID: ${originalType.id}) itself", e)
// 3. Show appropriate snackbar message.
if (!conversionErrorOccurred && showSnackbarMaster) {
if (valuesConvertedCount > 0) {
sharedViewModel.showSnackbar(
messageResId = R.string.measurement_type_updated_error,
formatArgs = listOf(originalType.name ?: originalType.key.toString()) // Context für getDisplayName wäre besser
messageResId = R.string.measurement_type_updated_and_values_converted_successfully,
formatArgs = listOf(
updatedType.name ?: updatedType.key.toString(),
valuesConvertedCount.toString()
)
)
} else if (originalType.unit != updatedType.unit && (originalType.inputType == InputFieldType.FLOAT)) {
// Also show if unit changed but no values were converted (e.g., none existed or input type wasn't FLOAT but recalculation was still triggered)
sharedViewModel.showSnackbar(
messageResId = R.string.measurement_type_updated_unit_changed_no_values_converted, // Or a more specific message
formatArgs = listOf(updatedType.name ?: updatedType.key.toString())
)
} else { // Generic success if no unit change or no conversion needed
sharedViewModel.showSnackbar(
messageResId = R.string.measurement_type_updated_successfully,
formatArgs = listOf(updatedType.name ?: updatedType.key.toString())
)
}
} else {
LogManager.w(
TAG,
"Skipped updating MeasurementType definition (ID: ${originalType.id}) due to prior value conversion error."
)
}
}
}