From 4ca33c37ada3947e79d4975625d51df262c3d105 Mon Sep 17 00:00:00 2001 From: atsunatsu Date: Mon, 3 Aug 2026 00:55:05 +0800 Subject: [PATCH] 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 --- .../java/com/rtbishop/look4sat/MainScreen.kt | 10 ++- .../core/domain/repository/IMainContainer.kt | 1 + .../look4sat/feature/mutual/MutualScreen.kt | 1 + .../look4sat/feature/radar/RadarScreen.kt | 66 ++++++------------- .../look4sat/feature/radar/RadarView.kt | 22 ++++++- 5 files changed, 51 insertions(+), 49 deletions(-) diff --git a/app/src/main/java/com/rtbishop/look4sat/MainScreen.kt b/app/src/main/java/com/rtbishop/look4sat/MainScreen.kt index 3c48290a..018fca4a 100644 --- a/app/src/main/java/com/rtbishop/look4sat/MainScreen.kt +++ b/app/src/main/java/com/rtbishop/look4sat/MainScreen.kt @@ -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 { - val viewModel = MutualViewModel(container.satelliteRepo, container.settingsRepo) MutualScreen( - viewModel = viewModel, + viewModel = mutualViewModel, navigateUp = navigateBack, navigateToRadar = { catNum, aosTime, pass -> pass?.let { container.setMutualPassData(it) } diff --git a/core/domain/src/main/java/com/rtbishop/look4sat/core/domain/repository/IMainContainer.kt b/core/domain/src/main/java/com/rtbishop/look4sat/core/domain/repository/IMainContainer.kt index 017ace38..70de0f81 100644 --- a/core/domain/src/main/java/com/rtbishop/look4sat/core/domain/repository/IMainContainer.kt +++ b/core/domain/src/main/java/com/rtbishop/look4sat/core/domain/repository/IMainContainer.kt @@ -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, diff --git a/feature/mutual/src/main/java/com/rtbishop/look4sat/feature/mutual/MutualScreen.kt b/feature/mutual/src/main/java/com/rtbishop/look4sat/feature/mutual/MutualScreen.kt index 620fecad..dc39a00c 100644 --- a/feature/mutual/src/main/java/com/rtbishop/look4sat/feature/mutual/MutualScreen.kt +++ b/feature/mutual/src/main/java/com/rtbishop/look4sat/feature/mutual/MutualScreen.kt @@ -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, diff --git a/feature/radar/src/main/java/com/rtbishop/look4sat/feature/radar/RadarScreen.kt b/feature/radar/src/main/java/com/rtbishop/look4sat/feature/radar/RadarScreen.kt index d4cfb65a..102149f1 100644 --- a/feature/radar/src/main/java/com/rtbishop/look4sat/feature/radar/RadarScreen.kt +++ b/feature/radar/src/main/java/com/rtbishop/look4sat/feature/radar/RadarScreen.kt @@ -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 = 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 - ) - } - } - } -} diff --git a/feature/radar/src/main/java/com/rtbishop/look4sat/feature/radar/RadarView.kt b/feature/radar/src/main/java/com/rtbishop/look4sat/feature/radar/RadarView.kt index bedf5a7d..8a5d034c 100644 --- a/feature/radar/src/main/java/com/rtbishop/look4sat/feature/radar/RadarView.kt +++ b/feature/radar/src/main/java/com/rtbishop/look4sat/feature/radar/RadarView.kt @@ -71,6 +71,8 @@ private const val SWEEP_DURATION_MS = 8_000 fun RadarViewCompose( item: OrbitalPos, items: List, + trackB: List? = null, + trackBColor: Color = MaterialTheme.colorScheme.tertiary, azimElev: Pair, 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?>(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(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) }