Added normalized time for AOS/LOS, fixed passes refresh

This commit is contained in:
Arty Bishop committed 2026-04-04 18:43:05 +01:00
1 parent b59f9a6c17
commit be96602c76
6 files changed
+118 -91

No files matched your search

@@ -44,6 +44,7 @@ import androidx.compose.material3.pulltorefresh.PullToRefreshBox
import androidx.compose.material3.pulltorefresh.PullToRefreshDefaults
import androidx.compose.material3.pulltorefresh.rememberPullToRefreshState
import androidx.compose.runtime.Composable
import androidx.compose.runtime.remember
import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier
import androidx.compose.ui.res.painterResource
@@ -223,6 +224,10 @@ private fun NearEarthPass(
) {
val passSatId = stringResource(id = R.string.pass_satId, pass.catNum)
val horizontalPadding = if (isVerticalLayout) 6.dp else 10.dp
// Cache formatted date/time strings — aosTime/losTime never change for a given pass
val aosDateStr = remember(pass.aosTime) { sdfDate.format(Date(pass.aosTime)) }
val aosTimeStr = remember(pass.aosTime) { sdfTime.format(Date(pass.aosTime)) }
val losTimeStr = remember(pass.losTime) { sdfTime.format(Date(pass.losTime)) }
Surface(color = MaterialTheme.colorScheme.background, modifier = modifier) {
Surface(modifier = Modifier
.padding(bottom = 2.dp)
@@ -274,7 +279,7 @@ private fun NearEarthPass(
Text(text = stringResource(R.string.pass_deep_space), fontSize = 15.sp)
} else {
Text(
text = sdfDate.format(Date(pass.aosTime)),
text = aosDateStr,
fontSize = 15.sp
)
}
@@ -317,7 +322,7 @@ private fun NearEarthPass(
) {
val defaultTime = " - - : - - "
Text(
text = if (pass.isDeepSpace) defaultTime else sdfTime.format(Date(pass.aosTime)),
text = if (pass.isDeepSpace) defaultTime else aosTimeStr,
fontSize = 15.sp
)
LinearProgressIndicator(
@@ -326,7 +331,7 @@ private fun NearEarthPass(
modifier = modifier.fillMaxWidth(0.75f)
)
Text(
text = if (pass.isDeepSpace) defaultTime else sdfTime.format(Date(pass.losTime)),
text = if (pass.isDeepSpace) defaultTime else losTimeStr,
fontSize = 15.sp
)
}
@@ -27,10 +27,9 @@ import com.rtbishop.look4sat.core.domain.predict.OrbitalPass
import com.rtbishop.look4sat.core.domain.repository.IContainerProvider
import com.rtbishop.look4sat.core.domain.repository.ISatelliteRepo
import com.rtbishop.look4sat.core.domain.repository.ISettingsRepo
import com.rtbishop.look4sat.core.domain.utility.round
import com.rtbishop.look4sat.core.domain.utility.toTimerString
import com.rtbishop.look4sat.core.presentation.getDefaultPass
import kotlinx.coroutines.Job
import kotlinx.coroutines.cancelAndJoin
import kotlinx.coroutines.delay
import kotlinx.coroutines.flow.MutableStateFlow
import kotlinx.coroutines.flow.StateFlow
@@ -44,6 +43,7 @@ class PassesViewModel(
private val settingsRepo: ISettingsRepo
) : ViewModel() {
private val defaultPass = getDefaultPass()
private val _uiState = MutableStateFlow(
PassesState(
isPassesDialogShown = false,
@@ -52,7 +52,7 @@ class PassesViewModel(
isUtc = settingsRepo.otherSettings.value.stateOfUtc,
nextTime = "00:00:00",
isNextTimeAos = true,
nextPass = getDefaultPass(),
nextPass = defaultPass,
hours = settingsRepo.passesSettings.value.hoursAhead,
elevation = settingsRepo.passesSettings.value.minElevation,
modes = settingsRepo.passesSettings.value.selectedModes,
@@ -61,25 +61,36 @@ class PassesViewModel(
takeAction = ::handleAction
)
)
private var processing: Job? = null
val uiState: StateFlow<PassesState> = _uiState
init {
// React to raw pass list changes (initial load, recalculation, filter change)
viewModelScope.launch {
delay(1000)
var initialLoadDone = false
satelliteRepo.passes.collectLatest { passes ->
processing?.cancelAndJoin()
processing = viewModelScope.launch {
while (isActive) {
val timeNow = System.currentTimeMillis()
val newPasses = satelliteRepo.processPasses(passes, timeNow)
setPassInfo(newPasses, timeNow)
_uiState.update { it.copy(isRefreshing = false, itemsList = newPasses) }
delay(1000)
}
if (!initialLoadDone && passes.isNotEmpty()) {
initialLoadDone = true
_uiState.update { it.copy(isRefreshing = false) }
}
}
}
// Local tick loop — computes pass progress and countdown timer every second
viewModelScope.launch {
while (isActive) {
val timeNow = System.currentTimeMillis()
val processed = computePassProgress(satelliteRepo.passes.value, timeNow)
val nextInfo = resolveNextPass(processed, timeNow)
_uiState.update {
it.copy(
itemsList = processed,
nextPass = nextInfo.first,
nextTime = nextInfo.second,
isNextTimeAos = nextInfo.third
)
}
delay(1000)
}
}
viewModelScope.launch {
settingsRepo.otherSettings.collectLatest { settings ->
_uiState.update { it.copy(shouldSeeWhatsNew = settings.shouldSeeWhatsNew) }
@@ -87,6 +98,43 @@ class PassesViewModel(
}
}
/** Computes live progress for each pass, filtering out expired ones. */
private fun computePassProgress(passList: List<OrbitalPass>, time: Long): List<OrbitalPass> {
val result = ArrayList<OrbitalPass>(passList.size)
for (pass in passList) {
if (!pass.isDeepSpace && time > pass.aosTime) {
val deltaNow = time.minus(pass.aosTime).toFloat()
val deltaTotal = pass.losTime.minus(pass.aosTime).toFloat()
val newProgress = (deltaNow / deltaTotal).round(2)
if (newProgress >= 1.0f) continue
if (newProgress != pass.progress) {
result.add(pass.copy(progress = newProgress))
} else {
result.add(pass)
}
} else {
result.add(pass)
}
}
return result
}
/** Resolves the next upcoming or active pass and its countdown timer. */
private fun resolveNextPass(
passes: List<OrbitalPass>,
timeNow: Long
): Triple<OrbitalPass, String, Boolean> {
val upcoming = passes.firstOrNull { it.aosTime.minus(timeNow) > 0 }
if (upcoming != null) {
return Triple(upcoming, upcoming.aosTime.minus(timeNow).toTimerString(), true)
}
if (passes.isNotEmpty()) {
val lastPass = passes.last()
return Triple(lastPass, lastPass.losTime.minus(timeNow).toTimerString(), false)
}
return Triple(defaultPass, "00:00:00", true)
}
private fun handleAction(action: PassesAction) {
when (action) {
PassesAction.DismissWhatsNew -> settingsRepo.setWhatsNewDismissed()
@@ -100,30 +148,17 @@ class PassesViewModel(
private fun applyFilter(hoursAhead: Int, minElevation: Double, modes: List<String>) = viewModelScope.launch {
_uiState.update { it.copy(isRefreshing = true) }
processing?.cancelAndJoin()
settingsRepo.setPassesSettings(PassesSettings(hoursAhead, minElevation, modes))
_uiState.update { it.copy(hours = hoursAhead, elevation = minElevation, modes = modes) }
satelliteRepo.calculatePasses(System.currentTimeMillis(), hoursAhead, minElevation, modes)
_uiState.update { it.copy(isRefreshing = false) }
}
private fun refreshPasses() = viewModelScope.launch {
_uiState.update { it.copy(isRefreshing = true) }
processing?.cancelAndJoin()
val (hoursAhead, minElevation, modes) = settingsRepo.passesSettings.value
satelliteRepo.calculatePasses(System.currentTimeMillis(), hoursAhead, minElevation, modes)
}
private fun setPassInfo(passes: List<OrbitalPass>, timeNow: Long) {
if (passes.isEmpty()) return
try {
val nextPass = passes.first { it.aosTime.minus(timeNow) > 0 }
val time = nextPass.aosTime.minus(timeNow).toTimerString()
_uiState.update { it.copy(nextPass = nextPass, nextTime = time, isNextTimeAos = true) }
} catch (_: NoSuchElementException) {
val lastPass = passes.last()
val time = lastPass.losTime.minus(timeNow).toTimerString()
_uiState.update { it.copy(nextPass = lastPass, nextTime = time, isNextTimeAos = false) }
}
_uiState.update { it.copy(isRefreshing = false) }
}
private fun toggleFilterDialog() {
@@ -26,7 +26,6 @@ import androidx.lifecycle.viewmodel.initializer
import androidx.lifecycle.viewmodel.viewModelFactory
import com.rtbishop.look4sat.core.domain.model.SatRadio
import com.rtbishop.look4sat.core.domain.predict.OrbitalObject
import com.rtbishop.look4sat.core.domain.predict.OrbitalPass
import com.rtbishop.look4sat.core.domain.predict.OrbitalPos
import com.rtbishop.look4sat.core.domain.repository.IContainerProvider
import com.rtbishop.look4sat.core.domain.repository.IReporter
@@ -75,6 +74,7 @@ class RadarViewModel(
val uiState: StateFlow<RadarState> = _uiState
init {
// Compass sensor collection
if (settingsRepo.otherSettings.value.stateOfSensors) {
viewModelScope.launch {
sensorsRepo.enableSensor()
@@ -84,6 +84,7 @@ class RadarViewModel(
}
}
}
// Resolve which pass we're tracking
viewModelScope.launch {
val catNum = savedStateHandle.get<Int>("catNum") ?: 0
val aosTime = savedStateHandle.get<Long>("aosTime") ?: 0L
@@ -93,25 +94,33 @@ class RadarViewModel(
currentPass?.let { satPass ->
_uiState.update { it.copy(currentPass = satPass) }
val transmitters = satelliteRepo.getRadiosWithId(satPass.catNum)
// Compute track once (it doesn't change)
if (!satPass.isDeepSpace) {
val track = satelliteRepo.getTrack(
satPass.orbitalObject, stationPos, satPass.aosTime, satPass.losTime
)
_uiState.update { it.copy(satTrack = track) }
}
// Local tick loop — computes position only while the radar screen is alive
while (isActive) {
val timeNow = System.currentTimeMillis()
val pos = satelliteRepo.getPosition(satPass.orbitalObject, stationPos, timeNow)
when {
satPass.isDeepSpace -> {
val time = 0L.toTimerString()
_uiState.update { it.copy(currentTime = time, isCurrentTimeAos = false) }
_uiState.update { it.copy(currentTime = time, isCurrentTimeAos = false, orbitalPos = pos) }
}
satPass.aosTime > timeNow -> {
val time = satPass.aosTime.minus(timeNow).toTimerString()
_uiState.update { it.copy(currentTime = time, isCurrentTimeAos = true) }
_uiState.update { it.copy(currentTime = time, isCurrentTimeAos = true, orbitalPos = pos) }
}
else -> {
val time = satPass.losTime.minus(timeNow).toTimerString()
_uiState.update { it.copy(currentTime = time, isCurrentTimeAos = false) }
_uiState.update { it.copy(currentTime = time, isCurrentTimeAos = false, orbitalPos = pos) }
}
}
processRadios(transmitters, satPass.orbitalObject, timeNow)
sendPassData(satPass, pos, satPass.orbitalObject)
sendPassData(pos)
sendPassDataBT(pos)
delay(1000)
}
@@ -131,14 +140,7 @@ class RadarViewModel(
}
}
private suspend fun sendPassData(orbitalPass: OrbitalPass, orbitalPos: OrbitalPos, orbitalObject: OrbitalObject) {
var track: List<OrbitalPos> = emptyList()
if (!orbitalPass.isDeepSpace) {
track = satelliteRepo.getTrack(
orbitalObject, stationPos, orbitalPass.aosTime, orbitalPass.losTime
)
}
_uiState.update { it.copy(orbitalPos = orbitalPos, satTrack = track) }
private fun sendPassData(orbitalPos: OrbitalPos) {
viewModelScope.launch {
val rc = settingsRepo.rcSettings.value
if (rc.rotatorState) {