diff --git a/app/src/main/java/com/rtbishop/look4sat/presentation/MainActivity.kt b/app/src/main/java/com/rtbishop/look4sat/presentation/MainActivity.kt index 714dc738..49495583 100644 --- a/app/src/main/java/com/rtbishop/look4sat/presentation/MainActivity.kt +++ b/app/src/main/java/com/rtbishop/look4sat/presentation/MainActivity.kt @@ -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) } } diff --git a/app/src/main/java/com/rtbishop/look4sat/presentation/passesScreen/PassesScreen.kt b/app/src/main/java/com/rtbishop/look4sat/presentation/passesScreen/PassesScreen.kt index e07c4f7b..9f216c6e 100644 --- a/app/src/main/java/com/rtbishop/look4sat/presentation/passesScreen/PassesScreen.kt +++ b/app/src/main/java/com/rtbishop/look4sat/presentation/passesScreen/PassesScreen.kt @@ -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, - navToRadar: () -> Unit + navToRadar: (Int, Long) -> Unit ) { ElevatedCard(modifier = Modifier.fillMaxSize()) { Box(Modifier.pullRefresh(refreshState)) { diff --git a/app/src/main/java/com/rtbishop/look4sat/presentation/radarScreen/RadarScreen.kt b/app/src/main/java/com/rtbishop/look4sat/presentation/radarScreen/RadarScreen.kt index 07fb233b..cbc26b19 100644 --- a/app/src/main/java/com/rtbishop/look4sat/presentation/radarScreen/RadarScreen.kt +++ b/app/src/main/java/com/rtbishop/look4sat/presentation/radarScreen/RadarScreen.kt @@ -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) { + 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) -// } // } // } //} diff --git a/app/src/main/java/com/rtbishop/look4sat/presentation/radarScreen/RadarViewCompose.kt b/app/src/main/java/com/rtbishop/look4sat/presentation/radarScreen/RadarViewCompose.kt new file mode 100644 index 00000000..4cff8156 --- /dev/null +++ b/app/src/main/java/com/rtbishop/look4sat/presentation/radarScreen/RadarViewCompose.kt @@ -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, orientation: Triple) { + 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, 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() +} diff --git a/app/src/main/java/com/rtbishop/look4sat/presentation/radarScreen/RadarViewModel.kt b/app/src/main/java/com/rtbishop/look4sat/presentation/radarScreen/RadarViewModel.kt index 776815ae..def9e8ce 100644 --- a/app/src/main/java/com/rtbishop/look4sat/presentation/radarScreen/RadarViewModel.kt +++ b/app/src/main/java/com/rtbishop/look4sat/presentation/radarScreen/RadarViewModel.kt @@ -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() - private val _transmitters = MutableLiveData>() - private val _orientation = MutableLiveData>() - val radarData: LiveData = _passData - val transmitters: LiveData> = _transmitters - val orientation: LiveData> = _orientation + private val _passData = MutableStateFlow(null) + private val _transmitters = MutableStateFlow>(emptyList()) + private val _orientation = MutableStateFlow?>(null) + val radarData: StateFlow = _passData + val transmitters: StateFlow> = _transmitters + val orientation: StateFlow?> = _orientation - fun getPass(catNum: Int, aosTime: Long) = flow { + init { + enableSensor() + } + + override fun onCleared() { + disableSensor() + super.onCleared() + } + + fun getPass() = flow { + val catNum = savedStateHandle.get("catNum") ?: 0 + val aosTime = savedStateHandle.get("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 } } }