mirror of
https://github.com/atsunatsu/Look4Sat.git
synced 2026-10-02 03:15:37 +00:00
feat: keep mutual state across nav + overlay station-B track on main radar
- MutualViewModel is Activity-scoped, so returning from Radar keeps query results - TrackSampleData gains time field for live cut-off - Removed standalone mutual card from radar page - RadarViewCompose draws optional dashed station-B track + current dot - RadarScreen builds trackB from mutual data up to current time
This commit is contained in:
1 parent
bd42e643ea
commit
4ca33c37ad
5 files changed
+51
-49
No files matched your search
@@ -63,7 +63,9 @@ import androidx.compose.ui.res.stringResource
|
||||
import androidx.compose.ui.text.font.FontWeight
|
||||
import androidx.compose.ui.unit.dp
|
||||
import androidx.compose.ui.unit.sp
|
||||
import androidx.lifecycle.ViewModelStoreOwner
|
||||
import androidx.lifecycle.compose.collectAsStateWithLifecycle
|
||||
import androidx.lifecycle.viewmodel.compose.viewModel
|
||||
import androidx.lifecycle.viewmodel.navigation3.rememberViewModelStoreNavEntryDecorator
|
||||
import androidx.navigation3.runtime.entryProvider
|
||||
import androidx.navigation3.runtime.rememberNavBackStack
|
||||
@@ -133,6 +135,11 @@ fun MainScreen(navigateToRadar: () -> Unit = {}) {
|
||||
val container = (context.applicationContext as IContainerProvider).getMainContainer()
|
||||
val trackingState by container.radioTrackingService.state.collectAsStateWithLifecycle()
|
||||
val otherSettings by container.settingsRepo.otherSettings.collectAsStateWithLifecycle()
|
||||
// Activity-scoped so the mutual query results survive navigation to Radar and back
|
||||
val mutualViewModel: MutualViewModel = viewModel(
|
||||
viewModelStoreOwner = context as ViewModelStoreOwner,
|
||||
factory = MutualViewModel.factory(container)
|
||||
)
|
||||
|
||||
CompositionLocalProvider(
|
||||
LocalElevationThresholds provides ElevationThresholds(
|
||||
@@ -205,9 +212,8 @@ fun MainScreen(navigateToRadar: () -> Unit = {}) {
|
||||
MapDestination()
|
||||
}
|
||||
entry<Screen.Mutual> {
|
||||
val viewModel = MutualViewModel(container.satelliteRepo, container.settingsRepo)
|
||||
MutualScreen(
|
||||
viewModel = viewModel,
|
||||
viewModel = mutualViewModel,
|
||||
navigateUp = navigateBack,
|
||||
navigateToRadar = { catNum, aosTime, pass ->
|
||||
pass?.let { container.setMutualPassData(it) }
|
||||
|
||||
+1
@@ -56,6 +56,7 @@ data class MutualPassData(
|
||||
|
||||
/** Minimal track sample for cross-module sharing (angles in degrees). */
|
||||
data class TrackSampleData(
|
||||
val time: Long = 0L,
|
||||
val azimuthA: Double,
|
||||
val elevationA: Double,
|
||||
val azimuthB: Double,
|
||||
|
||||
@@ -329,6 +329,7 @@ private fun MutualContent(
|
||||
samples = pass.elevationSamples,
|
||||
trackSamples = pass.trackSamples.map {
|
||||
TrackSampleData(
|
||||
time = it.time,
|
||||
azimuthA = it.azimuthA,
|
||||
elevationA = it.elevationA,
|
||||
azimuthB = it.azimuthB,
|
||||
|
||||
@@ -32,10 +32,8 @@ import androidx.compose.foundation.layout.Arrangement
|
||||
import androidx.compose.foundation.layout.Box
|
||||
import androidx.compose.foundation.layout.Column
|
||||
import androidx.compose.foundation.layout.Row
|
||||
import androidx.compose.foundation.layout.Spacer
|
||||
import androidx.compose.foundation.layout.fillMaxSize
|
||||
import androidx.compose.foundation.layout.fillMaxWidth
|
||||
import androidx.compose.foundation.layout.height
|
||||
import androidx.compose.foundation.layout.padding
|
||||
import androidx.compose.foundation.pager.HorizontalPager
|
||||
import androidx.compose.foundation.pager.rememberPagerState
|
||||
@@ -73,10 +71,8 @@ import com.rtbishop.look4sat.core.presentation.formatFrequency
|
||||
import com.rtbishop.look4sat.core.presentation.getDefaultPass
|
||||
import com.rtbishop.look4sat.core.presentation.isVerticalLayout
|
||||
import com.rtbishop.look4sat.core.presentation.layoutPadding
|
||||
import com.rtbishop.look4sat.feature.mutual.ElevationCurveChart
|
||||
import com.rtbishop.look4sat.feature.mutual.MutualRadarView
|
||||
import com.rtbishop.look4sat.feature.mutual.TrackSample
|
||||
import kotlinx.coroutines.launch
|
||||
import kotlin.math.PI
|
||||
|
||||
private enum class RadarPage(val title: String) {
|
||||
Transceivers("Transceivers"),
|
||||
@@ -121,6 +117,17 @@ private fun RadarScreen(
|
||||
val addToCalendar: () -> Unit = {
|
||||
uiState.currentPass?.let { onAction(RadarAction.AddToCalendar(it.name, it.aosTime, it.losTime)) }
|
||||
}
|
||||
// Live station-B track overlay: samples up to "now" (recomposed each second tick)
|
||||
val timeNow = System.currentTimeMillis()
|
||||
val trackB = mutualData.trackSamples
|
||||
.filter { it.time <= timeNow }
|
||||
.map {
|
||||
OrbitalPos(
|
||||
azimuth = it.azimuthB * PI / 180.0,
|
||||
elevation = it.elevationB * PI / 180.0,
|
||||
time = it.time
|
||||
)
|
||||
}
|
||||
Column(
|
||||
modifier = Modifier
|
||||
.layoutPadding()
|
||||
@@ -144,17 +151,11 @@ private fun RadarScreen(
|
||||
}
|
||||
}
|
||||
if (isVertical) {
|
||||
RadarCard(uiState, Modifier.weight(1f))
|
||||
if (mutualData.samples.isNotEmpty()) {
|
||||
MutualElevationCard(mutualData, Modifier.weight(1f))
|
||||
}
|
||||
RadarCard(uiState, trackB, Modifier.weight(1f))
|
||||
PagerCard(uiState, onAction, requestMicPermission, Modifier.weight(1f))
|
||||
} else {
|
||||
Row(horizontalArrangement = Arrangement.spacedBy(6.dp)) {
|
||||
RadarCard(uiState, Modifier.weight(1f))
|
||||
if (mutualData.samples.isNotEmpty()) {
|
||||
MutualElevationCard(mutualData, Modifier.weight(1f))
|
||||
}
|
||||
RadarCard(uiState, trackB, Modifier.weight(1f))
|
||||
PagerCard(uiState, onAction, requestMicPermission, Modifier.weight(1f))
|
||||
}
|
||||
}
|
||||
@@ -210,7 +211,11 @@ private fun PagerCard(
|
||||
}
|
||||
|
||||
@Composable
|
||||
private fun RadarCard(uiState: RadarState, modifier: Modifier = Modifier) {
|
||||
private fun RadarCard(
|
||||
uiState: RadarState,
|
||||
trackB: List<OrbitalPos> = emptyList(),
|
||||
modifier: Modifier = Modifier
|
||||
) {
|
||||
val satellitePos = uiState.orbitalPos
|
||||
val shouldAnimateBorder = satellitePos?.aboveHorizon == true && satellitePos.eclipsed
|
||||
// Always call these composables unconditionally — conditional composable calls violate
|
||||
@@ -243,6 +248,7 @@ private fun RadarCard(uiState: RadarState, modifier: Modifier = Modifier) {
|
||||
RadarViewCompose(
|
||||
item = position,
|
||||
items = uiState.satTrack,
|
||||
trackB = trackB.takeIf { it.isNotEmpty() },
|
||||
azimElev = uiState.orientationValues,
|
||||
shouldShowSweep = uiState.shouldShowSweep,
|
||||
shouldUseCompass = uiState.shouldUseCompass,
|
||||
@@ -318,35 +324,3 @@ private fun RadarLabel(
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@Composable
|
||||
private fun MutualElevationCard(mutualData: MutualPassData, modifier: Modifier = Modifier) {
|
||||
ElevatedCard(modifier = modifier) {
|
||||
Column(modifier = Modifier.fillMaxSize().padding(8.dp)) {
|
||||
if (mutualData.trackSamples.isNotEmpty()) {
|
||||
MutualRadarView(
|
||||
trackSamples = mutualData.trackSamples.map {
|
||||
TrackSample(
|
||||
time = 0L,
|
||||
azimuthA = it.azimuthA,
|
||||
elevationA = it.elevationA,
|
||||
azimuthB = it.azimuthB,
|
||||
elevationB = it.elevationB
|
||||
)
|
||||
},
|
||||
labelA = mutualData.labelA,
|
||||
labelB = mutualData.labelB
|
||||
)
|
||||
Spacer(Modifier.height(4.dp))
|
||||
}
|
||||
if (mutualData.samples.isNotEmpty()) {
|
||||
ElevationCurveChart(
|
||||
samples = mutualData.samples,
|
||||
startTime = mutualData.startTime,
|
||||
endTime = mutualData.endTime,
|
||||
maxElev = mutualData.maxElev
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -71,6 +71,8 @@ private const val SWEEP_DURATION_MS = 8_000
|
||||
fun RadarViewCompose(
|
||||
item: OrbitalPos,
|
||||
items: List<OrbitalPos>,
|
||||
trackB: List<OrbitalPos>? = null,
|
||||
trackBColor: Color = MaterialTheme.colorScheme.tertiary,
|
||||
azimElev: Pair<Float, Float>,
|
||||
shouldShowSweep: Boolean,
|
||||
shouldUseCompass: Boolean,
|
||||
@@ -107,18 +109,23 @@ fun RadarViewCompose(
|
||||
var cachedSweepColor by remember { mutableStateOf(Color.Unspecified) }
|
||||
var trackPath by remember { mutableStateOf(Path()) }
|
||||
var trackEffect by remember { mutableStateOf(PathEffect.cornerPathEffect(0f)) }
|
||||
// Station-B overlay track (dashed)
|
||||
var cachedTrackBRef by remember { mutableStateOf<List<OrbitalPos>?>(null) }
|
||||
var trackBPath by remember { mutableStateOf(Path()) }
|
||||
// ShaderBrush is cached to avoid allocating a new GPU shader object every frame
|
||||
var cachedSweepBrush by remember { mutableStateOf<ShaderBrush?>(null) }
|
||||
|
||||
Canvas(modifier = modifier.aspectRatio(1f)) {
|
||||
val radius = size.minDimension / 2f * 0.95f
|
||||
// Rebuild track path and sweep brush when canvas size or track data changes
|
||||
if (radius != cachedRadius || items !== cachedItemsRef) {
|
||||
if (radius != cachedRadius || items !== cachedItemsRef || trackB !== cachedTrackBRef) {
|
||||
trackPath = createTrackPath(items, radius)
|
||||
trackEffect = createTrackEffect(trackPath)
|
||||
trackBPath = trackB?.let { createTrackPath(it, radius) } ?: Path()
|
||||
cachedSweepBrush = makeSweepBrush(center, primaryColor)
|
||||
cachedRadius = radius
|
||||
cachedItemsRef = items
|
||||
cachedTrackBRef = trackB
|
||||
cachedSweepColor = primaryColor
|
||||
} else if (primaryColor != cachedSweepColor) {
|
||||
// Rebuild brush on theme change without waiting for a size change
|
||||
@@ -131,6 +138,19 @@ fun RadarViewCompose(
|
||||
drawElevationLabels(radius, primaryColor, measurer)
|
||||
translate(center.x, center.y) {
|
||||
drawTrack(trackPath, trackEffect, aimColor, primaryColor)
|
||||
// Station-B overlay: dashed track + current position dot
|
||||
if (trackB != null && trackB.isNotEmpty() && !trackBPath.isEmpty) {
|
||||
drawPath(
|
||||
trackBPath, trackBColor,
|
||||
style = Stroke(STROKE_WIDTH, pathEffect = PathEffect.dashPathEffect(floatArrayOf(18f, 12f)))
|
||||
)
|
||||
val lastB = trackB.last()
|
||||
if (lastB.elevation > 0) {
|
||||
val posB = sph2Cart(lastB.azimuth, lastB.elevation, radius.toDouble())
|
||||
drawCircle(trackBColor, 26f, posB, style = Stroke(2f))
|
||||
drawCircle(trackBColor, 12f, posB)
|
||||
}
|
||||
}
|
||||
if (item.elevation > 0) {
|
||||
drawPosition(item, radius, animScale, primaryColor)
|
||||
}
|
||||
|
||||
Reference in new issue
Block a user