Added multiple fixes to MapScreen and components

This commit is contained in:
Arty Bishop committed 2025-12-27 15:01:47 +00:00
1 parent 5b62474c27
commit 880fe10bfd
6 files changed
+192 -134

No files matched your search

@@ -209,11 +209,12 @@ fun CardButton(onClick: () -> Unit, text: String, modifier: Modifier = Modifier)
}
@Composable
fun CardIcon(onClick: () -> Unit, iconId: Int, description: String? = null) {
fun CardIcon(onClick: () -> Unit, iconId: Int, modifier: Modifier = Modifier) {
val clickableModifier = Modifier.clickable { onClick() }
val iconRes = ImageVector.vectorResource(iconId)
ElevatedCard(modifier = Modifier.size(48.dp)) {
Box(modifier = clickableModifier.fillMaxSize(), contentAlignment = Alignment.Center) {
Icon(imageVector = ImageVector.vectorResource(iconId), contentDescription = description)
Icon(imageVector = iconRes, contentDescription = null, modifier = modifier)
}
}
}
@@ -16,9 +16,10 @@ import androidx.compose.foundation.layout.padding
import androidx.compose.material3.ElevatedCard
import androidx.compose.runtime.Composable
import androidx.compose.runtime.DisposableEffect
import androidx.compose.runtime.collectAsState
import androidx.compose.runtime.State
import androidx.compose.runtime.remember
import androidx.compose.ui.Modifier
import androidx.compose.ui.draw.rotate
import androidx.compose.ui.platform.LocalContext
import androidx.compose.ui.unit.dp
import androidx.compose.ui.viewinterop.AndroidView
@@ -26,6 +27,7 @@ import androidx.core.content.ContextCompat
import androidx.lifecycle.Lifecycle
import androidx.lifecycle.LifecycleEventObserver
import androidx.lifecycle.compose.LocalLifecycleOwner
import androidx.lifecycle.compose.collectAsStateWithLifecycle
import androidx.lifecycle.viewmodel.compose.viewModel
import androidx.navigation.NavGraphBuilder
import androidx.navigation.compose.composable
@@ -38,7 +40,6 @@ import com.rtbishop.look4sat.presentation.components.CardIcon
import com.rtbishop.look4sat.presentation.components.NextPassRow
import com.rtbishop.look4sat.presentation.components.TimerBar
import com.rtbishop.look4sat.presentation.components.TimerRow
import com.rtbishop.look4sat.presentation.components.getDefaultPass
import org.osmdroid.tileprovider.tilesource.XYTileSource
import org.osmdroid.util.GeoPoint
import org.osmdroid.views.CustomZoomButtonsController
@@ -70,31 +71,34 @@ private val textPaint = Paint(Paint.ANTI_ALIAS_FLAG).apply {
private val labelRect = Rect()
fun NavGraphBuilder.mapDestination() {
composable(Screen.Map.route) { MapScreen() }
composable(Screen.Map.route) {
val viewModel = viewModel(MapViewModel::class.java, factory = MapViewModel.Factory)
val uiState = viewModel.uiState.collectAsStateWithLifecycle()
MapScreen(uiState)
}
}
@Composable
private fun MapScreen() {
val viewModel = viewModel(MapViewModel::class.java, factory = MapViewModel.Factory)
viewModel.selectDefaultSatellite(-1)
val stationPos = viewModel.stationPosition.collectAsState(initial = null)
val positions = viewModel.positions.collectAsState(initial = null)
val satTrack = viewModel.track.collectAsState(initial = null)
val footprint = viewModel.footprint.collectAsState(initial = null)
val positionClick = { orbitalObject: OrbitalObject -> viewModel.selectSatellite(orbitalObject) }
private fun MapScreen(uiState: State<MapState>) {
val onItemClick = { item: OrbitalObject -> uiState.value.sendAction(MapAction.SelectItem(item)) }
val selectPrev = { uiState.value.sendAction(MapAction.SelectPrev) }
val selectNext = { uiState.value.sendAction(MapAction.SelectNext) }
val rotateMod = Modifier.rotate(180f)
val timeString = uiState.value.mapData?.aosTime ?: "00:00:00"
val isTimeAos = uiState.value.mapData?.isTimeAos ?: true
Column(modifier = Modifier.padding(6.dp), verticalArrangement = Arrangement.spacedBy(6.dp)) {
TimerRow {
CardIcon(onClick = {}, iconId = R.drawable.ic_filter)
TimerBar(timeString = "88:88:88", isTimeAos = true)
CardIcon(onClick = {}, iconId = R.drawable.ic_satellite)
CardIcon(onClick = selectPrev, iconId = R.drawable.ic_arrow, modifier = rotateMod)
TimerBar(timeString = timeString, isTimeAos = isTimeAos)
CardIcon(onClick = selectNext, iconId = R.drawable.ic_arrow)
}
NextPassRow(pass = getDefaultPass())
NextPassRow(pass = uiState.value.orbitalPass)
ElevatedCard(modifier = Modifier.fillMaxSize()) {
MapView(modifier = Modifier.fillMaxSize(), isLightTheme = viewModel.stateOfLightTheme) { mapView ->
stationPos.value?.let { setStationPosition(it, mapView) }
positions.value?.let { setPositions(it, mapView, positionClick) }
satTrack.value?.let { setSatelliteTrack(it, mapView) }
footprint.value?.let { setFootprint(it, mapView) }
MapView(modifier = Modifier.fillMaxSize(), isLightUi = uiState.value.isLightUi) { mapView ->
uiState.value.stationPosition?.let { setStationPosition(it, mapView) }
uiState.value.positions?.let { setPositions(it, mapView, onItemClick) }
uiState.value.track?.let { setSatelliteTrack(it, mapView) }
uiState.value.footprint?.let { setFootprint(it, mapView) }
}
}
}
@@ -218,10 +222,10 @@ private fun setFootprint(orbitalPos: OrbitalPos, mapView: MapView) {
@Composable
private fun MapView(
modifier: Modifier = Modifier,
isLightTheme: Boolean,
isLightUi: Boolean,
update: ((map: MapView) -> Unit)? = null
) {
val mapView = rememberMapViewWithLifecycle(isLightTheme)
val mapView = rememberMapViewWithLifecycle(isLightUi)
AndroidView({ mapView }, modifier) { update?.invoke(it) }
}
@@ -18,11 +18,33 @@
package com.rtbishop.look4sat.presentation.map
import com.rtbishop.look4sat.domain.predict.GeoPos
import com.rtbishop.look4sat.domain.predict.OrbitalObject
import com.rtbishop.look4sat.domain.predict.OrbitalPass
import com.rtbishop.look4sat.domain.predict.OrbitalPos
data class MapState(
val mapData: MapData?,
val isLightUi: Boolean,
val stationPosition: GeoPos?,
val orbitalPass: OrbitalPass,
val track: List<List<GeoPos>>?,
val footprint: OrbitalPos?,
val positions: Map<OrbitalObject, GeoPos>?,
val sendAction: (MapAction) -> Unit
)
sealed class MapAction {
data object SelectPrev: MapAction()
data object SelectNext: MapAction()
data class SelectItem(val item: OrbitalObject): MapAction()
data class SelectDefaultItem(val catnum: Int): MapAction()
}
data class MapData(
val catNum: Int,
val name: String,
val aosTime: String,
val isTimeAos: Boolean,
val azimuth: Double,
val elevation: Double,
val range: Double,
@@ -25,7 +25,6 @@ import androidx.lifecycle.viewmodel.viewModelFactory
import com.rtbishop.look4sat.MainApplication
import com.rtbishop.look4sat.domain.predict.GeoPos
import com.rtbishop.look4sat.domain.predict.OrbitalObject
import com.rtbishop.look4sat.domain.predict.OrbitalPos
import com.rtbishop.look4sat.domain.repository.ISatelliteRepo
import com.rtbishop.look4sat.domain.repository.ISettingsRepo
import com.rtbishop.look4sat.domain.utility.clipLat
@@ -33,60 +32,62 @@ import com.rtbishop.look4sat.domain.utility.clipLon
import com.rtbishop.look4sat.domain.utility.positionToQth
import com.rtbishop.look4sat.domain.utility.toDegrees
import com.rtbishop.look4sat.domain.utility.toTimerString
import java.util.Date
import kotlin.collections.set
import com.rtbishop.look4sat.presentation.components.getDefaultPass
import kotlinx.coroutines.Job
import kotlinx.coroutines.cancelAndJoin
import kotlinx.coroutines.delay
import kotlinx.coroutines.flow.MutableSharedFlow
import kotlinx.coroutines.flow.SharedFlow
import kotlinx.coroutines.flow.flow
import kotlinx.coroutines.flow.MutableStateFlow
import kotlinx.coroutines.flow.StateFlow
import kotlinx.coroutines.flow.update
import kotlinx.coroutines.isActive
import kotlinx.coroutines.launch
import java.util.Date
import kotlin.collections.set
class MapViewModel(private val satelliteRepo: ISatelliteRepo, settingsRepo: ISettingsRepo) : ViewModel() {
class MapViewModel(private val satelliteRepo: ISatelliteRepo, settingsRepo: ISettingsRepo) :
ViewModel() {
private val stationPos = settingsRepo.stationPosition.value
private val defaultPass = getDefaultPass()
private val _uiState = MutableStateFlow(
MapState(
mapData = null,
isLightUi = settingsRepo.otherSettings.value.stateOfLightTheme,
stationPosition = null,
orbitalPass = defaultPass,
track = null,
footprint = null,
positions = null,
sendAction = ::handleAction
)
)
private var allPasses = satelliteRepo.passes.value
private var allSatellites = satelliteRepo.satellites.value
private var dataUpdateJob: Job? = null
private var dataUpdateRate = 1000L
private var selectedOrbitalObject: OrbitalObject? = null
val uiState: StateFlow<MapState> = _uiState
val stateOfLightTheme = settingsRepo.otherSettings.value.stateOfLightTheme
val stationPosition = flow {
val osmLat = clipLat(stationPos.latitude)
val osmLon = clipLon(stationPos.longitude)
emit(GeoPos(osmLat, osmLon))
init {
selectDefaultSatellite(-1)
}
private val _track = MutableSharedFlow<List<List<GeoPos>>>()
val track: SharedFlow<List<List<GeoPos>>> = _track
private fun handleAction(action: MapAction) {
when (action) {
MapAction.SelectPrev -> scrollSelection(true)
MapAction.SelectNext -> scrollSelection(false)
is MapAction.SelectItem -> selectSatellite(action.item)
is MapAction.SelectDefaultItem -> selectDefaultSatellite(action.catnum)
}
}
private val _footprint = MutableSharedFlow<OrbitalPos>()
val footprint: SharedFlow<OrbitalPos> = _footprint
private fun getStationPosition() {
val osmLat = clipLat(stationPos.latitude)
val osmLon = clipLon(stationPos.longitude)
_uiState.update { it.copy(stationPosition = GeoPos(osmLat, osmLon)) }
}
private val _mapData = MutableSharedFlow<MapData>()
// val mapData: SharedFlow<MapData> = _mapData
private val _positions = MutableSharedFlow<Map<OrbitalObject, GeoPos>>()
val positions: SharedFlow<Map<OrbitalObject, GeoPos>> = _positions
// fun scrollSelection(decrement: Boolean) {
// if (allSatellites.isNotEmpty()) {
// val index = allSatellites.indexOf(selectedSatellite)
// if (decrement) {
// if (index > 0) selectSatellite(allSatellites[index - 1])
// else selectSatellite(allSatellites[allSatellites.size - 1])
// } else {
// if (index < allSatellites.size - 1) selectSatellite(allSatellites[index + 1])
// else selectSatellite(allSatellites[0])
// }
// }
// }
fun selectDefaultSatellite(catnum: Int) {
private fun selectDefaultSatellite(catnum: Int) {
if (allSatellites.isNotEmpty()) {
if (catnum == -1) {
allPasses.find { pass -> pass.progress < 100 && !pass.isDeepSpace }
@@ -97,12 +98,26 @@ class MapViewModel(private val satelliteRepo: ISatelliteRepo, settingsRepo: ISet
}
}
fun selectSatellite(orbitalObject: OrbitalObject, updateFreq: Long = dataUpdateRate) {
private fun scrollSelection(decrement: Boolean) {
if (allSatellites.isNotEmpty()) {
val index = allSatellites.indexOf(selectedOrbitalObject)
if (decrement) {
if (index > 0) selectSatellite(allSatellites[index - 1])
else selectSatellite(allSatellites[allSatellites.size - 1])
} else {
if (index < allSatellites.size - 1) selectSatellite(allSatellites[index + 1])
else selectSatellite(allSatellites[0])
}
}
}
private fun selectSatellite(orbitalObject: OrbitalObject, updateFreq: Long = dataUpdateRate) {
selectedOrbitalObject = orbitalObject
viewModelScope.launch {
dataUpdateJob?.cancelAndJoin()
dataUpdateJob = launch {
val dateNow = Date()
getStationPosition()
getSatTrack(orbitalObject, stationPos, dateNow)
while (isActive) {
dateNow.time = System.currentTimeMillis()
@@ -141,7 +156,7 @@ class MapViewModel(private val satelliteRepo: ISatelliteRepo, settingsRepo: ISet
currentTrack.add(currentPosition)
}
satTracks.add(currentTrack)
_track.emit(satTracks)
_uiState.update { it.copy(track = satTracks) }
}
private suspend fun getPositions(orbitalObjects: List<OrbitalObject>, pos: GeoPos, date: Date) {
@@ -152,24 +167,29 @@ class MapViewModel(private val satelliteRepo: ISatelliteRepo, settingsRepo: ISet
val osmLon = clipLon(satPos.longitude.toDegrees())
positions[satellite] = GeoPos(osmLat, osmLon)
}
_positions.emit(positions)
_uiState.update { it.copy(positions = positions) }
}
private suspend fun getSatFootprint(orbitalObject: OrbitalObject, pos: GeoPos, date: Date) {
val satPos = satelliteRepo.getPosition(orbitalObject, pos, date.time)
_footprint.emit(satPos)
_uiState.update { it.copy(footprint = satPos) }
}
private suspend fun getSatData(sat: OrbitalObject, pos: GeoPos, date: Date) {
var orbitalPass = defaultPass
var aosTime = 0L.toTimerString()
var isTimeAos = true
allPasses.find { pass -> pass.catNum == sat.data.catnum && pass.progress < 100 }
?.let { satPass ->
orbitalPass = satPass
if (!satPass.isDeepSpace) {
aosTime = if (date.time < satPass.aosTime) {
val millisBeforeStart = satPass.aosTime.minus(date.time)
isTimeAos = true
millisBeforeStart.toTimerString()
} else {
val millisBeforeEnd = satPass.losTime.minus(date.time)
isTimeAos = false
millisBeforeEnd.toTimerString()
}
}
@@ -188,6 +208,7 @@ class MapViewModel(private val satelliteRepo: ISatelliteRepo, settingsRepo: ISet
sat.data.catnum,
sat.data.name,
aosTime,
isTimeAos,
azimuth,
elevation,
satPos.distance,
@@ -199,7 +220,7 @@ class MapViewModel(private val satelliteRepo: ISatelliteRepo, settingsRepo: ISet
phase,
visibility
)
_mapData.emit(satData)
_uiState.update { it.copy(mapData = satData, orbitalPass = orbitalPass) }
}
companion object {
@@ -4,6 +4,7 @@ import androidx.compose.foundation.MarqueeSpacing
import androidx.compose.foundation.background
import androidx.compose.foundation.basicMarquee
import androidx.compose.foundation.layout.Arrangement
import androidx.compose.foundation.layout.Box
import androidx.compose.foundation.layout.Column
import androidx.compose.foundation.layout.Row
import androidx.compose.foundation.layout.RowScope
@@ -38,6 +39,7 @@ import androidx.navigation.compose.composable
import androidx.navigation.navArgument
import com.rtbishop.look4sat.R
import com.rtbishop.look4sat.domain.model.SatRadio
import com.rtbishop.look4sat.domain.utility.toDegrees
import com.rtbishop.look4sat.presentation.MainTheme
import com.rtbishop.look4sat.presentation.Screen
import com.rtbishop.look4sat.presentation.components.CardIcon
@@ -78,15 +80,53 @@ private fun RadarScreen(uiState: RadarState, navigateBack: () -> Unit) {
CardIcon(onClick = addToCalendar, iconId = R.drawable.ic_calendar)
}
NextPassRow(pass = uiState.currentPass ?: getDefaultPass())
ElevatedCard(modifier = Modifier.aspectRatio(1f)) {
uiState.orbitalPos?.let { item ->
RadarViewCompose(
item = item,
items = uiState.satTrack,
azimElev = uiState.orientationValues,
shouldShowSweep = uiState.shouldShowSweep,
shouldUseCompass = uiState.shouldUseCompass
)
Box(contentAlignment = Alignment.Center, modifier = Modifier.aspectRatio(1f)) {
uiState.orbitalPos?.let { position ->
ElevatedCard {
RadarViewCompose(
item = position,
items = uiState.satTrack,
azimElev = uiState.orientationValues,
shouldShowSweep = uiState.shouldShowSweep,
shouldUseCompass = uiState.shouldUseCompass
)
}
Column(
verticalArrangement = Arrangement.SpaceBetween,
modifier = Modifier.fillMaxSize()
.padding(horizontal = 6.dp, vertical = 4.dp)
) {
Row(
horizontalArrangement = Arrangement.SpaceBetween,
modifier = Modifier.fillMaxWidth()
) {
RadarTextTop(
position.azimuth,
stringResource(R.string.radar_az_text),
true
)
RadarTextTop(
position.elevation,
stringResource(R.string.radar_el_text),
false
)
}
Row(
horizontalArrangement = Arrangement.SpaceBetween,
modifier = Modifier.fillMaxWidth()
) {
RadarTextBottom(
position.altitude,
stringResource(R.string.radar_alt_text),
true
)
RadarTextBottom(
position.distance,
stringResource(R.string.radar_dist_text),
false
)
}
}
}
}
ElevatedCard(modifier = Modifier.fillMaxSize()) {
@@ -96,6 +136,26 @@ private fun RadarScreen(uiState: RadarState, navigateBack: () -> Unit) {
}
}
@Composable
private fun RadarTextTop(value: Double, text: String, isLeft: Boolean) {
val alignment = if (isLeft) Alignment.Start else Alignment.End
val degValue = stringResource(R.string.radar_az_value, value.toDegrees())
Column(horizontalAlignment = alignment) {
Text(text = degValue, fontSize = 18.sp)
Text(text = text, fontSize = 15.sp)
}
}
@Composable
private fun RadarTextBottom(value: Double, text: String, isLeft: Boolean) {
val alignment = if (isLeft) Alignment.Start else Alignment.End
val degValue = stringResource(R.string.radar_alt_value, value)
Column(horizontalAlignment = alignment) {
Text(text = text, fontSize = 15.sp)
Text(text = degValue, fontSize = 18.sp)
}
}
@Composable
private fun TransmittersList(transmitters: List<SatRadio>) {
LazyColumn(modifier = Modifier.fillMaxSize()) {
@@ -120,19 +180,19 @@ private fun TransmitterItem(radio: SatRadio) {
Surface(color = MaterialTheme.colorScheme.background) {
Surface(modifier = Modifier.padding(bottom = 2.dp)) {
Column(
verticalArrangement = Arrangement.spacedBy(2.dp),
verticalArrangement = Arrangement.spacedBy(4.dp),
modifier = Modifier
.background(MaterialTheme.colorScheme.surface)
.padding(6.dp)
.padding(horizontal = 6.dp, vertical = 8.dp)
) {
Row(
verticalAlignment = Alignment.CenterVertically,
modifier = Modifier.fillMaxWidth()
) {
Icon(
painter = painterResource(id = R.drawable.ic_back),
painter = painterResource(id = R.drawable.ic_arrow),
tint = Color.Green,
contentDescription = null, modifier = Modifier.rotate(-90f)
contentDescription = null, modifier = Modifier.rotate(90f).weight(0.15f)
)
Text(
text = if (radio.isInverted) "INVERTED: ${radio.info} " else "${radio.info} ",
@@ -140,14 +200,14 @@ private fun TransmitterItem(radio: SatRadio) {
modifier = Modifier
.basicMarquee(
iterations = Int.MAX_VALUE,
spacing = MarqueeSpacing(0.dp)
spacing = MarqueeSpacing(16.dp)
)
.weight(1f)
.weight(0.7f)
)
Icon(
painter = painterResource(id = R.drawable.ic_back),
painter = painterResource(id = R.drawable.ic_arrow),
tint = Color.Red,
contentDescription = null, modifier = Modifier.rotate(90f)
contentDescription = null, modifier = Modifier.rotate(-90f).weight(0.15f)
)
}
FrequencyRow(satRadio = radio)
@@ -185,7 +245,6 @@ private fun RowScope.ModeText(mode: String?) {
Text(
text = mode ?: "- - : - -",
textAlign = TextAlign.Center,
fontSize = 15.sp,
modifier = Modifier.weight(0.15f)
)
}
@@ -203,53 +262,3 @@ private fun FrequencyText(frequency: Long?) {
color = MaterialTheme.colorScheme.primary
)
}
//private val divider = 1000000f
//radioDownlink.text = String.format(Locale.ENGLISH, link, downlink / divider)
//radioUplink.text = String.format(Locale.ENGLISH, link, uplink / divider)
//private fun setupObservers() {
// viewModel.getPass(45000, System.currentTimeMillis()).observe(viewLifecycleOwner) { pass ->
// binding?.run {
// radarView = RadarView(requireContext()).apply {
// setShowAim(viewModel.getUseCompass())
// setScanning(viewModel.getShowSweep())
// }
// viewModel.radarData.observe(viewLifecycleOwner) { passData ->
// setPassText(pass, passData.satPos)
// }
// }
// }
//}
//
//private fun setPassText(satPass: SatPass, satPos: SatPos) {
// binding?.run {
// val timeNow = System.currentTimeMillis()
// val radarAzim = getString(R.string.radar_az_value)
// val radarElev = getString(R.string.radar_el_value)
// val radarAlt = getString(R.string.radar_alt_value)
// val radarDist = getString(R.string.radar_dist_value)
// radarAzValue.text = String.format(radarAzim, satPos.azimuth.toDegrees())
// radarElValue.text = String.format(radarElev, satPos.elevation.toDegrees())
// radarAltValue.text = String.format(radarAlt, satPos.altitude)
// radarDstValue.text = String.format(radarDist, satPos.distance)
// if (satPos.eclipsed) {
// radarVisibility.text = getText(R.string.radar_eclipsed)
// } else {
// radarVisibility.text = getText(R.string.radar_visible)
// }
// if (!satPass.isDeepSpace) {
// if (timeNow < satPass.aosTime) {
// val millisBeforeStart = satPass.aosTime.minus(timeNow)
// radarTimer.text = millisBeforeStart.toTimerString()
// } else {
// val millisBeforeEnd = satPass.losTime.minus(timeNow)
// radarTimer.text = millisBeforeEnd.toTimerString()
// if (timeNow > satPass.losTime) {
// radarTimer.text = 0L.toTimerString()
//// findNavController().navigateUp()
// }
// }
// } else radarTimer.text = 0L.toTimerString()
// }
//}
@@ -195,6 +195,7 @@ private fun TypeCard(types: List<String>, onClick: () -> Unit, modifier: Modifie
fontSize = 16.sp,
fontWeight = FontWeight.Medium,
modifier = Modifier
.weight(1f)
.basicMarquee(iterations = Int.MAX_VALUE, spacing = MarqueeSpacing(16.dp))
)
}