Added navigation parameters to RadarScreen

pull/122/head
Arty Bishop 2023-03-05 11:49:08 +00:00
rodzic 5a8a57e314
commit 521c365376
Nie znaleziono w bazie danych klucza dla tego podpisu
ID klucza GPG: 5C71CFDC37AD73CC
5 zmienionych plików z 308 dodań i 43 usunięć

Wyświetl plik

@ -39,6 +39,7 @@ import androidx.navigation.compose.NavHost
import androidx.navigation.compose.composable
import androidx.navigation.compose.currentBackStackEntryAsState
import androidx.navigation.compose.rememberNavController
import androidx.navigation.navArgument
import com.rtbishop.look4sat.R
import com.rtbishop.look4sat.presentation.entriesScreen.EntriesScreen
import com.rtbishop.look4sat.presentation.mapScreen.MapScreen
@ -89,7 +90,7 @@ private fun MainNavBar(navController: NavController) {
val navBackStackEntry = navController.currentBackStackEntryAsState()
val currentRoute = navBackStackEntry.value?.destination?.route
items.forEach { item ->
NavigationBarItem(selected = currentRoute == item.route, onClick = {
NavigationBarItem(selected = currentRoute?.contains(item.route) ?: false, onClick = {
navController.navigate(item.route) {
navController.graph.startDestinationRoute?.let { route ->
popUpTo(route) { saveState = false }
@ -104,10 +105,13 @@ private fun MainNavBar(navController: NavController) {
@Composable
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) {
composable(Screen.Entries.route) { EntriesScreen(navController) }
composable(Screen.Passes.route) { PassesScreen(navController) }
composable(Screen.Radar.route) { RadarScreen() }
composable(radarRoute, radarArgs) { RadarScreen(navController) }
composable(Screen.Map.route) { MapScreen() }
composable(Screen.Settings.route) { SettingsScreen(navController) }
}

Wyświetl plik

@ -41,7 +41,9 @@ private val sdf = SimpleDateFormat("HH:mm:ss", Locale.ENGLISH)
@Composable
fun PassesScreen(navController: NavController) {
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 timerText = viewModel.timerText.collectAsState(initial = null)
val isRefreshing = state.value is DataState.Loading
@ -105,15 +107,15 @@ private fun PassPreview() {
)
val satellite = NearEarthSat(data)
val pass = SatPass(1L, 25.0, 10L, 75.0, 850, 45.0, satellite, 0.5f)
MainTheme { Pass(pass = pass, {}) }
MainTheme { Pass(pass = pass, { _,_ -> }) }
}
@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(modifier = Modifier
.padding(bottom = 2.dp)
.clickable { navToRadar() }) {
.clickable { navToRadar(pass.catNum, pass.aosTime) }) {
Column(
verticalArrangement = Arrangement.spacedBy(2.dp),
modifier = Modifier
@ -202,7 +204,7 @@ private fun PassesCard(
refreshState: PullRefreshState,
isRefreshing: Boolean,
passes: List<SatPass>,
navToRadar: () -> Unit
navToRadar: (Int, Long) -> Unit
) {
ElevatedCard(modifier = Modifier.fillMaxSize()) {
Box(Modifier.pullRefresh(refreshState)) {

Wyświetl plik

@ -1,20 +1,35 @@
package com.rtbishop.look4sat.presentation.radarScreen
import androidx.compose.foundation.background
import androidx.compose.foundation.layout.*
import androidx.compose.foundation.lazy.LazyColumn
import androidx.compose.foundation.lazy.items
import androidx.compose.material3.*
import androidx.compose.runtime.Composable
import androidx.compose.runtime.collectAsState
import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier
import androidx.compose.ui.platform.LocalContext
import androidx.compose.ui.res.painterResource
import androidx.compose.ui.res.stringResource
import androidx.compose.ui.text.font.FontWeight
import androidx.compose.ui.text.style.TextAlign
import androidx.compose.ui.unit.dp
import androidx.compose.ui.unit.sp
import androidx.compose.ui.viewinterop.AndroidView
import androidx.hilt.navigation.compose.hiltViewModel
import androidx.navigation.NavController
import com.rtbishop.look4sat.R
import com.rtbishop.look4sat.domain.model.SatRadio
import timber.log.Timber
@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)) {
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)
// 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 ->
// 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)
// }
// }
// }
//}

Wyświetl plik

@ -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()
}

Wyświetl plik

@ -19,8 +19,7 @@ package com.rtbishop.look4sat.presentation.radarScreen
import android.hardware.GeomagneticField
import android.util.Log
import androidx.lifecycle.LiveData
import androidx.lifecycle.MutableLiveData
import androidx.lifecycle.SavedStateHandle
import androidx.lifecycle.ViewModel
import androidx.lifecycle.viewModelScope
import com.rtbishop.look4sat.domain.IDataRepository
@ -37,6 +36,8 @@ import com.rtbishop.look4sat.utility.round
import com.rtbishop.look4sat.utility.toDegrees
import dagger.hilt.android.lifecycle.HiltViewModel
import kotlinx.coroutines.delay
import kotlinx.coroutines.flow.MutableStateFlow
import kotlinx.coroutines.flow.StateFlow
import kotlinx.coroutines.flow.flow
import kotlinx.coroutines.isActive
import kotlinx.coroutines.launch
@ -44,6 +45,7 @@ import javax.inject.Inject
@HiltViewModel
class RadarViewModel @Inject constructor(
private val savedStateHandle: SavedStateHandle,
private val orientationManager: OrientationManager,
private val reporter: DataReporter,
private val btReporter: BTReporter,
@ -53,17 +55,29 @@ class RadarViewModel @Inject constructor(
) : ViewModel(), OrientationManager.OrientationListener {
private val stationPos = settings.loadStationPosition()
private val _passData = MutableLiveData<RadarData>()
private val _transmitters = MutableLiveData<List<SatRadio>>()
private val _orientation = MutableLiveData<Triple<Float, Float, Float>>()
val radarData: LiveData<RadarData> = _passData
val transmitters: LiveData<List<SatRadio>> = _transmitters
val orientation: LiveData<Triple<Float, Float, Float>> = _orientation
private val _passData = MutableStateFlow<RadarData?>(null)
private val _transmitters = MutableStateFlow<List<SatRadio>>(emptyList())
private val _orientation = MutableStateFlow<Triple<Float, Float, Float>?>(null)
val radarData: StateFlow<RadarData?> = _passData
val transmitters: StateFlow<List<SatRadio>> = _transmitters
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 ->
val pass = passes.find { pass -> pass.catNum == catNum && pass.aosTime == aosTime }
pass?.let { satPass ->
val currentPass = pass ?: passes.firstOrNull()
currentPass?.let { satPass ->
emit(satPass)
val transmitters = repository.getRadiosWithId(satPass.catNum)
viewModelScope.launch {
@ -115,7 +129,7 @@ class RadarViewModel @Inject constructor(
val elevation = satPos.elevation.toDegrees().round(1)
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 {
delay(125)
val list = satManager.processRadios(satellite, stationPos, radios, time)
_transmitters.postValue(list)
_transmitters.value = list
}
}
}