mirror of
https://github.com/atsunatsu/Look4Sat.git
synced 2026-10-06 04:27:29 +00:00
Added basic convention plugins setup for modularization
This commit is contained in:
1 parent
de99e09f5e
commit
4d88214c1d
5675 files changed
+10339
-9969
No files matched your search
@@ -0,0 +1,4 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<manifest>
|
||||
|
||||
</manifest>
|
||||
@@ -0,0 +1,495 @@
|
||||
/*
|
||||
* Look4Sat. Amateur radio satellite tracker and pass predictor.
|
||||
* Copyright (C) 2019-2026 Arty Bishop and contributors.
|
||||
*
|
||||
* This program is free software: you can redistribute it and/or modify
|
||||
* it under the terms of the GNU General Public License as published by
|
||||
* the Free Software Foundation, either version 3 of the License, or
|
||||
* (at your option) any later version.
|
||||
*
|
||||
* This program is distributed in the hope that it will be useful,
|
||||
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
* GNU General Public License for more details.
|
||||
*
|
||||
* You should have received a copy of the GNU General Public License
|
||||
* along with this program. If not, see <https://www.gnu.org/licenses/>.
|
||||
*/
|
||||
package com.rtbishop.look4sat.feature.radar
|
||||
|
||||
import androidx.compose.animation.core.LinearEasing
|
||||
import androidx.compose.animation.core.RepeatMode
|
||||
import androidx.compose.animation.core.animateFloat
|
||||
import androidx.compose.animation.core.infiniteRepeatable
|
||||
import androidx.compose.animation.core.rememberInfiniteTransition
|
||||
import androidx.compose.animation.core.tween
|
||||
import androidx.compose.foundation.background
|
||||
import androidx.compose.foundation.border
|
||||
import androidx.compose.foundation.clickable
|
||||
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.fillMaxSize
|
||||
import androidx.compose.foundation.layout.fillMaxWidth
|
||||
import androidx.compose.foundation.layout.padding
|
||||
import androidx.compose.foundation.layout.size
|
||||
import androidx.compose.foundation.lazy.LazyColumn
|
||||
import androidx.compose.foundation.lazy.items
|
||||
import androidx.compose.material3.ElevatedCard
|
||||
import androidx.compose.material3.Icon
|
||||
import androidx.compose.material3.MaterialTheme
|
||||
import androidx.compose.material3.Surface
|
||||
import androidx.compose.material3.Text
|
||||
import androidx.compose.runtime.Composable
|
||||
import androidx.compose.ui.Alignment
|
||||
import androidx.compose.ui.Modifier
|
||||
import androidx.compose.ui.draw.alpha
|
||||
import androidx.compose.ui.draw.rotate
|
||||
import androidx.compose.ui.keepScreenOn
|
||||
import androidx.compose.ui.res.painterResource
|
||||
import androidx.compose.ui.res.stringResource
|
||||
import androidx.compose.ui.semantics.contentDescription
|
||||
import androidx.compose.ui.semantics.semantics
|
||||
import androidx.compose.ui.text.font.FontWeight
|
||||
import androidx.compose.ui.text.style.TextAlign
|
||||
import androidx.compose.ui.tooling.preview.Preview
|
||||
import androidx.compose.ui.unit.dp
|
||||
import androidx.compose.ui.unit.sp
|
||||
import androidx.lifecycle.compose.collectAsStateWithLifecycle
|
||||
import androidx.lifecycle.viewmodel.compose.viewModel
|
||||
import androidx.navigation.NavGraphBuilder
|
||||
import androidx.navigation.compose.composable
|
||||
import androidx.navigation.navArgument
|
||||
import com.rtbishop.look4sat.core.domain.model.SatRadio
|
||||
import com.rtbishop.look4sat.core.domain.utility.toDegrees
|
||||
import com.rtbishop.look4sat.core.presentation.EmptyListCard
|
||||
import com.rtbishop.look4sat.core.presentation.IconCard
|
||||
import com.rtbishop.look4sat.core.presentation.MainTheme
|
||||
import com.rtbishop.look4sat.core.presentation.NextPassRow
|
||||
import com.rtbishop.look4sat.core.presentation.R
|
||||
import com.rtbishop.look4sat.core.presentation.Screen
|
||||
import com.rtbishop.look4sat.core.presentation.TimerRow
|
||||
import com.rtbishop.look4sat.core.presentation.TopBar
|
||||
import com.rtbishop.look4sat.core.presentation.getDefaultPass
|
||||
import com.rtbishop.look4sat.core.presentation.infiniteMarquee
|
||||
import com.rtbishop.look4sat.core.presentation.isVerticalLayout
|
||||
import com.rtbishop.look4sat.core.presentation.layoutPadding
|
||||
|
||||
fun NavGraphBuilder.radarDestination(navigateUp: () -> Unit) {
|
||||
val radarRoute = "${Screen.Radar.route}?catNum={catNum}&aosTime={aosTime}"
|
||||
val radarArgs = listOf(
|
||||
navArgument("catNum") { defaultValue = 0 },
|
||||
navArgument("aosTime") { defaultValue = 0L }
|
||||
)
|
||||
composable(radarRoute, radarArgs) {
|
||||
val viewModel = viewModel(RadarViewModel::class.java, factory = RadarViewModel.Factory)
|
||||
val uiState = viewModel.uiState.collectAsStateWithLifecycle().value
|
||||
RadarScreen(uiState, navigateUp)
|
||||
}
|
||||
}
|
||||
|
||||
@Composable
|
||||
private fun RadarScreen(uiState: RadarState, navigateUp: () -> Unit) {
|
||||
// BluetoothCIV.init(LocalContext.current)
|
||||
val addToCalendar: () -> Unit = {
|
||||
uiState.currentPass?.let { pass ->
|
||||
uiState.sendAction(RadarAction.AddToCalendar(pass.name, pass.aosTime, pass.losTime))
|
||||
}
|
||||
}
|
||||
val upcomingPass = uiState.currentPass ?: getDefaultPass()
|
||||
if (upcomingPass.losTime < System.currentTimeMillis()) navigateUp()
|
||||
Column(
|
||||
modifier = Modifier
|
||||
.layoutPadding()
|
||||
.keepScreenOn(), verticalArrangement = Arrangement.spacedBy(6.dp)
|
||||
) {
|
||||
if (isVerticalLayout()) {
|
||||
TopBar {
|
||||
IconCard(action = navigateUp, resId = R.drawable.ic_back)
|
||||
TimerRow(timeString = uiState.currentTime, isTimeAos = uiState.isCurrentTimeAos)
|
||||
IconCard(action = addToCalendar, resId = R.drawable.ic_calendar)
|
||||
}
|
||||
TopBar { NextPassRow(pass = upcomingPass) }
|
||||
} else {
|
||||
TopBar {
|
||||
IconCard(action = navigateUp, resId = R.drawable.ic_back)
|
||||
TimerRow(timeString = uiState.currentTime, isTimeAos = uiState.isCurrentTimeAos)
|
||||
NextPassRow(pass = upcomingPass, modifier = Modifier.weight(1f))
|
||||
IconCard(action = addToCalendar, resId = R.drawable.ic_calendar)
|
||||
}
|
||||
}
|
||||
if(isVerticalLayout()) {
|
||||
ElevatedCard(modifier = Modifier.weight(1f)) {
|
||||
Box(contentAlignment = Alignment.Center) {
|
||||
if (uiState.orbitalPos == null) {
|
||||
ElevatedCard(modifier = Modifier.fillMaxSize()) {
|
||||
EmptyListCard(message = "")
|
||||
}
|
||||
}
|
||||
uiState.orbitalPos?.let { position ->
|
||||
RadarViewCompose(
|
||||
item = position,
|
||||
items = uiState.satTrack,
|
||||
azimElev = uiState.orientationValues,
|
||||
shouldShowSweep = uiState.shouldShowSweep,
|
||||
shouldUseCompass = uiState.shouldUseCompass,
|
||||
modifier = Modifier.align(Alignment.Center)
|
||||
)
|
||||
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.weight(1f)) {
|
||||
if (uiState.transmitters.isEmpty()) {
|
||||
Box(contentAlignment = Alignment.Center, modifier = Modifier.fillMaxSize()) {
|
||||
Column(
|
||||
horizontalAlignment = Alignment.CenterHorizontally,
|
||||
verticalArrangement = Arrangement.spacedBy(24.dp),
|
||||
modifier = Modifier.padding(32.dp)
|
||||
) {
|
||||
Text(text = """¯\_(ツ)_/¯""", fontSize = 32.sp)
|
||||
Text(
|
||||
text = stringResource(R.string.empty_list_message),
|
||||
fontSize = 21.sp,
|
||||
textAlign = TextAlign.Center
|
||||
)
|
||||
Text(
|
||||
text = stringResource(R.string.radar_no_data),
|
||||
fontSize = 18.sp,
|
||||
textAlign = TextAlign.Center
|
||||
)
|
||||
}
|
||||
}
|
||||
} else {
|
||||
Box(modifier = Modifier.fillMaxSize(), contentAlignment = Alignment.Center) {
|
||||
TransmittersList(
|
||||
transmitters = uiState.transmitters,
|
||||
selectedUuid = uiState.selectedTransmitterUuid,
|
||||
onSelect = { uuid ->
|
||||
if (uiState.selectedTransmitterUuid != null) {
|
||||
uiState.sendAction(RadarAction.SelectTransmitter(uuid))
|
||||
}
|
||||
}
|
||||
)
|
||||
if (uiState.orbitalPos?.eclipsed == true) {
|
||||
EclipsedIndicator()
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
} else {
|
||||
Row(horizontalArrangement = Arrangement.spacedBy(6.dp)) {
|
||||
ElevatedCard(modifier = Modifier.weight(1f)) {
|
||||
Box(contentAlignment = Alignment.Center) {
|
||||
if (uiState.orbitalPos == null) {
|
||||
ElevatedCard(modifier = Modifier.fillMaxSize()) {
|
||||
EmptyListCard(message = "")
|
||||
}
|
||||
}
|
||||
uiState.orbitalPos?.let { position ->
|
||||
RadarViewCompose(
|
||||
item = position,
|
||||
items = uiState.satTrack,
|
||||
azimElev = uiState.orientationValues,
|
||||
shouldShowSweep = uiState.shouldShowSweep,
|
||||
shouldUseCompass = false,
|
||||
modifier = Modifier.align(Alignment.Center)
|
||||
)
|
||||
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.weight(1f)) {
|
||||
if (uiState.transmitters.isEmpty()) {
|
||||
Box(contentAlignment = Alignment.Center, modifier = Modifier.fillMaxSize()) {
|
||||
Column(
|
||||
horizontalAlignment = Alignment.CenterHorizontally,
|
||||
verticalArrangement = Arrangement.spacedBy(24.dp),
|
||||
modifier = Modifier.padding(32.dp)
|
||||
) {
|
||||
Text(text = """¯\_(ツ)_/¯""", fontSize = 32.sp)
|
||||
Text(
|
||||
text = stringResource(R.string.empty_list_message),
|
||||
fontSize = 21.sp,
|
||||
textAlign = TextAlign.Center
|
||||
)
|
||||
Text(
|
||||
text = stringResource(R.string.radar_no_data),
|
||||
fontSize = 18.sp,
|
||||
textAlign = TextAlign.Center
|
||||
)
|
||||
}
|
||||
}
|
||||
} else {
|
||||
Box(modifier = Modifier.fillMaxSize(), contentAlignment = Alignment.Center) {
|
||||
TransmittersList(
|
||||
transmitters = uiState.transmitters,
|
||||
selectedUuid = uiState.selectedTransmitterUuid,
|
||||
onSelect = { uuid ->
|
||||
if (uiState.selectedTransmitterUuid != null) {
|
||||
uiState.sendAction(RadarAction.SelectTransmitter(uuid))
|
||||
}
|
||||
}
|
||||
)
|
||||
if (uiState.orbitalPos?.eclipsed == true) {
|
||||
EclipsedIndicator()
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@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>,
|
||||
selectedUuid: String?,
|
||||
onSelect: (String) -> Unit
|
||||
) {
|
||||
LazyColumn(modifier = Modifier.fillMaxSize()) {
|
||||
items(items = transmitters, key = { item -> item.uuid }) { radio ->
|
||||
TransmitterItem(
|
||||
radio,
|
||||
(selectedUuid != null),
|
||||
(radio.uuid == selectedUuid)
|
||||
) { onSelect(radio.uuid) }
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@Preview
|
||||
@Composable
|
||||
private fun TransmitterItemPreview() {
|
||||
val transmitter = SatRadio(
|
||||
"", "Extremely powerful transmitter", true, 10000000000L, 10000000000L,
|
||||
null, 10000000000L, 10000000000L, "FSK AX.100 Mode 5", true, 0
|
||||
)
|
||||
MainTheme { TransmitterItem(transmitter, isClickable = true, isSelected = true, onClick = {}) }
|
||||
}
|
||||
|
||||
@Composable
|
||||
private fun EclipsedIndicator() {
|
||||
val bgColor = MaterialTheme.colorScheme.primary
|
||||
val bgShape = MaterialTheme.shapes.medium
|
||||
val infiniteTransition = rememberInfiniteTransition()
|
||||
val textAlpha = infiniteTransition.animateFloat(
|
||||
initialValue = 1.0f,
|
||||
targetValue = 0.0f,
|
||||
animationSpec = infiniteRepeatable(
|
||||
animation = tween(
|
||||
durationMillis = 1000,
|
||||
delayMillis = 25,
|
||||
easing = LinearEasing
|
||||
),
|
||||
repeatMode = RepeatMode.Reverse
|
||||
)
|
||||
)
|
||||
Box(
|
||||
modifier = Modifier
|
||||
.fillMaxSize()
|
||||
.alpha(textAlpha.value)
|
||||
.border(width = 2.dp, color = bgColor, shape = bgShape),
|
||||
contentAlignment = Alignment.Center
|
||||
) {
|
||||
Text(
|
||||
text = stringResource(R.string.radar_eclipsed),
|
||||
fontSize = 18.sp,
|
||||
fontWeight = FontWeight.Medium,
|
||||
color = MaterialTheme.colorScheme.background,
|
||||
modifier = Modifier
|
||||
.background(color = bgColor, shape = bgShape)
|
||||
.padding(12.dp)
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
@Composable
|
||||
private fun TransmitterItem(
|
||||
radio: SatRadio,
|
||||
isClickable: Boolean,
|
||||
isSelected: Boolean,
|
||||
onClick: () -> Unit
|
||||
) {
|
||||
val title = if (radio.isInverted) "INVERTED: ${radio.info}" else radio.info
|
||||
val fullTitle = "$title - (${radio.downlinkMode ?: "--"}/${radio.uplinkMode ?: "--"})"
|
||||
Surface(
|
||||
color = MaterialTheme.colorScheme.background,
|
||||
modifier = Modifier
|
||||
.fillMaxWidth()
|
||||
.then(if (isClickable) Modifier.clickable { onClick() } else Modifier)
|
||||
) {
|
||||
Surface(modifier = Modifier.padding(bottom = 2.dp)) {
|
||||
Column(
|
||||
horizontalAlignment = Alignment.CenterHorizontally,
|
||||
verticalArrangement = Arrangement.spacedBy(6.dp),
|
||||
modifier = Modifier
|
||||
.background(MaterialTheme.colorScheme.surface)
|
||||
.padding(horizontal = 8.dp, vertical = 6.dp)
|
||||
) {
|
||||
Box {
|
||||
Text(
|
||||
text = fullTitle,
|
||||
textAlign = TextAlign.Center,
|
||||
modifier = Modifier
|
||||
.fillMaxWidth()
|
||||
.padding(horizontal = 6.dp)
|
||||
.infiniteMarquee()
|
||||
)
|
||||
if (isSelected) {
|
||||
Icon(
|
||||
painter = painterResource(id = R.drawable.ic_radios),
|
||||
contentDescription = null,
|
||||
tint = MaterialTheme.colorScheme.primary,
|
||||
modifier = Modifier.background(color = MaterialTheme.colorScheme.surface)
|
||||
)
|
||||
}
|
||||
}
|
||||
FrequencyRow(radio = radio, isDownlink = true)
|
||||
FrequencyRow(radio = radio, isDownlink = false)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@Composable
|
||||
private fun FrequencyRow(radio: SatRadio, isDownlink: Boolean, modifier: Modifier = Modifier) {
|
||||
Row(verticalAlignment = Alignment.CenterVertically, modifier = modifier.fillMaxWidth()) {
|
||||
val rotateMod = Modifier.rotate(if (isDownlink) 90f else -90f)
|
||||
val weightMod = Modifier.weight(1f)
|
||||
val desc = if (isDownlink) stringResource(R.string.radar_downlink) else stringResource(R.string.radar_uplink)
|
||||
Text(
|
||||
text = if (isDownlink) "D:" else "U:",
|
||||
textAlign = TextAlign.Center,
|
||||
fontSize = 18.sp,
|
||||
color = MaterialTheme.colorScheme.onSurface,
|
||||
modifier = modifier
|
||||
.size(width = 24.dp, height = 24.dp)
|
||||
.semantics { contentDescription = desc }
|
||||
)
|
||||
FrequencyText(if (isDownlink) radio.downlinkLow else radio.uplinkLow, weightMod)
|
||||
Text(
|
||||
text = "-",
|
||||
textAlign = TextAlign.Center,
|
||||
fontSize = 21.sp,
|
||||
fontWeight = FontWeight.Bold,
|
||||
color = MaterialTheme.colorScheme.primary,
|
||||
modifier = modifier
|
||||
)
|
||||
FrequencyText(if (isDownlink) radio.downlinkHigh else radio.uplinkHigh, weightMod)
|
||||
Icon(
|
||||
painter = painterResource(id = R.drawable.ic_arrow),
|
||||
tint = MaterialTheme.colorScheme.onSurface,
|
||||
contentDescription = null,
|
||||
modifier = rotateMod.size(width = 24.dp, height = 24.dp)
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
@Composable
|
||||
private fun FrequencyText(frequency: Long?, modifier: Modifier = Modifier) {
|
||||
val noLinkText = stringResource(R.string.radar_no_link)
|
||||
val freqValue = frequency?.let { it / 1000000f }
|
||||
Text(
|
||||
text = freqValue?.let { stringResource(id = R.string.radar_link_low, it) } ?: noLinkText,
|
||||
textAlign = TextAlign.Center,
|
||||
fontSize = 21.sp,
|
||||
fontWeight = FontWeight.Bold,
|
||||
color = MaterialTheme.colorScheme.primary,
|
||||
modifier = modifier
|
||||
)
|
||||
}
|
||||
@@ -0,0 +1,42 @@
|
||||
/*
|
||||
* Look4Sat. Amateur radio satellite tracker and pass predictor.
|
||||
* Copyright (C) 2019-2026 Arty Bishop and contributors.
|
||||
*
|
||||
* This program is free software: you can redistribute it and/or modify
|
||||
* it under the terms of the GNU General Public License as published by
|
||||
* the Free Software Foundation, either version 3 of the License, or
|
||||
* (at your option) any later version.
|
||||
*
|
||||
* This program is distributed in the hope that it will be useful,
|
||||
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
* GNU General Public License for more details.
|
||||
*
|
||||
* You should have received a copy of the GNU General Public License
|
||||
* along with this program. If not, see <https://www.gnu.org/licenses/>.
|
||||
*/
|
||||
package com.rtbishop.look4sat.feature.radar
|
||||
|
||||
import com.rtbishop.look4sat.core.domain.model.SatRadio
|
||||
import com.rtbishop.look4sat.core.domain.predict.OrbitalPass
|
||||
import com.rtbishop.look4sat.core.domain.predict.OrbitalPos
|
||||
|
||||
data class RadarState(
|
||||
val currentPass: OrbitalPass?,
|
||||
val currentTime: String,
|
||||
val isCurrentTimeAos: Boolean,
|
||||
val orientationValues: Pair<Float, Float>,
|
||||
val orbitalPos: OrbitalPos?,
|
||||
val satTrack: List<OrbitalPos>,
|
||||
val shouldShowSweep: Boolean,
|
||||
val shouldUseCompass: Boolean,
|
||||
val transmitters: List<SatRadio>,
|
||||
val selectedTransmitterUuid: String?,
|
||||
val selectedFrequency: Long?,
|
||||
val sendAction: (RadarAction) -> Unit
|
||||
)
|
||||
|
||||
sealed class RadarAction {
|
||||
data class AddToCalendar(val name: String, val aosTime: Long, val losTime: Long) : RadarAction()
|
||||
data class SelectTransmitter(val uuid: String) : RadarAction()
|
||||
}
|
||||
@@ -0,0 +1,252 @@
|
||||
/*
|
||||
* Look4Sat. Amateur radio satellite tracker and pass predictor.
|
||||
* Copyright (C) 2019-2026 Arty Bishop and contributors.
|
||||
*
|
||||
* This program is free software: you can redistribute it and/or modify
|
||||
* it under the terms of the GNU General Public License as published by
|
||||
* the Free Software Foundation, either version 3 of the License, or
|
||||
* (at your option) any later version.
|
||||
*
|
||||
* This program is distributed in the hope that it will be useful,
|
||||
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
* GNU General Public License for more details.
|
||||
*
|
||||
* You should have received a copy of the GNU General Public License
|
||||
* along with this program. If not, see <https://www.gnu.org/licenses/>.
|
||||
*/
|
||||
package com.rtbishop.look4sat.feature.radar
|
||||
|
||||
import androidx.compose.animation.core.animateFloat
|
||||
import androidx.compose.animation.core.infiniteRepeatable
|
||||
import androidx.compose.animation.core.rememberInfiniteTransition
|
||||
import androidx.compose.animation.core.tween
|
||||
import androidx.compose.foundation.Canvas
|
||||
import androidx.compose.foundation.layout.aspectRatio
|
||||
import androidx.compose.material3.MaterialTheme
|
||||
import androidx.compose.runtime.Composable
|
||||
import androidx.compose.runtime.mutableFloatStateOf
|
||||
import androidx.compose.runtime.mutableStateOf
|
||||
import androidx.compose.runtime.remember
|
||||
import androidx.compose.ui.Modifier
|
||||
import androidx.compose.ui.geometry.Offset
|
||||
import androidx.compose.ui.graphics.Color
|
||||
import androidx.compose.ui.graphics.Path
|
||||
import androidx.compose.ui.graphics.PathEffect
|
||||
import androidx.compose.ui.graphics.PathMeasure
|
||||
import androidx.compose.ui.graphics.ShaderBrush
|
||||
import androidx.compose.ui.graphics.StampedPathEffectStyle
|
||||
import androidx.compose.ui.graphics.SweepGradientShader
|
||||
import androidx.compose.ui.graphics.drawscope.DrawScope
|
||||
import androidx.compose.ui.graphics.drawscope.Fill
|
||||
import androidx.compose.ui.graphics.drawscope.Stroke
|
||||
import androidx.compose.ui.graphics.drawscope.rotate
|
||||
import androidx.compose.ui.graphics.drawscope.translate
|
||||
import androidx.compose.ui.text.TextMeasurer
|
||||
import androidx.compose.ui.text.TextStyle
|
||||
import androidx.compose.ui.text.drawText
|
||||
import androidx.compose.ui.text.rememberTextMeasurer
|
||||
import androidx.compose.ui.unit.sp
|
||||
import com.rtbishop.look4sat.core.domain.predict.OrbitalPos
|
||||
import com.rtbishop.look4sat.core.domain.predict.PI_2
|
||||
import com.rtbishop.look4sat.core.domain.utility.toRadians
|
||||
import kotlin.math.cos
|
||||
import kotlin.math.sin
|
||||
|
||||
@Composable
|
||||
fun RadarViewCompose(
|
||||
item: OrbitalPos,
|
||||
items: List<OrbitalPos>,
|
||||
azimElev: Pair<Float, Float>,
|
||||
shouldShowSweep: Boolean,
|
||||
shouldUseCompass: Boolean,
|
||||
modifier: Modifier = Modifier
|
||||
) {
|
||||
// val context = LocalContext.current
|
||||
// val view = LocalView.current
|
||||
val radarColor = MaterialTheme.colorScheme.secondary
|
||||
val trackColor = MaterialTheme.colorScheme.primary
|
||||
val aimColor = MaterialTheme.colorScheme.error
|
||||
val strokeWidth = 6f
|
||||
val animTransition = rememberInfiniteTransition(label = "animScale")
|
||||
val animSpec = infiniteRepeatable<Float>(tween(1000))
|
||||
val animScale = animTransition.animateFloat(16f, 64f, animSpec, label = "animScale")
|
||||
// val aimThreshold = 0.05f
|
||||
val measurer = rememberTextMeasurer()
|
||||
val sweepDegrees = remember { mutableFloatStateOf(0f) }
|
||||
val trackCreated = remember { mutableStateOf(false) }
|
||||
val trackPath = remember { mutableStateOf(Path()) }
|
||||
val trackEffect = remember { mutableStateOf(PathEffect.cornerPathEffect(0f)) }
|
||||
// val soundPool = remember { mutableStateOf<SoundPool?>(null) }
|
||||
// val beepSoundId = remember { mutableIntStateOf(0) }
|
||||
// val aimTargetDifference = remember { mutableFloatStateOf(0f) }
|
||||
|
||||
// LaunchedEffect(item.azimuth, item.elevation, azimElev.first, azimElev.second) {
|
||||
// val aimAzimuthRadians = azimElev.first.toDouble().toRadians()
|
||||
// val aimElevationRadians = abs(min(azimElev.second, 0f)).toDouble().toRadians()
|
||||
// // radius of 0.5 makes the aimTargetDifference range 0.0 to 1.0
|
||||
// val radius = 0.5
|
||||
// val aimX = sph2CartX(aimAzimuthRadians, aimElevationRadians, radius)
|
||||
// val aimY = sph2CartY(aimAzimuthRadians, aimElevationRadians, radius)
|
||||
// val satX = sph2CartX(item.azimuth, item.elevation, radius)
|
||||
// val satY = sph2CartY(item.azimuth, item.elevation, radius)
|
||||
// aimTargetDifference.floatValue = sqrt((satX - aimX).pow(2) + (satY - aimY).pow(2))
|
||||
// val minPlaybackRate = 0.5f
|
||||
// val maxPlaybackRate = 2.0f
|
||||
// val playbackRate =
|
||||
// maxPlaybackRate - (aimTargetDifference.floatValue / (maxPlaybackRate - minPlaybackRate))
|
||||
// soundPool.value?.setRate(beepSoundId.intValue, playbackRate)
|
||||
// }
|
||||
//
|
||||
// LaunchedEffect(aimTargetDifference.floatValue < aimThreshold) {
|
||||
// if (aimTargetDifference.floatValue < aimThreshold) {
|
||||
// view.performHapticFeedback(HapticFeedbackConstantsCompat.VIRTUAL_KEY)
|
||||
// soundPool.value?.pause(beepSoundId.intValue)
|
||||
// } else {
|
||||
// soundPool.value?.resume(beepSoundId.intValue)
|
||||
// }
|
||||
// }
|
||||
//
|
||||
// LaunchedEffect(item.elevation > 0) {
|
||||
// if(item.elevation > 0) {
|
||||
// soundPool.value?.resume(beepSoundId.intValue)
|
||||
// } else {
|
||||
// soundPool.value?.pause(beepSoundId.intValue)
|
||||
// }
|
||||
// }
|
||||
//
|
||||
// DisposableEffect(Unit) {
|
||||
// val audioAttributes = AudioAttributes.Builder()
|
||||
// .setUsage(AudioAttributes.USAGE_ASSISTANCE_SONIFICATION)
|
||||
// .setContentType(AudioAttributes.CONTENT_TYPE_SONIFICATION)
|
||||
// .build()
|
||||
// soundPool.value = SoundPool.Builder()
|
||||
// .setMaxStreams(1)
|
||||
// .setAudioAttributes(audioAttributes)
|
||||
// .build()
|
||||
// beepSoundId.intValue = soundPool.value?.load(context, R.raw.beep, 1) ?: 0
|
||||
// soundPool.value?.setOnLoadCompleteListener { soundPool, _, status ->
|
||||
// if (status == 0) {
|
||||
// soundPool.play(beepSoundId.intValue, 0.5f, 0.5f, 0, -1, 1f)
|
||||
// }
|
||||
// }
|
||||
// onDispose {
|
||||
// soundPool.value?.release()
|
||||
// }
|
||||
// }
|
||||
|
||||
Canvas(modifier = modifier.aspectRatio(1f)) {
|
||||
val radius = (size.minDimension / 2f) * 0.95f
|
||||
if (!trackCreated.value) {
|
||||
trackPath.value = createTrackPath(items, radius)
|
||||
trackEffect.value = createTrackEffect(trackPath.value)
|
||||
trackCreated.value = true
|
||||
}
|
||||
rotate(if (shouldUseCompass) -azimElev.first else 0f) {
|
||||
if (shouldShowSweep) {
|
||||
drawSweep(center, sweepDegrees.floatValue, radius, trackColor)
|
||||
}
|
||||
drawRadar(radius, radarColor, strokeWidth, 3)
|
||||
drawInfo(radius, trackColor, measurer, 3)
|
||||
translate(center.x, center.y) {
|
||||
drawTrack(trackPath.value, trackEffect.value, aimColor, trackColor)
|
||||
if (item.elevation > 0) {
|
||||
drawPosition(item, radius, animScale.value, trackColor)
|
||||
}
|
||||
if (shouldUseCompass) {
|
||||
drawAim(azimElev.first, azimElev.second, radius, strokeWidth, aimColor)
|
||||
}
|
||||
}
|
||||
sweepDegrees.floatValue = (sweepDegrees.floatValue + 360 / 12.0f / 60) % 360
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private fun DrawScope.drawRadar(radius: Float, color: Color, width: Float, circles: Int) {
|
||||
for (i in 0 until circles) {
|
||||
val circleRadius = radius - radius / circles.toFloat() * i.toFloat()
|
||||
drawCircle(color, circleRadius, style = Stroke(width))
|
||||
}
|
||||
drawLine(color, center.copy(x = center.x - radius), center.copy(x = center.x + radius), width)
|
||||
drawLine(color, center.copy(y = center.y - radius), center.copy(y = center.y + radius), width)
|
||||
}
|
||||
|
||||
private fun DrawScope.drawInfo(radius: Float, color: Color, measurer: TextMeasurer, circles: Int) {
|
||||
for (i in 0 until circles) {
|
||||
val textY = (radius - radius / circles * i) - 32f
|
||||
val textDeg = " ${(90 / circles) * (circles - i)}°"
|
||||
drawText(measurer, textDeg, center.copy(y = textY), style = TextStyle(color, 15.sp))
|
||||
}
|
||||
}
|
||||
|
||||
private fun DrawScope.drawTrack(path: Path, effect: PathEffect, color: Color, effectColor: Color) {
|
||||
drawPath(path, color, style = Stroke(6f))
|
||||
drawPath(path, effectColor, style = Stroke(pathEffect = effect))
|
||||
}
|
||||
|
||||
private fun DrawScope.drawPosition(item: OrbitalPos, radius: Float, posRadius: Float, color: Color) {
|
||||
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))
|
||||
drawCircle(color.copy(alpha = 1 - (posRadius / 64f)), posRadius, center.copy(satX, -satY))
|
||||
}
|
||||
|
||||
private fun DrawScope.drawAim(azim: Float, elev: Float, radius: Float, width: Float, color: Color) {
|
||||
val size = 36f
|
||||
val azimRadians = azim.toDouble().toRadians()
|
||||
val tempElevRadians = elev.toDouble().toRadians()
|
||||
val elevRadians = if (tempElevRadians > 0.0) 0.0 else tempElevRadians
|
||||
val aimX = sph2CartX(azimRadians, -elevRadians, radius.toDouble())
|
||||
val aimY = sph2CartY(azimRadians, -elevRadians, radius.toDouble())
|
||||
try {
|
||||
drawLine(color, center.copy(aimX - size, -aimY), center.copy(aimX + size, -aimY), width)
|
||||
drawLine(color, center.copy(aimX, -aimY - size), center.copy(aimX, -aimY + size), width)
|
||||
drawCircle(color, size / 2, center.copy(aimX, -aimY), style = Stroke(width))
|
||||
} catch (exception: Exception) {
|
||||
println(exception)
|
||||
}
|
||||
}
|
||||
|
||||
private fun DrawScope.drawSweep(center: Offset, degrees: Float, radius: Float, color: Color) {
|
||||
val colors = listOf(Color.Transparent, color.copy(alpha = 0.5f), color)
|
||||
val colorStops = listOf(0.64f, 0.995f, 1f)
|
||||
val brush = ShaderBrush(SweepGradientShader(center, colors, colorStops))
|
||||
rotate(-90 + degrees, center) { drawCircle(brush, radius, style = Fill) }
|
||||
}
|
||||
|
||||
private fun createTrackPath(positions: List<OrbitalPos>, radius: Float): Path {
|
||||
val trackPath = Path()
|
||||
positions.forEachIndexed { index, satPos ->
|
||||
val passX = sph2CartX(satPos.azimuth, satPos.elevation, radius.toDouble())
|
||||
val passY = sph2CartY(satPos.azimuth, satPos.elevation, radius.toDouble())
|
||||
if (index == 0) trackPath.moveTo(passX, -passY) else trackPath.lineTo(passX, -passY)
|
||||
}
|
||||
return trackPath
|
||||
}
|
||||
|
||||
private fun createTrackEffect(trackPath: Path): PathEffect {
|
||||
val shape = Path()
|
||||
val shapeRadius = 24f
|
||||
val angle = 120.0.toRadians()
|
||||
shape.moveTo((shapeRadius * cos(angle)).toFloat(), (shapeRadius * sin(angle)).toFloat())
|
||||
for (i in 1 until 3) {
|
||||
val x = (shapeRadius * cos(angle - angle * i)).toFloat()
|
||||
val y = (shapeRadius * sin(angle - angle * i)).toFloat()
|
||||
shape.lineTo(x, y)
|
||||
}
|
||||
shape.close()
|
||||
val trackLength = PathMeasure().apply { setPath(trackPath, false) }.length
|
||||
val advance = trackLength / 2f
|
||||
val phase = trackLength / 4f
|
||||
return PathEffect.stampedPathEffect(shape, advance, phase, StampedPathEffectStyle.Rotate)
|
||||
}
|
||||
|
||||
private fun sph2CartX(azim: Double, elev: Double, r: Double): Float {
|
||||
val radius = r * (PI_2 - elev) / PI_2
|
||||
return (radius * cos(PI_2 - azim)).toFloat()
|
||||
}
|
||||
|
||||
private fun sph2CartY(azim: Double, elev: Double, r: Double): Float {
|
||||
val radius = r * (PI_2 - elev) / PI_2
|
||||
return (radius * sin(PI_2 - azim)).toFloat()
|
||||
}
|
||||
@@ -0,0 +1,243 @@
|
||||
/*
|
||||
* Look4Sat. Amateur radio satellite tracker and pass predictor.
|
||||
* Copyright (C) 2019-2026 Arty Bishop and contributors.
|
||||
*
|
||||
* This program is free software: you can redistribute it and/or modify
|
||||
* it under the terms of the GNU General Public License as published by
|
||||
* the Free Software Foundation, either version 3 of the License, or
|
||||
* (at your option) any later version.
|
||||
*
|
||||
* This program is distributed in the hope that it will be useful,
|
||||
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
* GNU General Public License for more details.
|
||||
*
|
||||
* You should have received a copy of the GNU General Public License
|
||||
* along with this program. If not, see <https://www.gnu.org/licenses/>.
|
||||
*/
|
||||
package com.rtbishop.look4sat.feature.radar
|
||||
|
||||
import androidx.lifecycle.SavedStateHandle
|
||||
import androidx.lifecycle.ViewModel
|
||||
import androidx.lifecycle.ViewModelProvider
|
||||
import androidx.lifecycle.createSavedStateHandle
|
||||
import androidx.lifecycle.viewModelScope
|
||||
import androidx.lifecycle.viewmodel.initializer
|
||||
import androidx.lifecycle.viewmodel.viewModelFactory
|
||||
import com.rtbishop.look4sat.core.domain.model.SatRadio
|
||||
import com.rtbishop.look4sat.core.domain.predict.OrbitalObject
|
||||
import com.rtbishop.look4sat.core.domain.predict.OrbitalPass
|
||||
import com.rtbishop.look4sat.core.domain.predict.OrbitalPos
|
||||
import com.rtbishop.look4sat.core.domain.repository.BtService
|
||||
import com.rtbishop.look4sat.core.domain.repository.ExtendedParams
|
||||
import com.rtbishop.look4sat.core.domain.repository.IContainerProvider
|
||||
import com.rtbishop.look4sat.core.domain.repository.IReporterRepo
|
||||
import com.rtbishop.look4sat.core.domain.repository.ISatelliteRepo
|
||||
import com.rtbishop.look4sat.core.domain.repository.ISensorsRepo
|
||||
import com.rtbishop.look4sat.core.domain.repository.ISettingsRepo
|
||||
import com.rtbishop.look4sat.core.domain.repository.WithoutExtParams
|
||||
import com.rtbishop.look4sat.core.domain.usecase.IAddToCalendar
|
||||
import com.rtbishop.look4sat.core.domain.utility.round
|
||||
import com.rtbishop.look4sat.core.domain.utility.toDegrees
|
||||
import com.rtbishop.look4sat.core.domain.utility.toTimerString
|
||||
import kotlinx.coroutines.delay
|
||||
import kotlinx.coroutines.flow.MutableStateFlow
|
||||
import kotlinx.coroutines.flow.StateFlow
|
||||
import kotlinx.coroutines.flow.update
|
||||
import kotlinx.coroutines.isActive
|
||||
import kotlinx.coroutines.launch
|
||||
|
||||
class RadarViewModel(
|
||||
private val savedStateHandle: SavedStateHandle,
|
||||
private val bluetoothReporter: IReporterRepo<WithoutExtParams>,
|
||||
private val networkReporter: IReporterRepo<ExtendedParams>,
|
||||
private val satelliteRepo: ISatelliteRepo,
|
||||
private val settingsRepo: ISettingsRepo,
|
||||
private val sensorsRepo: ISensorsRepo,
|
||||
private val addToCalendar: IAddToCalendar
|
||||
) : ViewModel() {
|
||||
|
||||
private val _uiState = MutableStateFlow(
|
||||
RadarState(
|
||||
currentPass = null,
|
||||
currentTime = "00:00:00",
|
||||
isCurrentTimeAos = true,
|
||||
orientationValues = sensorsRepo.orientation.value,
|
||||
orbitalPos = null,
|
||||
satTrack = emptyList(),
|
||||
shouldShowSweep = settingsRepo.otherSettings.value.stateOfSweep,
|
||||
shouldUseCompass = settingsRepo.otherSettings.value.stateOfSensors,
|
||||
selectedTransmitterUuid = null,
|
||||
selectedFrequency = null,
|
||||
transmitters = emptyList(),
|
||||
sendAction = ::handleAction,
|
||||
)
|
||||
)
|
||||
private val stationPos = settingsRepo.stationPosition.value
|
||||
private val magDeclination = sensorsRepo.getMagDeclination(stationPos)
|
||||
val uiState: StateFlow<RadarState> = _uiState
|
||||
|
||||
init {
|
||||
if (settingsRepo.otherSettings.value.stateOfSensors) {
|
||||
viewModelScope.launch {
|
||||
sensorsRepo.enableSensor()
|
||||
sensorsRepo.orientation.collect { data ->
|
||||
val orientationValues = Pair(data.first + magDeclination, data.second)
|
||||
_uiState.update { it.copy(orientationValues = orientationValues) }
|
||||
}
|
||||
}
|
||||
}
|
||||
viewModelScope.launch {
|
||||
val catNum = savedStateHandle.get<Int>("catNum") ?: 0
|
||||
val aosTime = savedStateHandle.get<Long>("aosTime") ?: 0L
|
||||
val passes = satelliteRepo.passes.value
|
||||
val pass = passes.find { pass -> pass.catNum == catNum && pass.aosTime == aosTime }
|
||||
val currentPass = pass ?: passes.firstOrNull()
|
||||
currentPass?.let { satPass ->
|
||||
_uiState.update { it.copy(currentPass = satPass) }
|
||||
val transmitters = satelliteRepo.getRadiosWithId(satPass.catNum)
|
||||
while (isActive) {
|
||||
val timeNow = System.currentTimeMillis()
|
||||
val pos = satelliteRepo.getPosition(satPass.orbitalObject, stationPos, timeNow)
|
||||
when {
|
||||
satPass.isDeepSpace -> {
|
||||
val time = 0L.toTimerString()
|
||||
_uiState.update { it.copy(currentTime = time, isCurrentTimeAos = false) }
|
||||
}
|
||||
satPass.aosTime > timeNow -> {
|
||||
val time = satPass.aosTime.minus(timeNow).toTimerString()
|
||||
_uiState.update { it.copy(currentTime = time, isCurrentTimeAos = true) }
|
||||
}
|
||||
else -> {
|
||||
val time = satPass.losTime.minus(timeNow).toTimerString()
|
||||
_uiState.update { it.copy(currentTime = time, isCurrentTimeAos = false) }
|
||||
}
|
||||
}
|
||||
processRadios(transmitters, satPass.orbitalObject, timeNow)
|
||||
sendPassData(satPass, pos, satPass.orbitalObject)
|
||||
sendPassDataBT(pos)
|
||||
delay(1000)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
override fun onCleared() {
|
||||
sensorsRepo.disableSensor()
|
||||
super.onCleared()
|
||||
}
|
||||
|
||||
private fun handleAction(action: RadarAction) {
|
||||
when (action) {
|
||||
is RadarAction.AddToCalendar -> addToCalendar(action.name, action.aosTime, action.losTime)
|
||||
is RadarAction.SelectTransmitter -> { _uiState.update { it.copy(selectedTransmitterUuid = action.uuid) } }
|
||||
}
|
||||
}
|
||||
|
||||
private suspend fun sendPassData(orbitalPass: OrbitalPass, orbitalPos: OrbitalPos, orbitalObject: OrbitalObject) {
|
||||
var track: List<OrbitalPos> = emptyList()
|
||||
if (!orbitalPass.isDeepSpace) {
|
||||
track = satelliteRepo.getTrack(
|
||||
orbitalObject, stationPos, orbitalPass.aosTime, orbitalPass.losTime
|
||||
)
|
||||
}
|
||||
_uiState.update { it.copy(orbitalPos = orbitalPos, satTrack = track) }
|
||||
viewModelScope.launch {
|
||||
if (settingsRepo.rcSettings.value.rotatorState) {
|
||||
val server = settingsRepo.rcSettings.value.rotatorAddress
|
||||
val port = settingsRepo.rcSettings.value.rotatorPort.toInt()
|
||||
val format = settingsRepo.rcSettings.value.rotatorFormat
|
||||
val azimuth = orbitalPos.azimuth.toDegrees().round(2)
|
||||
val elevation = orbitalPos.elevation.toDegrees().round(2)
|
||||
networkReporter.reportRotation(format, azimuth, elevation, ExtendedParams(server, port))
|
||||
}
|
||||
if (settingsRepo.rcSettings.value.frequencyState) {
|
||||
_uiState.value.selectedFrequency?.let { freq ->
|
||||
val server = settingsRepo.rcSettings.value.frequencyAddress
|
||||
val port = settingsRepo.rcSettings.value.frequencyPort.toInt()
|
||||
val format = settingsRepo.rcSettings.value.frequencyFormat
|
||||
networkReporter.reportFrequency(format,freq, ExtendedParams(server, port))
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private fun sendPassDataBT(orbitalPos: OrbitalPos) {
|
||||
viewModelScope.launch {
|
||||
if (settingsRepo.rcSettings.value.bluetoothRotatorState) {
|
||||
val btRotatorDevice = settingsRepo.rcSettings.value.bluetoothRotatorAddress
|
||||
if (bluetoothReporter.isConnected(BtService.ROTATOR)) {
|
||||
val format = settingsRepo.rcSettings.value.bluetoothRotatorFormat
|
||||
val azimuth = orbitalPos.azimuth.toDegrees().round(0)
|
||||
val elevation = orbitalPos.elevation.toDegrees().round(0)
|
||||
bluetoothReporter.reportRotation(format, azimuth, elevation, WithoutExtParams(0))
|
||||
} else if (!bluetoothReporter.isConnecting(BtService.ROTATOR)) {
|
||||
// Log.i("BTReporter", "BTReporter (rotator): Attempting to connect...")
|
||||
bluetoothReporter.connect(BtService.ROTATOR,btRotatorDevice)
|
||||
}
|
||||
}
|
||||
if (settingsRepo.rcSettings.value.bluetoothFrequencyState) {
|
||||
val btFrequencyDevice = settingsRepo.rcSettings.value.bluetoothFrequencyAddress
|
||||
if (bluetoothReporter.isConnected(BtService.FREQUENCY)) {
|
||||
val format = settingsRepo.rcSettings.value.bluetoothFrequencyFormat
|
||||
val frequency = _uiState.value.selectedFrequency
|
||||
frequency?.let { freq ->
|
||||
bluetoothReporter.reportFrequency(format, freq, WithoutExtParams(0))
|
||||
}
|
||||
} else if (!bluetoothReporter.isConnecting(BtService.FREQUENCY)) {
|
||||
// Log.i("BTReporter", "BTReporter (frequency): Attempting to connect...")
|
||||
bluetoothReporter.connect(BtService.FREQUENCY, btFrequencyDevice)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private suspend fun processRadios(radios: List<SatRadio>, orbitalObject: OrbitalObject, time: Long) {
|
||||
val transmitters = satelliteRepo.getRadios(orbitalObject, stationPos, radios, time)
|
||||
_uiState.update { state ->
|
||||
if (!settingsRepo.rcSettings.value.frequencyState && !settingsRepo.rcSettings.value.bluetoothFrequencyState) {
|
||||
return@update state.copy(
|
||||
transmitters = transmitters,
|
||||
selectedTransmitterUuid = null,
|
||||
selectedFrequency = null
|
||||
)
|
||||
}
|
||||
val selectedUuid = state.selectedTransmitterUuid ?: transmitters.firstOrNull()?.uuid
|
||||
val selectedRadio = transmitters.firstOrNull { it.uuid == selectedUuid }
|
||||
val freq = selectedRadio?.let { radio ->
|
||||
val low = radio.downlinkLow
|
||||
val high = radio.downlinkHigh
|
||||
when {
|
||||
low != null && high != null ->
|
||||
(low + high) / 2
|
||||
low != null ->
|
||||
low
|
||||
else -> null
|
||||
}
|
||||
}
|
||||
state.copy(
|
||||
transmitters = transmitters,
|
||||
selectedTransmitterUuid = selectedUuid,
|
||||
selectedFrequency = freq
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
companion object {
|
||||
val Factory: ViewModelProvider.Factory = viewModelFactory {
|
||||
val applicationKey = ViewModelProvider.AndroidViewModelFactory.APPLICATION_KEY
|
||||
initializer {
|
||||
val container = (this[applicationKey] as IContainerProvider).getMainContainer()
|
||||
RadarViewModel(
|
||||
createSavedStateHandle(),
|
||||
container.provideBluetoothReporter(),
|
||||
container.provideNetworkReporter(),
|
||||
container.satelliteRepo,
|
||||
container.settingsRepo,
|
||||
container.provideSensorsRepo(),
|
||||
container.provideAddToCalendar()
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
Reference in new issue
Block a user