Added navigation parameters to RadarScreen

This commit is contained in:
Arty Bishop committed 2025-12-27 15:01:33 +00:00
1 parent 049d84e261
commit 05b5af41ba
5 files changed
+308 -43

No files matched your search

@@ -39,6 +39,7 @@ import androidx.navigation.compose.NavHost
import androidx.navigation.compose.composable import androidx.navigation.compose.composable
import androidx.navigation.compose.currentBackStackEntryAsState import androidx.navigation.compose.currentBackStackEntryAsState
import androidx.navigation.compose.rememberNavController import androidx.navigation.compose.rememberNavController
import androidx.navigation.navArgument
import com.rtbishop.look4sat.R import com.rtbishop.look4sat.R
import com.rtbishop.look4sat.presentation.entriesScreen.EntriesScreen import com.rtbishop.look4sat.presentation.entriesScreen.EntriesScreen
import com.rtbishop.look4sat.presentation.mapScreen.MapScreen import com.rtbishop.look4sat.presentation.mapScreen.MapScreen
@@ -89,7 +90,7 @@ private fun MainNavBar(navController: NavController) {
val navBackStackEntry = navController.currentBackStackEntryAsState() val navBackStackEntry = navController.currentBackStackEntryAsState()
val currentRoute = navBackStackEntry.value?.destination?.route val currentRoute = navBackStackEntry.value?.destination?.route
items.forEach { item -> items.forEach { item ->
NavigationBarItem(selected = currentRoute == item.route, onClick = { NavigationBarItem(selected = currentRoute?.contains(item.route) ?: false, onClick = {
navController.navigate(item.route) { navController.navigate(item.route) {
navController.graph.startDestinationRoute?.let { route -> navController.graph.startDestinationRoute?.let { route ->
popUpTo(route) { saveState = false } popUpTo(route) { saveState = false }
@@ -104,10 +105,13 @@ private fun MainNavBar(navController: NavController) {
@Composable @Composable
private fun MainNavGraph(navController: NavHostController) { private fun MainNavGraph(navController: NavHostController) {
val radarRoute = "${Screen.Radar.route}?catNum={catNum}&aosTime={aosTime}"
val radarArgs = listOf(navArgument("catNum") { defaultValue = 0 },
navArgument("aosTime") { defaultValue = 0L })
NavHost(navController, startDestination = Screen.Passes.route) { NavHost(navController, startDestination = Screen.Passes.route) {
composable(Screen.Entries.route) { EntriesScreen(navController) } composable(Screen.Entries.route) { EntriesScreen(navController) }
composable(Screen.Passes.route) { PassesScreen(navController) } composable(Screen.Passes.route) { PassesScreen(navController) }
composable(Screen.Radar.route) { RadarScreen() } composable(radarRoute, radarArgs) { RadarScreen(navController) }
composable(Screen.Map.route) { MapScreen() } composable(Screen.Map.route) { MapScreen() }
composable(Screen.Settings.route) { SettingsScreen(navController) } composable(Screen.Settings.route) { SettingsScreen(navController) }
} }
@@ -41,7 +41,9 @@ private val sdf = SimpleDateFormat("HH:mm:ss", Locale.ENGLISH)
@Composable @Composable
fun PassesScreen(navController: NavController) { fun PassesScreen(navController: NavController) {
val viewModel: PassesViewModel = hiltViewModel() val viewModel: PassesViewModel = hiltViewModel()
val navToRadar = { navController.navigate(Screen.Radar.route) } val navToRadar = { catNum: Int, aosTime: Long ->
navController.navigate("${Screen.Radar.route}?catNum=${catNum}&aosTime=${aosTime}")
}
val state = viewModel.passes.collectAsState(initial = null) val state = viewModel.passes.collectAsState(initial = null)
val timerText = viewModel.timerText.collectAsState(initial = null) val timerText = viewModel.timerText.collectAsState(initial = null)
val isRefreshing = state.value is DataState.Loading val isRefreshing = state.value is DataState.Loading
@@ -105,15 +107,15 @@ private fun PassPreview() {
) )
val satellite = NearEarthSat(data) val satellite = NearEarthSat(data)
val pass = SatPass(1L, 25.0, 10L, 75.0, 850, 45.0, satellite, 0.5f) val pass = SatPass(1L, 25.0, 10L, 75.0, 850, 45.0, satellite, 0.5f)
MainTheme { Pass(pass = pass, {}) } MainTheme { Pass(pass = pass, { _,_ -> }) }
} }
@Composable @Composable
private fun Pass(pass: SatPass, navToRadar: () -> Unit, modifier: Modifier = Modifier) { private fun Pass(pass: SatPass, navToRadar: (Int, Long) -> Unit, modifier: Modifier = Modifier) {
Surface(color = MaterialTheme.colorScheme.background, modifier = modifier) { Surface(color = MaterialTheme.colorScheme.background, modifier = modifier) {
Surface(modifier = Modifier Surface(modifier = Modifier
.padding(bottom = 2.dp) .padding(bottom = 2.dp)
.clickable { navToRadar() }) { .clickable { navToRadar(pass.catNum, pass.aosTime) }) {
Column( Column(
verticalArrangement = Arrangement.spacedBy(2.dp), verticalArrangement = Arrangement.spacedBy(2.dp),
modifier = Modifier modifier = Modifier
@@ -202,7 +204,7 @@ private fun PassesCard(
refreshState: PullRefreshState, refreshState: PullRefreshState,
isRefreshing: Boolean, isRefreshing: Boolean,
passes: List<SatPass>, passes: List<SatPass>,
navToRadar: () -> Unit navToRadar: (Int, Long) -> Unit
) { ) {
ElevatedCard(modifier = Modifier.fillMaxSize()) { ElevatedCard(modifier = Modifier.fillMaxSize()) {
Box(Modifier.pullRefresh(refreshState)) { Box(Modifier.pullRefresh(refreshState)) {
@@ -1,20 +1,35 @@
package com.rtbishop.look4sat.presentation.radarScreen package com.rtbishop.look4sat.presentation.radarScreen
import androidx.compose.foundation.background
import androidx.compose.foundation.layout.* import androidx.compose.foundation.layout.*
import androidx.compose.foundation.lazy.LazyColumn
import androidx.compose.foundation.lazy.items
import androidx.compose.material3.* import androidx.compose.material3.*
import androidx.compose.runtime.Composable import androidx.compose.runtime.Composable
import androidx.compose.runtime.collectAsState
import androidx.compose.ui.Alignment import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier import androidx.compose.ui.Modifier
import androidx.compose.ui.platform.LocalContext
import androidx.compose.ui.res.painterResource import androidx.compose.ui.res.painterResource
import androidx.compose.ui.res.stringResource
import androidx.compose.ui.text.font.FontWeight import androidx.compose.ui.text.font.FontWeight
import androidx.compose.ui.text.style.TextAlign import androidx.compose.ui.text.style.TextAlign
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.compose.ui.viewinterop.AndroidView
import androidx.hilt.navigation.compose.hiltViewModel import androidx.hilt.navigation.compose.hiltViewModel
import androidx.navigation.NavController
import com.rtbishop.look4sat.R import com.rtbishop.look4sat.R
import com.rtbishop.look4sat.domain.model.SatRadio
import timber.log.Timber
@Composable @Composable
fun RadarScreen(viewModel: RadarViewModel = hiltViewModel()) { fun RadarScreen(navController: NavController) {
val viewModel: RadarViewModel = hiltViewModel()
val currentPass = viewModel.getPass().collectAsState(null)
val radarData = viewModel.radarData.collectAsState()
val transmitters = viewModel.transmitters.collectAsState()
val orientation = viewModel.orientation.collectAsState()
Column(modifier = Modifier.padding(6.dp), verticalArrangement = Arrangement.spacedBy(6.dp)) { Column(modifier = Modifier.padding(6.dp), verticalArrangement = Arrangement.spacedBy(6.dp)) {
ElevatedCard(modifier = Modifier.height(52.dp)) { ElevatedCard(modifier = Modifier.height(52.dp)) {
@@ -47,11 +62,106 @@ fun RadarScreen(viewModel: RadarViewModel = hiltViewModel()) {
) )
} }
} }
ElevatedCard(modifier = Modifier.fillMaxSize().weight(1f)) { ElevatedCard(modifier = Modifier
.fillMaxSize()
.weight(1f)) {
// val context = LocalContext.current
// AndroidView({ RadarView(context) }, modifier = Modifier.fillMaxSize()) { radarView ->
// Timber.d("Radar view recomposition")
// radarView.setShowAim(true)
// radarView.setScanning(true)
// radarData.value?.let {
// radarView.setPosition(it.satPos)
// radarView.setPositions(it.satTrack)
// }
// orientation.value?.let {
// radarView.setOrientation(it.first, it.second, it.first)
// }
// }
radarData.value?.let { data ->
orientation.value?.let { triple ->
RadarViewCompose(
item = data.satPos,
items = data.satTrack,
orientation = triple
)
}
}
} }
ElevatedCard(modifier = Modifier.fillMaxSize().weight(1f)) { ElevatedCard(modifier = Modifier
.fillMaxSize()
.weight(1f)) {
TransmittersList(transmitters = transmitters.value)
}
}
}
@Composable
private fun TransmittersList(transmitters: List<SatRadio>) {
LazyColumn(modifier = Modifier.fillMaxSize()) {
items(items = transmitters, key = { item -> item.uuid }) { radio ->
TransmitterItem(radio)
}
}
}
@Composable
private fun TransmitterItem(radio: SatRadio) {
Surface(color = MaterialTheme.colorScheme.background) {
Surface(modifier = Modifier.padding(bottom = 2.dp)) {
Column(
verticalArrangement = Arrangement.spacedBy(2.dp),
modifier = Modifier
.background(MaterialTheme.colorScheme.surface)
.padding(6.dp)
) {
Row(verticalAlignment = Alignment.CenterVertically, modifier = Modifier.fillMaxWidth()) {
Icon(
painter = painterResource(id = R.drawable.ic_prev),
contentDescription = null
)
Text(
text = radio.info,
modifier = Modifier.weight(1f),
textAlign = TextAlign.Center
)
Icon(
painter = painterResource(id = R.drawable.ic_next),
contentDescription = null
)
}
Row(
verticalAlignment = Alignment.CenterVertically,
modifier = Modifier.fillMaxWidth()
) {
Text(
text = stringResource(id = R.string.radio_downlink, radio.downlink ?: 0L),
textAlign = TextAlign.Center,
fontSize = 15.sp,
modifier = Modifier.weight(0.5f)
)
Text(
text = stringResource(id = R.string.radio_uplink, radio.uplink ?: 0L),
textAlign = TextAlign.Center,
fontSize = 15.sp,
modifier = Modifier.weight(0.5f)
)
}
Row(
verticalAlignment = Alignment.CenterVertically,
horizontalArrangement = Arrangement.SpaceBetween,
modifier = Modifier.fillMaxWidth()
) {
Text(
text = radio.mode ?: "",
fontSize = 15.sp
)
Text(
text = radio.isInverted.toString(),
fontSize = 15.sp
)
}
}
} }
} }
} }
@@ -73,29 +183,9 @@ fun RadarScreen(viewModel: RadarViewModel = hiltViewModel()) {
// radarView?.setPositions(passData.satTrack) // radarView?.setPositions(passData.satTrack)
// setPassText(pass, passData.satPos) // setPassText(pass, passData.satPos)
// } // }
// viewModel.transmitters.observe(viewLifecycleOwner) { list ->
// if (list.isNotEmpty()) {
// radioAdapter.submitList(list)
// radarProgress.visibility = View.INVISIBLE
// } else {
// radarProgress.visibility = View.INVISIBLE
// radarEmptyLayout.visibility = View.VISIBLE
// }
// radarView?.invalidate()
// }
// viewModel.orientation.observe(viewLifecycleOwner) { value -> // viewModel.orientation.observe(viewLifecycleOwner) { value ->
// radarView?.setOrientation(value.first, value.second, value.third) // radarView?.setOrientation(value.first, value.second, value.third)
// } // }
// radarBtnBack.clickWithDebounce { findNavController().navigateUp() }
// radarBtnMap.clickWithDebounce {
// val direction = RadarFragmentDirections.globalToMap(pass.catNum)
// findNavController().navigate(direction)
// }
// radarBtnNotify.isEnabled = false
// radarBtnSettings.clickWithDebounce {
// val direction = RadarFragmentDirections.globalToSettings()
// findNavController().navigate(direction)
// }
// } // }
// } // }
//} //}
@@ -0,0 +1,155 @@
@file:OptIn(ExperimentalTextApi::class)
package com.rtbishop.look4sat.presentation.radarScreen
import androidx.compose.foundation.Canvas
import androidx.compose.foundation.layout.fillMaxSize
import androidx.compose.runtime.Composable
import androidx.compose.runtime.mutableStateOf
import androidx.compose.runtime.remember
import androidx.compose.ui.Modifier
import androidx.compose.ui.graphics.*
import androidx.compose.ui.graphics.drawscope.*
import androidx.compose.ui.text.*
import androidx.compose.ui.unit.sp
import com.rtbishop.look4sat.domain.predict.PI_2
import com.rtbishop.look4sat.domain.predict.SatPos
import com.rtbishop.look4sat.domain.predict.TWO_PI
import com.rtbishop.look4sat.utility.toRadians
import kotlin.math.cos
import kotlin.math.sin
@Composable
fun RadarViewCompose(item: SatPos, items: List<SatPos>, orientation: Triple<Float, Float, Float>) {
val measurer = rememberTextMeasurer()
val newTrackPath = remember { mutableStateOf(Path()) }
val newArrowPath = remember { mutableStateOf(Path()) }
var isTrackCreated = false
val yellowColor = Color(0xFFFFE082)
val whiteColor = Color(0xCCFFFFFF)
val redColor = Color(0xFFB71C1C)
val strokeWidth = 4f
Canvas(modifier = Modifier.fillMaxSize()) {
val radius = (size.minDimension / 2f) * 0.95f
if (items.isNotEmpty() && !isTrackCreated) {
newTrackPath.value = createPassTrajectory(items, radius)
newArrowPath.value = createPassTrajectoryArrow()
isTrackCreated = true
}
rotate(-orientation.first) {
drawRadarCirle(radius, whiteColor, strokeWidth)
drawRadarCross(radius, whiteColor, strokeWidth)
drawRadarText(radius, yellowColor, measurer)
translate(center.x, center.y) {
if (items.isNotEmpty()) {
val effect = createPathEffect(newTrackPath.value, newArrowPath.value)
drawPath(newTrackPath.value, redColor, style = Stroke(4f))
drawPath(newTrackPath.value, yellowColor, style = Stroke(pathEffect = effect))
}
drawSatellite(item, radius, yellowColor)
drawCrosshair(orientation.first, orientation.second, radius, strokeWidth)
}
}
}
}
private fun DrawScope.drawRadarCirle(radius: Float, color: Color, width: Float, circles: Int = 3) {
for (i in 0 until circles) {
val circleRadius = radius - radius / circles.toFloat() * i.toFloat()
drawCircle(color, circleRadius, style = Stroke(width))
}
}
private fun DrawScope.drawRadarCross(radius: Float, color: Color, width: Float) {
drawLine(color, center, center.copy(x = center.x - radius), strokeWidth = width)
drawLine(color, center, center.copy(x = center.x + radius), strokeWidth = width)
drawLine(color, center, center.copy(y = center.y - radius), strokeWidth = width)
drawLine(color, center, center.copy(y = center.y + radius), strokeWidth = width)
}
private fun DrawScope.drawRadarText(
radius: Float, color: Color, measurer: TextMeasurer, circles: Int = 3
) {
for (i in 0..circles) {
val textY = (radius - radius / circles * i) + 25f
val textDeg = " ${(90 / circles) * (circles - i)}°"
drawText(measurer, textDeg, center.copy(y = textY), style = TextStyle(color, 16.sp))
}
}
private fun DrawScope.drawSatellite(item: SatPos, radius: Float, color: Color) {
if (item.elevation > 0) {
val satX = sph2CartX(item.azimuth, item.elevation, radius.toDouble())
val satY = sph2CartY(item.azimuth, item.elevation, radius.toDouble())
drawCircle(color, 16f, center.copy(satX, -satY))
}
}
private fun DrawScope.drawCrosshair(azimuth: Float, elevation: Float, radius: Float, width: Float) {
val redColor = Color(0xFFFF0000)
val size = 36f
val azimuthRad = azimuth.toDouble().toRadians()
val tmpElevation = elevation.toDouble().toRadians()
val elevationRad = if (tmpElevation > 0.0) 0.0 else tmpElevation
val crossX = sph2CartX(azimuthRad, -elevationRad, radius.toDouble())
val crossY = sph2CartY(azimuthRad, -elevationRad, radius.toDouble())
drawLine(
redColor,
center.copy(crossX - size, -crossY),
center.copy(crossX + size, -crossY),
strokeWidth = width
)
drawLine(
redColor,
center.copy(crossX, -crossY - size),
center.copy(crossX, -crossY + size),
strokeWidth = width
)
drawCircle(redColor, size / 2, center.copy(crossX, -crossY), style = Stroke(width))
}
private fun createPassTrajectory(positions: List<SatPos>, radarRadius: Float): Path {
val trackPath = Path()
positions.forEachIndexed { index, satPos ->
val passX = sph2CartX(satPos.azimuth, satPos.elevation, radarRadius.toDouble())
val passY = sph2CartY(satPos.azimuth, satPos.elevation, radarRadius.toDouble())
if (index == 0) {
trackPath.moveTo(passX, -passY)
} else {
trackPath.lineTo(passX, -passY)
}
}
return trackPath
}
private fun createPassTrajectoryArrow(): Path {
val arrowPath = Path()
val radius = 24f
val sides = 3
val angle = TWO_PI / sides
arrowPath.moveTo((radius * cos(angle)).toFloat(), (radius * sin(angle)).toFloat())
for (i in 1 until sides) {
val x = (radius * cos(angle - angle * i)).toFloat()
val y = (radius * sin(angle - angle * i)).toFloat()
arrowPath.lineTo(x, y)
}
arrowPath.close()
return arrowPath
}
private fun createPathEffect(trackPath: Path, arrowPath: Path): PathEffect {
val trackLength = PathMeasure().apply { setPath(trackPath, false) }.length
val quarter = trackLength / 4f
val center = trackLength / 2f
return PathEffect.stampedPathEffect(arrowPath, center, quarter, StampedPathEffectStyle.Rotate)
}
private fun sph2CartX(azimuth: Double, elevation: Double, r: Double): Float {
val radius = r * (PI_2 - elevation) / PI_2
return (radius * cos(PI_2 - azimuth)).toFloat()
}
private fun sph2CartY(azimuth: Double, elevation: Double, r: Double): Float {
val radius = r * (PI_2 - elevation) / PI_2
return (radius * sin(PI_2 - azimuth)).toFloat()
}
@@ -19,8 +19,7 @@ package com.rtbishop.look4sat.presentation.radarScreen
import android.hardware.GeomagneticField import android.hardware.GeomagneticField
import android.util.Log import android.util.Log
import androidx.lifecycle.LiveData import androidx.lifecycle.SavedStateHandle
import androidx.lifecycle.MutableLiveData
import androidx.lifecycle.ViewModel import androidx.lifecycle.ViewModel
import androidx.lifecycle.viewModelScope import androidx.lifecycle.viewModelScope
import com.rtbishop.look4sat.domain.IDataRepository import com.rtbishop.look4sat.domain.IDataRepository
@@ -37,6 +36,8 @@ import com.rtbishop.look4sat.utility.round
import com.rtbishop.look4sat.utility.toDegrees import com.rtbishop.look4sat.utility.toDegrees
import dagger.hilt.android.lifecycle.HiltViewModel import dagger.hilt.android.lifecycle.HiltViewModel
import kotlinx.coroutines.delay import kotlinx.coroutines.delay
import kotlinx.coroutines.flow.MutableStateFlow
import kotlinx.coroutines.flow.StateFlow
import kotlinx.coroutines.flow.flow import kotlinx.coroutines.flow.flow
import kotlinx.coroutines.isActive import kotlinx.coroutines.isActive
import kotlinx.coroutines.launch import kotlinx.coroutines.launch
@@ -44,6 +45,7 @@ import javax.inject.Inject
@HiltViewModel @HiltViewModel
class RadarViewModel @Inject constructor( class RadarViewModel @Inject constructor(
private val savedStateHandle: SavedStateHandle,
private val orientationManager: OrientationManager, private val orientationManager: OrientationManager,
private val reporter: DataReporter, private val reporter: DataReporter,
private val btReporter: BTReporter, private val btReporter: BTReporter,
@@ -53,17 +55,29 @@ class RadarViewModel @Inject constructor(
) : ViewModel(), OrientationManager.OrientationListener { ) : ViewModel(), OrientationManager.OrientationListener {
private val stationPos = settings.loadStationPosition() private val stationPos = settings.loadStationPosition()
private val _passData = MutableLiveData<RadarData>() private val _passData = MutableStateFlow<RadarData?>(null)
private val _transmitters = MutableLiveData<List<SatRadio>>() private val _transmitters = MutableStateFlow<List<SatRadio>>(emptyList())
private val _orientation = MutableLiveData<Triple<Float, Float, Float>>() private val _orientation = MutableStateFlow<Triple<Float, Float, Float>?>(null)
val radarData: LiveData<RadarData> = _passData val radarData: StateFlow<RadarData?> = _passData
val transmitters: LiveData<List<SatRadio>> = _transmitters val transmitters: StateFlow<List<SatRadio>> = _transmitters
val orientation: LiveData<Triple<Float, Float, Float>> = _orientation val orientation: StateFlow<Triple<Float, Float, Float>?> = _orientation
fun getPass(catNum: Int, aosTime: Long) = flow { init {
enableSensor()
}
override fun onCleared() {
disableSensor()
super.onCleared()
}
fun getPass() = flow {
val catNum = savedStateHandle.get<Int>("catNum") ?: 0
val aosTime = savedStateHandle.get<Long>("aosTime") ?: 0L
satManager.calculatedPasses.collect { passes -> satManager.calculatedPasses.collect { passes ->
val pass = passes.find { pass -> pass.catNum == catNum && pass.aosTime == aosTime } val pass = passes.find { pass -> pass.catNum == catNum && pass.aosTime == aosTime }
pass?.let { satPass -> val currentPass = pass ?: passes.firstOrNull()
currentPass?.let { satPass ->
emit(satPass) emit(satPass)
val transmitters = repository.getRadiosWithId(satPass.catNum) val transmitters = repository.getRadiosWithId(satPass.catNum)
viewModelScope.launch { viewModelScope.launch {
@@ -115,7 +129,7 @@ class RadarViewModel @Inject constructor(
val elevation = satPos.elevation.toDegrees().round(1) val elevation = satPos.elevation.toDegrees().round(1)
reporter.reportRotation(server, port, azimuth, elevation) reporter.reportRotation(server, port, azimuth, elevation)
} }
_passData.postValue(RadarData(satPos, track)) _passData.value = RadarData(satPos, track)
} }
} }
@@ -140,7 +154,7 @@ class RadarViewModel @Inject constructor(
viewModelScope.launch { viewModelScope.launch {
delay(125) delay(125)
val list = satManager.processRadios(satellite, stationPos, radios, time) val list = satManager.processRadios(satellite, stationPos, radios, time)
_transmitters.postValue(list) _transmitters.value = list
} }
} }
} }