mirror of
https://github.com/rt-bishop/Look4Sat.git
synced 2026-10-02 03:15:37 +00:00
Added normalized time for AOS/LOS, fixed passes refresh
This commit is contained in:
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
|
||||
)
|
||||
}
|
||||
|
||||
+64
-29
@@ -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) {
|
||||
|
||||
Reference in new issue
Block a user