diff --git a/app/src/main/java/com/rtbishop/look4sat/presentation/MainComponents.kt b/app/src/main/java/com/rtbishop/look4sat/presentation/MainComponents.kt index 5c66ea00..aacb79bc 100644 --- a/app/src/main/java/com/rtbishop/look4sat/presentation/MainComponents.kt +++ b/app/src/main/java/com/rtbishop/look4sat/presentation/MainComponents.kt @@ -3,18 +3,28 @@ package com.rtbishop.look4sat.presentation import android.content.Context import android.content.Intent 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.interaction.Interaction import androidx.compose.foundation.interaction.MutableInteractionSource +import androidx.compose.foundation.layout.Box 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.ElevatedButton import androidx.compose.material3.MaterialTheme import androidx.compose.material3.Text import androidx.compose.runtime.Composable +import androidx.compose.runtime.LaunchedEffect +import androidx.compose.runtime.mutableStateOf import androidx.compose.runtime.remember import androidx.compose.ui.Modifier import androidx.compose.ui.composed +import androidx.compose.ui.draw.scale import androidx.compose.ui.unit.dp import androidx.compose.ui.unit.sp import kotlinx.coroutines.flow.Flow @@ -30,6 +40,28 @@ fun CardButton(onClick: () -> Unit, text: String, modifier: Modifier = Modifier) ) { 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) { context.startActivity(Intent(Intent.ACTION_VIEW, Uri.parse(url))) } diff --git a/app/src/main/java/com/rtbishop/look4sat/presentation/bottomNav/BottomNavBar.kt b/app/src/main/java/com/rtbishop/look4sat/presentation/bottomNav/BottomNavBar.kt index 930d0ed6..4554f5b5 100644 --- a/app/src/main/java/com/rtbishop/look4sat/presentation/bottomNav/BottomNavBar.kt +++ b/app/src/main/java/com/rtbishop/look4sat/presentation/bottomNav/BottomNavBar.kt @@ -19,6 +19,7 @@ import androidx.navigation.compose.rememberNavController import com.rtbishop.look4sat.presentation.MainTheme import com.rtbishop.look4sat.presentation.aboutScreen.AboutScreen import com.rtbishop.look4sat.presentation.entriesScreen.EntriesScreen +import com.rtbishop.look4sat.presentation.passesScreen.PassesScreen @Composable fun BottomNavBar(navController: NavController) { @@ -52,8 +53,8 @@ fun NavigationGraph(navController: NavHostController) { NavHost(navController, startDestination = BottomNavItem.Passes.screen_route) { composable(BottomNavItem.Satellites.screen_route) { EntriesScreen(navToPasses) } composable(BottomNavItem.Passes.screen_route) { PassesScreen() } - composable(BottomNavItem.WorldMap.screen_route) { WorldMapScreen() } - composable(BottomNavItem.Settings.screen_route) { SettingsScreen() } + composable(BottomNavItem.WorldMap.screen_route) {} + composable(BottomNavItem.Settings.screen_route) {} composable(BottomNavItem.About.screen_route) { AboutScreen() } } } diff --git a/app/src/main/java/com/rtbishop/look4sat/presentation/bottomNav/BottomNavScreens.kt b/app/src/main/java/com/rtbishop/look4sat/presentation/bottomNav/BottomNavScreens.kt deleted file mode 100644 index 3a4ee9ba..00000000 --- a/app/src/main/java/com/rtbishop/look4sat/presentation/bottomNav/BottomNavScreens.kt +++ /dev/null @@ -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 - ) - } -} 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 new file mode 100644 index 00000000..5aba142d --- /dev/null +++ b/app/src/main/java/com/rtbishop/look4sat/presentation/passesScreen/PassesScreen.kt @@ -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>) { + 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)) + } + } + } + } +} diff --git a/base/src/main/java/com/rtbishop/look4sat/domain/predict/SatPass.kt b/base/src/main/java/com/rtbishop/look4sat/domain/predict/SatPass.kt index bf513399..018987b8 100644 --- a/base/src/main/java/com/rtbishop/look4sat/domain/predict/SatPass.kt +++ b/base/src/main/java/com/rtbishop/look4sat/domain/predict/SatPass.kt @@ -22,9 +22,9 @@ data class SatPass( val aosAzimuth: Double, val losTime: Long, val losAzimuth: Double, - val tcaTime: Long, - val tcaAzimuth: Double, - val altitude: Double, +// val tcaTime: Long, +// val tcaAzimuth: Double, + val altitude: Int, val maxElevation: Double, val satellite: Satellite, var progress: Int = 0 diff --git a/base/src/main/java/com/rtbishop/look4sat/domain/predict/SatelliteManager.kt b/base/src/main/java/com/rtbishop/look4sat/domain/predict/SatelliteManager.kt index 6c1fbfcb..e2e55366 100644 --- a/base/src/main/java/com/rtbishop/look4sat/domain/predict/SatelliteManager.kt +++ b/base/src/main/java/com/rtbishop/look4sat/domain/predict/SatelliteManager.kt @@ -19,6 +19,7 @@ package com.rtbishop.look4sat.domain.predict import com.rtbishop.look4sat.domain.ISatelliteManager import com.rtbishop.look4sat.domain.model.SatRadio +import com.rtbishop.look4sat.utility.round import com.rtbishop.look4sat.utility.toDegrees import kotlinx.coroutines.CoroutineDispatcher import kotlinx.coroutines.flow.MutableSharedFlow @@ -38,10 +39,7 @@ class SatelliteManager(private val defaultDispatcher: CoroutineDispatcher) : ISa } override suspend fun getTrack( - sat: Satellite, - pos: GeoPos, - start: Long, - end: Long + sat: Satellite, pos: GeoPos, start: Long, end: Long ): List { return withContext(defaultDispatcher) { val positions = mutableListOf() @@ -55,10 +53,7 @@ class SatelliteManager(private val defaultDispatcher: CoroutineDispatcher) : ISa } override suspend fun processRadios( - sat: Satellite, - pos: GeoPos, - radios: List, - time: Long + sat: Satellite, pos: GeoPos, radios: List, time: Long ): List { return withContext(defaultDispatcher) { val satPos = sat.getPosition(pos, time) @@ -92,11 +87,7 @@ class SatelliteManager(private val defaultDispatcher: CoroutineDispatcher) : ISa } override suspend fun calculatePasses( - satList: List, - pos: GeoPos, - time: Long, - hoursAhead: Int, - minElevation: Double + satList: List, pos: GeoPos, time: Long, hoursAhead: Int, minElevation: Double ) { if (satList.isEmpty()) { val newPasses = emptyList() @@ -142,21 +133,19 @@ class SatelliteManager(private val defaultDispatcher: CoroutineDispatcher) : ISa private fun List.filter(time: Long, hoursAhead: Int, minElev: Double): List { val timeFuture = time + (hoursAhead * 60L * 60L * 1000L) - return this.filter { it.losTime > time } - .filter { it.aosTime < timeFuture } - .filter { it.maxElevation > minElev } - .sortedBy { it.aosTime } + return this.filter { it.losTime > time }.filter { it.aosTime < timeFuture } + .filter { it.maxElevation > minElev }.sortedBy { it.aosTime } } private fun getGeoPass(sat: Satellite, pos: GeoPos, time: Long): SatPass { val satPos = sat.getPosition(pos, time) val aos = time - 24 * 60L * 60L * 1000L val los = time + 24 * 60L * 60L * 1000L - val tca = (aos + los) / 2 - val az = satPos.azimuth.toDegrees() - val elev = satPos.elevation.toDegrees() +// val tca = (aos + los) / 2 + val az = satPos.azimuth.toDegrees().round(1) + val elev = satPos.elevation.toDegrees().round(1) 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 { @@ -165,7 +154,7 @@ class SatelliteManager(private val defaultDispatcher: CoroutineDispatcher) : ISa var elevation: Double var maxElevation = 0.0 var alt = 0.0 - var tcaAz = 0.0 +// var tcaAz = 0.0 // rewind 1/4 of an orbit if (rewind) calendarTimeMillis += -quarterOrbitMin * 60L * 1000L @@ -188,7 +177,7 @@ class SatelliteManager(private val defaultDispatcher: CoroutineDispatcher) : ISa if (elevation > maxElevation) { maxElevation = elevation alt = satPos.altitude - tcaAz = satPos.azimuth.toDegrees() +// tcaAz = satPos.azimuth.toDegrees() } } while (satPos.elevation < 0.0) @@ -201,12 +190,12 @@ class SatelliteManager(private val defaultDispatcher: CoroutineDispatcher) : ISa if (elevation > maxElevation) { maxElevation = elevation alt = satPos.altitude - tcaAz = satPos.azimuth.toDegrees() +// tcaAz = satPos.azimuth.toDegrees() } } while (satPos.elevation < 0.0) val aos = satPos.time - val aosAz = satPos.azimuth.toDegrees() + val aosAz = satPos.azimuth.toDegrees().round(1) // find when sat goes below do { @@ -216,7 +205,7 @@ class SatelliteManager(private val defaultDispatcher: CoroutineDispatcher) : ISa if (elevation > maxElevation) { maxElevation = elevation alt = satPos.altitude - tcaAz = satPos.azimuth.toDegrees() +// tcaAz = satPos.azimuth.toDegrees() } } while (satPos.elevation > 0.0) @@ -229,14 +218,14 @@ class SatelliteManager(private val defaultDispatcher: CoroutineDispatcher) : ISa if (elevation > maxElevation) { maxElevation = elevation alt = satPos.altitude - tcaAz = satPos.azimuth.toDegrees() +// tcaAz = satPos.azimuth.toDegrees() } } while (satPos.elevation > 0.0) val los = satPos.time - val losAz = satPos.azimuth.toDegrees() - val tca = (aos + los) / 2 - val elev = maxElevation.toDegrees() - return SatPass(aos, aosAz, los, losAz, tca, tcaAz, alt, elev, sat) + val losAz = satPos.azimuth.toDegrees().round(1) +// val tca = (aos + los) / 2 + val elev = maxElevation.toDegrees().round(1) + return SatPass(aos, aosAz, los, losAz, alt.toInt(), elev, sat) } }