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.text.font.FontWeight
|
||||||
import androidx.compose.ui.unit.dp
|
import androidx.compose.ui.unit.dp
|
||||||
import androidx.compose.ui.unit.sp
|
import androidx.compose.ui.unit.sp
|
||||||
|
import androidx.lifecycle.ViewModelStoreOwner
|
||||||
import androidx.lifecycle.compose.collectAsStateWithLifecycle
|
import androidx.lifecycle.compose.collectAsStateWithLifecycle
|
||||||
|
import androidx.lifecycle.viewmodel.compose.viewModel
|
||||||
import androidx.lifecycle.viewmodel.navigation3.rememberViewModelStoreNavEntryDecorator
|
import androidx.lifecycle.viewmodel.navigation3.rememberViewModelStoreNavEntryDecorator
|
||||||
import androidx.navigation3.runtime.entryProvider
|
import androidx.navigation3.runtime.entryProvider
|
||||||
import androidx.navigation3.runtime.rememberNavBackStack
|
import androidx.navigation3.runtime.rememberNavBackStack
|
||||||
@@ -133,6 +135,11 @@ fun MainScreen(navigateToRadar: () -> Unit = {}) {
|
|||||||
val container = (context.applicationContext as IContainerProvider).getMainContainer()
|
val container = (context.applicationContext as IContainerProvider).getMainContainer()
|
||||||
val trackingState by container.radioTrackingService.state.collectAsStateWithLifecycle()
|
val trackingState by container.radioTrackingService.state.collectAsStateWithLifecycle()
|
||||||
val otherSettings by container.settingsRepo.otherSettings.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(
|
CompositionLocalProvider(
|
||||||
LocalElevationThresholds provides ElevationThresholds(
|
LocalElevationThresholds provides ElevationThresholds(
|
||||||
@@ -205,9 +212,8 @@ fun MainScreen(navigateToRadar: () -> Unit = {}) {
|
|||||||
MapDestination()
|
MapDestination()
|
||||||
}
|
}
|
||||||
entry<Screen.Mutual> {
|
entry<Screen.Mutual> {
|
||||||
val viewModel = MutualViewModel(container.satelliteRepo, container.settingsRepo)
|
|
||||||
MutualScreen(
|
MutualScreen(
|
||||||
viewModel = viewModel,
|
viewModel = mutualViewModel,
|
||||||
navigateUp = navigateBack,
|
navigateUp = navigateBack,
|
||||||
navigateToRadar = { catNum, aosTime, pass ->
|
navigateToRadar = { catNum, aosTime, pass ->
|
||||||
pass?.let { container.setMutualPassData(it) }
|
pass?.let { container.setMutualPassData(it) }
|
||||||
|
|||||||
+1
@@ -56,6 +56,7 @@ data class MutualPassData(
|
|||||||
|
|
||||||
/** Minimal track sample for cross-module sharing (angles in degrees). */
|
/** Minimal track sample for cross-module sharing (angles in degrees). */
|
||||||
data class TrackSampleData(
|
data class TrackSampleData(
|
||||||
|
val time: Long = 0L,
|
||||||
val azimuthA: Double,
|
val azimuthA: Double,
|
||||||
val elevationA: Double,
|
val elevationA: Double,
|
||||||
val azimuthB: Double,
|
val azimuthB: Double,
|
||||||
|
|||||||
@@ -329,6 +329,7 @@ private fun MutualContent(
|
|||||||
samples = pass.elevationSamples,
|
samples = pass.elevationSamples,
|
||||||
trackSamples = pass.trackSamples.map {
|
trackSamples = pass.trackSamples.map {
|
||||||
TrackSampleData(
|
TrackSampleData(
|
||||||
|
time = it.time,
|
||||||
azimuthA = it.azimuthA,
|
azimuthA = it.azimuthA,
|
||||||
elevationA = it.elevationA,
|
elevationA = it.elevationA,
|
||||||
azimuthB = it.azimuthB,
|
azimuthB = it.azimuthB,
|
||||||
|
|||||||
@@ -32,10 +32,8 @@ import androidx.compose.foundation.layout.Arrangement
|
|||||||
import androidx.compose.foundation.layout.Box
|
import androidx.compose.foundation.layout.Box
|
||||||
import androidx.compose.foundation.layout.Column
|
import androidx.compose.foundation.layout.Column
|
||||||
import androidx.compose.foundation.layout.Row
|
import androidx.compose.foundation.layout.Row
|
||||||
import androidx.compose.foundation.layout.Spacer
|
|
||||||
import androidx.compose.foundation.layout.fillMaxSize
|
import androidx.compose.foundation.layout.fillMaxSize
|
||||||
import androidx.compose.foundation.layout.fillMaxWidth
|
import androidx.compose.foundation.layout.fillMaxWidth
|
||||||
import androidx.compose.foundation.layout.height
|
|
||||||
import androidx.compose.foundation.layout.padding
|
import androidx.compose.foundation.layout.padding
|
||||||
import androidx.compose.foundation.pager.HorizontalPager
|
import androidx.compose.foundation.pager.HorizontalPager
|
||||||
import androidx.compose.foundation.pager.rememberPagerState
|
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.getDefaultPass
|
||||||
import com.rtbishop.look4sat.core.presentation.isVerticalLayout
|
import com.rtbishop.look4sat.core.presentation.isVerticalLayout
|
||||||
import com.rtbishop.look4sat.core.presentation.layoutPadding
|
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 kotlinx.coroutines.launch
|
||||||
|
import kotlin.math.PI
|
||||||
|
|
||||||
private enum class RadarPage(val title: String) {
|
private enum class RadarPage(val title: String) {
|
||||||
Transceivers("Transceivers"),
|
Transceivers("Transceivers"),
|
||||||
@@ -121,6 +117,17 @@ private fun RadarScreen(
|
|||||||
val addToCalendar: () -> Unit = {
|
val addToCalendar: () -> Unit = {
|
||||||
uiState.currentPass?.let { onAction(RadarAction.AddToCalendar(it.name, it.aosTime, it.losTime)) }
|
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(
|
Column(
|
||||||
modifier = Modifier
|
modifier = Modifier
|
||||||
.layoutPadding()
|
.layoutPadding()
|
||||||
@@ -144,17 +151,11 @@ private fun RadarScreen(
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
if (isVertical) {
|
if (isVertical) {
|
||||||
RadarCard(uiState, Modifier.weight(1f))
|
RadarCard(uiState, trackB, Modifier.weight(1f))
|
||||||
if (mutualData.samples.isNotEmpty()) {
|
|
||||||
MutualElevationCard(mutualData, Modifier.weight(1f))
|
|
||||||
}
|
|
||||||
PagerCard(uiState, onAction, requestMicPermission, Modifier.weight(1f))
|
PagerCard(uiState, onAction, requestMicPermission, Modifier.weight(1f))
|
||||||
} else {
|
} else {
|
||||||
Row(horizontalArrangement = Arrangement.spacedBy(6.dp)) {
|
Row(horizontalArrangement = Arrangement.spacedBy(6.dp)) {
|
||||||
RadarCard(uiState, Modifier.weight(1f))
|
RadarCard(uiState, trackB, Modifier.weight(1f))
|
||||||
if (mutualData.samples.isNotEmpty()) {
|
|
||||||
MutualElevationCard(mutualData, Modifier.weight(1f))
|
|
||||||
}
|
|
||||||
PagerCard(uiState, onAction, requestMicPermission, Modifier.weight(1f))
|
PagerCard(uiState, onAction, requestMicPermission, Modifier.weight(1f))
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -210,7 +211,11 @@ private fun PagerCard(
|
|||||||
}
|
}
|
||||||
|
|
||||||
@Composable
|
@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 satellitePos = uiState.orbitalPos
|
||||||
val shouldAnimateBorder = satellitePos?.aboveHorizon == true && satellitePos.eclipsed
|
val shouldAnimateBorder = satellitePos?.aboveHorizon == true && satellitePos.eclipsed
|
||||||
// Always call these composables unconditionally — conditional composable calls violate
|
// Always call these composables unconditionally — conditional composable calls violate
|
||||||
@@ -243,6 +248,7 @@ private fun RadarCard(uiState: RadarState, modifier: Modifier = Modifier) {
|
|||||||
RadarViewCompose(
|
RadarViewCompose(
|
||||||
item = position,
|
item = position,
|
||||||
items = uiState.satTrack,
|
items = uiState.satTrack,
|
||||||
|
trackB = trackB.takeIf { it.isNotEmpty() },
|
||||||
azimElev = uiState.orientationValues,
|
azimElev = uiState.orientationValues,
|
||||||
shouldShowSweep = uiState.shouldShowSweep,
|
shouldShowSweep = uiState.shouldShowSweep,
|
||||||
shouldUseCompass = uiState.shouldUseCompass,
|
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(
|
fun RadarViewCompose(
|
||||||
item: OrbitalPos,
|
item: OrbitalPos,
|
||||||
items: List<OrbitalPos>,
|
items: List<OrbitalPos>,
|
||||||
|
trackB: List<OrbitalPos>? = null,
|
||||||
|
trackBColor: Color = MaterialTheme.colorScheme.tertiary,
|
||||||
azimElev: Pair<Float, Float>,
|
azimElev: Pair<Float, Float>,
|
||||||
shouldShowSweep: Boolean,
|
shouldShowSweep: Boolean,
|
||||||
shouldUseCompass: Boolean,
|
shouldUseCompass: Boolean,
|
||||||
@@ -107,18 +109,23 @@ fun RadarViewCompose(
|
|||||||
var cachedSweepColor by remember { mutableStateOf(Color.Unspecified) }
|
var cachedSweepColor by remember { mutableStateOf(Color.Unspecified) }
|
||||||
var trackPath by remember { mutableStateOf(Path()) }
|
var trackPath by remember { mutableStateOf(Path()) }
|
||||||
var trackEffect by remember { mutableStateOf(PathEffect.cornerPathEffect(0f)) }
|
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
|
// ShaderBrush is cached to avoid allocating a new GPU shader object every frame
|
||||||
var cachedSweepBrush by remember { mutableStateOf<ShaderBrush?>(null) }
|
var cachedSweepBrush by remember { mutableStateOf<ShaderBrush?>(null) }
|
||||||
|
|
||||||
Canvas(modifier = modifier.aspectRatio(1f)) {
|
Canvas(modifier = modifier.aspectRatio(1f)) {
|
||||||
val radius = size.minDimension / 2f * 0.95f
|
val radius = size.minDimension / 2f * 0.95f
|
||||||
// Rebuild track path and sweep brush when canvas size or track data changes
|
// 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)
|
trackPath = createTrackPath(items, radius)
|
||||||
trackEffect = createTrackEffect(trackPath)
|
trackEffect = createTrackEffect(trackPath)
|
||||||
|
trackBPath = trackB?.let { createTrackPath(it, radius) } ?: Path()
|
||||||
cachedSweepBrush = makeSweepBrush(center, primaryColor)
|
cachedSweepBrush = makeSweepBrush(center, primaryColor)
|
||||||
cachedRadius = radius
|
cachedRadius = radius
|
||||||
cachedItemsRef = items
|
cachedItemsRef = items
|
||||||
|
cachedTrackBRef = trackB
|
||||||
cachedSweepColor = primaryColor
|
cachedSweepColor = primaryColor
|
||||||
} else if (primaryColor != cachedSweepColor) {
|
} else if (primaryColor != cachedSweepColor) {
|
||||||
// Rebuild brush on theme change without waiting for a size change
|
// Rebuild brush on theme change without waiting for a size change
|
||||||
@@ -131,6 +138,19 @@ fun RadarViewCompose(
|
|||||||
drawElevationLabels(radius, primaryColor, measurer)
|
drawElevationLabels(radius, primaryColor, measurer)
|
||||||
translate(center.x, center.y) {
|
translate(center.x, center.y) {
|
||||||
drawTrack(trackPath, trackEffect, aimColor, primaryColor)
|
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) {
|
if (item.elevation > 0) {
|
||||||
drawPosition(item, radius, animScale, primaryColor)
|
drawPosition(item, radius, animScale, primaryColor)
|
||||||
}
|
}
|
||||||
|
|||||||
Reference in new issue
Block a user