kopia lustrzana https://github.com/rt-bishop/Look4Sat
Initial pass item Compose implementation
rodzic
a3fd124fed
commit
e749c9ac2e
|
@ -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)))
|
||||
}
|
||||
|
|
|
@ -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() }
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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 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
|
||||
|
|
|
@ -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<SatPos> {
|
||||
return withContext(defaultDispatcher) {
|
||||
val positions = mutableListOf<SatPos>()
|
||||
|
@ -55,10 +53,7 @@ class SatelliteManager(private val defaultDispatcher: CoroutineDispatcher) : ISa
|
|||
}
|
||||
|
||||
override suspend fun processRadios(
|
||||
sat: Satellite,
|
||||
pos: GeoPos,
|
||||
radios: List<SatRadio>,
|
||||
time: Long
|
||||
sat: Satellite, pos: GeoPos, radios: List<SatRadio>, time: Long
|
||||
): List<SatRadio> {
|
||||
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<Satellite>,
|
||||
pos: GeoPos,
|
||||
time: Long,
|
||||
hoursAhead: Int,
|
||||
minElevation: Double
|
||||
satList: List<Satellite>, pos: GeoPos, time: Long, hoursAhead: Int, minElevation: Double
|
||||
) {
|
||||
if (satList.isEmpty()) {
|
||||
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> {
|
||||
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)
|
||||
}
|
||||
}
|
||||
|
|
Ładowanie…
Reference in New Issue