Initial pass item Compose implementation

This commit is contained in:
Arty Bishop committed 2025-12-27 15:01:32 +00:00
1 parent f13662747c
commit ac479f80ee
6 files changed
+168 -109

No files matched your search

@@ -3,18 +3,28 @@ package com.rtbishop.look4sat.presentation
import android.content.Context import android.content.Context
import android.content.Intent import android.content.Intent
import android.net.Uri import android.net.Uri
import androidx.compose.animation.core.animateFloatAsState
import androidx.compose.animation.core.infiniteRepeatable
import androidx.compose.animation.core.tween
import androidx.compose.foundation.border
import androidx.compose.foundation.clickable import androidx.compose.foundation.clickable
import androidx.compose.foundation.interaction.Interaction import androidx.compose.foundation.interaction.Interaction
import androidx.compose.foundation.interaction.MutableInteractionSource import androidx.compose.foundation.interaction.MutableInteractionSource
import androidx.compose.foundation.layout.Box
import androidx.compose.foundation.layout.padding import androidx.compose.foundation.layout.padding
import androidx.compose.foundation.layout.size
import androidx.compose.foundation.shape.CircleShape
import androidx.compose.material3.ButtonDefaults import androidx.compose.material3.ButtonDefaults
import androidx.compose.material3.ElevatedButton import androidx.compose.material3.ElevatedButton
import androidx.compose.material3.MaterialTheme import androidx.compose.material3.MaterialTheme
import androidx.compose.material3.Text import androidx.compose.material3.Text
import androidx.compose.runtime.Composable import androidx.compose.runtime.Composable
import androidx.compose.runtime.LaunchedEffect
import androidx.compose.runtime.mutableStateOf
import androidx.compose.runtime.remember import androidx.compose.runtime.remember
import androidx.compose.ui.Modifier import androidx.compose.ui.Modifier
import androidx.compose.ui.composed import androidx.compose.ui.composed
import androidx.compose.ui.draw.scale
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 kotlinx.coroutines.flow.Flow import kotlinx.coroutines.flow.Flow
@@ -30,6 +40,28 @@ fun CardButton(onClick: () -> Unit, text: String, modifier: Modifier = Modifier)
) { Text(text = text, fontSize = 17.sp) } ) { Text(text = text, fontSize = 17.sp) }
} }
@Composable
fun RadarPing() {
val pingColor = MaterialTheme.colorScheme.primary
val pingDurationMs = 1000
val scale = remember { mutableStateOf(0f) }
val scaleAnimation = animateFloatAsState(
targetValue = scale.value,
animationSpec = infiniteRepeatable(animation = tween(durationMillis = pingDurationMs))
)
LaunchedEffect(Unit) { scale.value = 1f }
Box(
modifier = Modifier
.size(size = 48.dp)
.scale(scale = scaleAnimation.value)
.border(
width = 4.dp,
shape = CircleShape,
color = pingColor.copy(alpha = 1 - scaleAnimation.value)
)
)
}
fun gotoUrl(context: Context, url: String) { fun gotoUrl(context: Context, url: String) {
context.startActivity(Intent(Intent.ACTION_VIEW, Uri.parse(url))) context.startActivity(Intent(Intent.ACTION_VIEW, Uri.parse(url)))
} }
@@ -19,6 +19,7 @@ import androidx.navigation.compose.rememberNavController
import com.rtbishop.look4sat.presentation.MainTheme import com.rtbishop.look4sat.presentation.MainTheme
import com.rtbishop.look4sat.presentation.aboutScreen.AboutScreen import com.rtbishop.look4sat.presentation.aboutScreen.AboutScreen
import com.rtbishop.look4sat.presentation.entriesScreen.EntriesScreen import com.rtbishop.look4sat.presentation.entriesScreen.EntriesScreen
import com.rtbishop.look4sat.presentation.passesScreen.PassesScreen
@Composable @Composable
fun BottomNavBar(navController: NavController) { fun BottomNavBar(navController: NavController) {
@@ -52,8 +53,8 @@ fun NavigationGraph(navController: NavHostController) {
NavHost(navController, startDestination = BottomNavItem.Passes.screen_route) { NavHost(navController, startDestination = BottomNavItem.Passes.screen_route) {
composable(BottomNavItem.Satellites.screen_route) { EntriesScreen(navToPasses) } composable(BottomNavItem.Satellites.screen_route) { EntriesScreen(navToPasses) }
composable(BottomNavItem.Passes.screen_route) { PassesScreen() } composable(BottomNavItem.Passes.screen_route) { PassesScreen() }
composable(BottomNavItem.WorldMap.screen_route) { WorldMapScreen() } composable(BottomNavItem.WorldMap.screen_route) {}
composable(BottomNavItem.Settings.screen_route) { SettingsScreen() } composable(BottomNavItem.Settings.screen_route) {}
composable(BottomNavItem.About.screen_route) { AboutScreen() } composable(BottomNavItem.About.screen_route) { AboutScreen() }
} }
} }
@@ -1,73 +0,0 @@
package com.rtbishop.look4sat.presentation.bottomNav
import androidx.compose.foundation.background
import androidx.compose.foundation.layout.Column
import androidx.compose.foundation.layout.fillMaxSize
import androidx.compose.foundation.layout.wrapContentSize
import androidx.compose.material3.Text
import androidx.compose.runtime.Composable
import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier
import androidx.compose.ui.graphics.Color
import androidx.compose.ui.res.colorResource
import androidx.compose.ui.text.font.FontWeight
import androidx.compose.ui.text.style.TextAlign
import androidx.compose.ui.unit.sp
import com.rtbishop.look4sat.R
@Composable
fun PassesScreen() {
Column(
modifier = Modifier
.fillMaxSize()
.background(colorResource(id = R.color.background))
.wrapContentSize(Alignment.Center)
) {
Text(
text = "Passes",
fontWeight = FontWeight.Bold,
color = Color.White,
modifier = Modifier.align(Alignment.CenterHorizontally),
textAlign = TextAlign.Center,
fontSize = 20.sp
)
}
}
@Composable
fun WorldMapScreen() {
Column(
modifier = Modifier
.fillMaxSize()
.background(colorResource(id = R.color.background))
.wrapContentSize(Alignment.Center)
) {
Text(
text = "World Map",
fontWeight = FontWeight.Bold,
color = Color.White,
modifier = Modifier.align(Alignment.CenterHorizontally),
textAlign = TextAlign.Center,
fontSize = 20.sp
)
}
}
@Composable
fun SettingsScreen() {
Column(
modifier = Modifier
.fillMaxSize()
.background(colorResource(id = R.color.background))
.wrapContentSize(Alignment.Center)
) {
Text(
text = "Settings",
fontWeight = FontWeight.Bold,
color = Color.White,
modifier = Modifier.align(Alignment.CenterHorizontally),
textAlign = TextAlign.Center,
fontSize = 20.sp
)
}
}
@@ -0,0 +1,110 @@
package com.rtbishop.look4sat.presentation.passesScreen
import androidx.compose.foundation.ExperimentalFoundationApi
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.livedata.observeAsState
import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier
import androidx.compose.ui.text.font.FontWeight
import androidx.compose.ui.text.style.TextOverflow
import androidx.compose.ui.tooling.preview.Preview
import androidx.compose.ui.unit.dp
import androidx.hilt.navigation.compose.hiltViewModel
import com.rtbishop.look4sat.domain.model.DataState
import com.rtbishop.look4sat.domain.predict.NearEarthSat
import com.rtbishop.look4sat.domain.predict.OrbitalData
import com.rtbishop.look4sat.domain.predict.SatPass
import com.rtbishop.look4sat.presentation.MainTheme
@Composable
fun PassesScreen(viewModel: PassesViewModel = hiltViewModel()) {
val state = viewModel.passes.observeAsState(DataState.Loading)
Column(modifier = Modifier.padding(6.dp), verticalArrangement = Arrangement.spacedBy(6.dp)) {
PassesCard(state = state.value)
}
}
@Preview(showBackground = true)
@Composable
private fun PassPreview() {
val data = OrbitalData(
"Satellite", 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 45000, 0.0
)
val satellite = NearEarthSat(data)
val pass = SatPass(1L, 25.0, 10L, 75.0, 850, 45.0, satellite, 150)
MainTheme { Pass(pass = pass) }
}
@Composable
private fun Pass(pass: SatPass, modifier: Modifier = Modifier) {
Surface(color = MaterialTheme.colorScheme.background, modifier = modifier) {
Surface(modifier = Modifier.padding(bottom = 1.dp)) {
Column(
verticalArrangement = Arrangement.spacedBy(2.dp),
modifier = Modifier
.background(MaterialTheme.colorScheme.surface)
.padding(6.dp)
) {
Row(verticalAlignment = Alignment.CenterVertically) {
Text(
text = pass.name,
modifier = Modifier
.weight(1f)
.padding(end = 6.dp),
fontWeight = FontWeight.Medium,
maxLines = 1,
overflow = TextOverflow.Ellipsis
)
Text(
text = "Id:${pass.catNum}", color = MaterialTheme.colorScheme.secondary
)
}
Row(
verticalAlignment = Alignment.CenterVertically,
horizontalArrangement = Arrangement.SpaceBetween,
modifier = Modifier.fillMaxWidth()
) {
Text(text = "AOS - 90")
Text(text = "Altitude: 1800 km")
Text(text = "180 - LOS")
}
Row(
verticalAlignment = Alignment.CenterVertically,
horizontalArrangement = Arrangement.SpaceBetween,
modifier = Modifier.fillMaxWidth()
) {
Text(text = "16:02:28 - Sun")
Text(text = "Elevation: 75")
Text(text = "Sun - 16:02:28")
}
LinearProgressIndicator(progress = 0.5f, modifier.fillMaxWidth())
}
}
}
}
@OptIn(ExperimentalFoundationApi::class)
@Composable
private fun PassesCard(state: DataState<List<SatPass>>) {
ElevatedCard(modifier = Modifier.fillMaxSize()) {
when (state) {
is DataState.Success -> {
LazyColumn {
items(items = state.data, key = { item -> item.aosTime }) { pass ->
Pass(pass, Modifier.animateItemPlacement())
}
}
}
else -> {
Box(contentAlignment = Alignment.Center, modifier = Modifier.fillMaxSize()) {
CircularProgressIndicator(modifier = Modifier.size(80.dp))
}
}
}
}
}
@@ -22,9 +22,9 @@ data class SatPass(
val aosAzimuth: Double, val aosAzimuth: Double,
val losTime: Long, val losTime: Long,
val losAzimuth: Double, val losAzimuth: Double,
val tcaTime: Long, // val tcaTime: Long,
val tcaAzimuth: Double, // val tcaAzimuth: Double,
val altitude: Double, val altitude: Int,
val maxElevation: Double, val maxElevation: Double,
val satellite: Satellite, val satellite: Satellite,
var progress: Int = 0 var progress: Int = 0
@@ -19,6 +19,7 @@ package com.rtbishop.look4sat.domain.predict
import com.rtbishop.look4sat.domain.ISatelliteManager import com.rtbishop.look4sat.domain.ISatelliteManager
import com.rtbishop.look4sat.domain.model.SatRadio import com.rtbishop.look4sat.domain.model.SatRadio
import com.rtbishop.look4sat.utility.round
import com.rtbishop.look4sat.utility.toDegrees import com.rtbishop.look4sat.utility.toDegrees
import kotlinx.coroutines.CoroutineDispatcher import kotlinx.coroutines.CoroutineDispatcher
import kotlinx.coroutines.flow.MutableSharedFlow import kotlinx.coroutines.flow.MutableSharedFlow
@@ -38,10 +39,7 @@ class SatelliteManager(private val defaultDispatcher: CoroutineDispatcher) : ISa
} }
override suspend fun getTrack( override suspend fun getTrack(
sat: Satellite, sat: Satellite, pos: GeoPos, start: Long, end: Long
pos: GeoPos,
start: Long,
end: Long
): List<SatPos> { ): List<SatPos> {
return withContext(defaultDispatcher) { return withContext(defaultDispatcher) {
val positions = mutableListOf<SatPos>() val positions = mutableListOf<SatPos>()
@@ -55,10 +53,7 @@ class SatelliteManager(private val defaultDispatcher: CoroutineDispatcher) : ISa
} }
override suspend fun processRadios( override suspend fun processRadios(
sat: Satellite, sat: Satellite, pos: GeoPos, radios: List<SatRadio>, time: Long
pos: GeoPos,
radios: List<SatRadio>,
time: Long
): List<SatRadio> { ): List<SatRadio> {
return withContext(defaultDispatcher) { return withContext(defaultDispatcher) {
val satPos = sat.getPosition(pos, time) val satPos = sat.getPosition(pos, time)
@@ -92,11 +87,7 @@ class SatelliteManager(private val defaultDispatcher: CoroutineDispatcher) : ISa
} }
override suspend fun calculatePasses( override suspend fun calculatePasses(
satList: List<Satellite>, satList: List<Satellite>, pos: GeoPos, time: Long, hoursAhead: Int, minElevation: Double
pos: GeoPos,
time: Long,
hoursAhead: Int,
minElevation: Double
) { ) {
if (satList.isEmpty()) { if (satList.isEmpty()) {
val newPasses = emptyList<SatPass>() val newPasses = emptyList<SatPass>()
@@ -142,21 +133,19 @@ class SatelliteManager(private val defaultDispatcher: CoroutineDispatcher) : ISa
private fun List<SatPass>.filter(time: Long, hoursAhead: Int, minElev: Double): List<SatPass> { private fun List<SatPass>.filter(time: Long, hoursAhead: Int, minElev: Double): List<SatPass> {
val timeFuture = time + (hoursAhead * 60L * 60L * 1000L) val timeFuture = time + (hoursAhead * 60L * 60L * 1000L)
return this.filter { it.losTime > time } return this.filter { it.losTime > time }.filter { it.aosTime < timeFuture }
.filter { it.aosTime < timeFuture } .filter { it.maxElevation > minElev }.sortedBy { it.aosTime }
.filter { it.maxElevation > minElev }
.sortedBy { it.aosTime }
} }
private fun getGeoPass(sat: Satellite, pos: GeoPos, time: Long): SatPass { private fun getGeoPass(sat: Satellite, pos: GeoPos, time: Long): SatPass {
val satPos = sat.getPosition(pos, time) val satPos = sat.getPosition(pos, time)
val aos = time - 24 * 60L * 60L * 1000L val aos = time - 24 * 60L * 60L * 1000L
val los = time + 24 * 60L * 60L * 1000L val los = time + 24 * 60L * 60L * 1000L
val tca = (aos + los) / 2 // val tca = (aos + los) / 2
val az = satPos.azimuth.toDegrees() val az = satPos.azimuth.toDegrees().round(1)
val elev = satPos.elevation.toDegrees() val elev = satPos.elevation.toDegrees().round(1)
val alt = satPos.altitude val alt = satPos.altitude
return SatPass(aos, az, los, az, tca, az, alt, elev, sat) return SatPass(aos, az, los, az, alt.toInt(), elev, sat)
} }
private fun getLeoPass(sat: Satellite, pos: GeoPos, time: Long, rewind: Boolean): SatPass { private fun getLeoPass(sat: Satellite, pos: GeoPos, time: Long, rewind: Boolean): SatPass {
@@ -165,7 +154,7 @@ class SatelliteManager(private val defaultDispatcher: CoroutineDispatcher) : ISa
var elevation: Double var elevation: Double
var maxElevation = 0.0 var maxElevation = 0.0
var alt = 0.0 var alt = 0.0
var tcaAz = 0.0 // var tcaAz = 0.0
// rewind 1/4 of an orbit // rewind 1/4 of an orbit
if (rewind) calendarTimeMillis += -quarterOrbitMin * 60L * 1000L if (rewind) calendarTimeMillis += -quarterOrbitMin * 60L * 1000L
@@ -188,7 +177,7 @@ class SatelliteManager(private val defaultDispatcher: CoroutineDispatcher) : ISa
if (elevation > maxElevation) { if (elevation > maxElevation) {
maxElevation = elevation maxElevation = elevation
alt = satPos.altitude alt = satPos.altitude
tcaAz = satPos.azimuth.toDegrees() // tcaAz = satPos.azimuth.toDegrees()
} }
} while (satPos.elevation < 0.0) } while (satPos.elevation < 0.0)
@@ -201,12 +190,12 @@ class SatelliteManager(private val defaultDispatcher: CoroutineDispatcher) : ISa
if (elevation > maxElevation) { if (elevation > maxElevation) {
maxElevation = elevation maxElevation = elevation
alt = satPos.altitude alt = satPos.altitude
tcaAz = satPos.azimuth.toDegrees() // tcaAz = satPos.azimuth.toDegrees()
} }
} while (satPos.elevation < 0.0) } while (satPos.elevation < 0.0)
val aos = satPos.time val aos = satPos.time
val aosAz = satPos.azimuth.toDegrees() val aosAz = satPos.azimuth.toDegrees().round(1)
// find when sat goes below // find when sat goes below
do { do {
@@ -216,7 +205,7 @@ class SatelliteManager(private val defaultDispatcher: CoroutineDispatcher) : ISa
if (elevation > maxElevation) { if (elevation > maxElevation) {
maxElevation = elevation maxElevation = elevation
alt = satPos.altitude alt = satPos.altitude
tcaAz = satPos.azimuth.toDegrees() // tcaAz = satPos.azimuth.toDegrees()
} }
} while (satPos.elevation > 0.0) } while (satPos.elevation > 0.0)
@@ -229,14 +218,14 @@ class SatelliteManager(private val defaultDispatcher: CoroutineDispatcher) : ISa
if (elevation > maxElevation) { if (elevation > maxElevation) {
maxElevation = elevation maxElevation = elevation
alt = satPos.altitude alt = satPos.altitude
tcaAz = satPos.azimuth.toDegrees() // tcaAz = satPos.azimuth.toDegrees()
} }
} while (satPos.elevation > 0.0) } while (satPos.elevation > 0.0)
val los = satPos.time val los = satPos.time
val losAz = satPos.azimuth.toDegrees() val losAz = satPos.azimuth.toDegrees().round(1)
val tca = (aos + los) / 2 // val tca = (aos + los) / 2
val elev = maxElevation.toDegrees() val elev = maxElevation.toDegrees().round(1)
return SatPass(aos, aosAz, los, losAz, tca, tcaAz, alt, elev, sat) return SatPass(aos, aosAz, los, losAz, alt.toInt(), elev, sat)
} }
} }