mirror of
https://github.com/atsunatsu/Look4Sat.git
synced 2026-10-02 03:15:37 +00:00
fix: smooth dual-station elevation curve
- Fix cubic Bezier control points (proper Catmull-Rom to Bezier conversion) - Refine AOS/LOS more robustly by walking from edge into the pass window - Still 5s sampling for smooth curves
This commit is contained in:
1 parent
5de370983d
commit
d7e1ce11c9
9 files changed
+437
-221
No files matched your search
@@ -206,7 +206,15 @@ fun MainScreen(navigateToRadar: () -> Unit = {}) {
|
|||||||
}
|
}
|
||||||
entry<Screen.Mutual> {
|
entry<Screen.Mutual> {
|
||||||
val viewModel = MutualViewModel(container.satelliteRepo, container.settingsRepo)
|
val viewModel = MutualViewModel(container.satelliteRepo, container.settingsRepo)
|
||||||
MutualScreen(viewModel = viewModel)
|
MutualScreen(
|
||||||
|
viewModel = viewModel,
|
||||||
|
navigateUp = navigateBack,
|
||||||
|
navigateToRadar = { catNum, aosTime, pass ->
|
||||||
|
pass?.let { container.setMutualPassData(it) }
|
||||||
|
container.satelliteRepo.selectPass(catNum, aosTime)
|
||||||
|
backStack.add(Screen.Radar)
|
||||||
|
}
|
||||||
|
)
|
||||||
}
|
}
|
||||||
entry<Screen.Settings> {
|
entry<Screen.Settings> {
|
||||||
SettingsDestination()
|
SettingsDestination()
|
||||||
|
|||||||
@@ -50,6 +50,7 @@ import com.rtbishop.look4sat.core.domain.repository.ISatelliteRepo
|
|||||||
import com.rtbishop.look4sat.core.domain.repository.ISelectionRepo
|
import com.rtbishop.look4sat.core.domain.repository.ISelectionRepo
|
||||||
import com.rtbishop.look4sat.core.domain.repository.ISensorsRepo
|
import com.rtbishop.look4sat.core.domain.repository.ISensorsRepo
|
||||||
import com.rtbishop.look4sat.core.domain.repository.ISettingsRepo
|
import com.rtbishop.look4sat.core.domain.repository.ISettingsRepo
|
||||||
|
import com.rtbishop.look4sat.core.domain.repository.MutualPassData
|
||||||
import com.rtbishop.look4sat.core.domain.source.ILocalSource
|
import com.rtbishop.look4sat.core.domain.source.ILocalSource
|
||||||
import com.rtbishop.look4sat.core.domain.source.IRemoteSource
|
import com.rtbishop.look4sat.core.domain.source.IRemoteSource
|
||||||
import com.rtbishop.look4sat.core.domain.usecase.IAddToCalendar
|
import com.rtbishop.look4sat.core.domain.usecase.IAddToCalendar
|
||||||
@@ -61,6 +62,9 @@ import kotlinx.coroutines.CoroutineExceptionHandler
|
|||||||
import kotlinx.coroutines.CoroutineScope
|
import kotlinx.coroutines.CoroutineScope
|
||||||
import kotlinx.coroutines.Dispatchers
|
import kotlinx.coroutines.Dispatchers
|
||||||
import kotlinx.coroutines.SupervisorJob
|
import kotlinx.coroutines.SupervisorJob
|
||||||
|
import kotlinx.coroutines.flow.MutableStateFlow
|
||||||
|
import kotlinx.coroutines.flow.StateFlow
|
||||||
|
import kotlinx.coroutines.flow.asStateFlow
|
||||||
import okhttp3.OkHttpClient
|
import okhttp3.OkHttpClient
|
||||||
|
|
||||||
class MainContainer(private val context: Context) : IMainContainer {
|
class MainContainer(private val context: Context) : IMainContainer {
|
||||||
@@ -77,6 +81,13 @@ class MainContainer(private val context: Context) : IMainContainer {
|
|||||||
RadioTrackingService(appScope, manager, satelliteRepo, settingsRepo)
|
RadioTrackingService(appScope, manager, satelliteRepo, settingsRepo)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private val _mutualPassData = MutableStateFlow(MutualPassData())
|
||||||
|
override val mutualPassData: StateFlow<MutualPassData> = _mutualPassData.asStateFlow()
|
||||||
|
|
||||||
|
override fun setMutualPassData(data: MutualPassData) {
|
||||||
|
_mutualPassData.value = data
|
||||||
|
}
|
||||||
|
|
||||||
override fun provideAddToCalendar(): IAddToCalendar = AddToCalendar(context)
|
override fun provideAddToCalendar(): IAddToCalendar = AddToCalendar(context)
|
||||||
|
|
||||||
override fun provideShowToast(): IShowToast = ShowToast(context)
|
override fun provideShowToast(): IShowToast = ShowToast(context)
|
||||||
|
|||||||
+13
-1
@@ -16,12 +16,13 @@
|
|||||||
* along with this program. If not, see <https://www.gnu.org/licenses/>.
|
* along with this program. If not, see <https://www.gnu.org/licenses/>.
|
||||||
*/
|
*/
|
||||||
package com.rtbishop.look4sat.core.domain.repository
|
package com.rtbishop.look4sat.core.domain.repository
|
||||||
|
import com.rtbishop.look4sat.core.domain.model.SatRadio
|
||||||
import com.rtbishop.look4sat.core.domain.usecase.IAddToCalendar
|
import com.rtbishop.look4sat.core.domain.usecase.IAddToCalendar
|
||||||
import com.rtbishop.look4sat.core.domain.usecase.IAudioCapture
|
import com.rtbishop.look4sat.core.domain.usecase.IAudioCapture
|
||||||
import com.rtbishop.look4sat.core.domain.usecase.ISaveImage
|
import com.rtbishop.look4sat.core.domain.usecase.ISaveImage
|
||||||
import com.rtbishop.look4sat.core.domain.usecase.IShowToast
|
import com.rtbishop.look4sat.core.domain.usecase.IShowToast
|
||||||
import kotlinx.coroutines.CoroutineScope
|
import kotlinx.coroutines.CoroutineScope
|
||||||
|
import kotlinx.coroutines.flow.StateFlow
|
||||||
|
|
||||||
interface IMainContainer {
|
interface IMainContainer {
|
||||||
val appScope: CoroutineScope
|
val appScope: CoroutineScope
|
||||||
@@ -30,6 +31,8 @@ interface IMainContainer {
|
|||||||
val satelliteRepo: ISatelliteRepo
|
val satelliteRepo: ISatelliteRepo
|
||||||
val databaseRepo: IDatabaseRepo
|
val databaseRepo: IDatabaseRepo
|
||||||
val radioTrackingService: IRadioTrackingService
|
val radioTrackingService: IRadioTrackingService
|
||||||
|
val mutualPassData: StateFlow<MutualPassData>
|
||||||
|
fun setMutualPassData(data: MutualPassData)
|
||||||
fun provideAddToCalendar(): IAddToCalendar
|
fun provideAddToCalendar(): IAddToCalendar
|
||||||
fun provideShowToast(): IShowToast
|
fun provideShowToast(): IShowToast
|
||||||
fun provideBluetoothReporter(): IReporter
|
fun provideBluetoothReporter(): IReporter
|
||||||
@@ -41,6 +44,15 @@ interface IMainContainer {
|
|||||||
fun provideSaveImage(): ISaveImage
|
fun provideSaveImage(): ISaveImage
|
||||||
}
|
}
|
||||||
|
|
||||||
|
data class MutualPassData(
|
||||||
|
val samples: List<Pair<Long, Pair<Double, Double>>> = emptyList(),
|
||||||
|
val startTime: Long = 0L,
|
||||||
|
val endTime: Long = 0L,
|
||||||
|
val maxElev: Double = 10.0,
|
||||||
|
val labelA: String = "你",
|
||||||
|
val labelB: String = "友台"
|
||||||
|
)
|
||||||
|
|
||||||
interface IContainerProvider {
|
interface IContainerProvider {
|
||||||
fun getMainContainer(): IMainContainer
|
fun getMainContainer(): IMainContainer
|
||||||
}
|
}
|
||||||
+15
-14
@@ -158,20 +158,21 @@ fun ElevationCurveChart(
|
|||||||
pathA.moveTo(ptsA[0].x, ptsA[0].y)
|
pathA.moveTo(ptsA[0].x, ptsA[0].y)
|
||||||
pathB.moveTo(ptsB[0].x, ptsB[0].y)
|
pathB.moveTo(ptsB[0].x, ptsB[0].y)
|
||||||
for (i in 1 until ptsA.size) {
|
for (i in 1 until ptsA.size) {
|
||||||
val prev = ptsA.getOrNull(i - 2) ?: ptsA[i - 1]
|
val p0a = ptsA.getOrNull(i - 2) ?: ptsA[i - 1]
|
||||||
val curr = ptsA[i - 1]
|
val p1a = ptsA[i - 1]
|
||||||
val next = ptsA.getOrNull(i + 1) ?: ptsA[i]
|
val p2a = ptsA[i]
|
||||||
val cp1 = Offset(curr.x + (next.x - prev.x) / 6f, curr.y + (next.y - prev.y) / 6f)
|
val p3a = ptsA.getOrNull(i + 1) ?: ptsA[i]
|
||||||
val cp2 = Offset(ptsA[i].x - (ptsA[i].x - curr.x) / 6f, ptsA[i].y - (ptsA[i].y - curr.y) / 6f)
|
val cp1a = Offset(p1a.x + (p2a.x - p0a.x) / 6f, p1a.y + (p2a.y - p0a.y) / 6f)
|
||||||
pathA.cubicTo(cp1.x, cp1.y, cp2.x, cp2.y, ptsA[i].x, ptsA[i].y)
|
val cp2a = Offset(p2a.x - (p3a.x - p1a.x) / 6f, p2a.y - (p3a.y - p1a.y) / 6f)
|
||||||
}
|
pathA.cubicTo(cp1a.x, cp1a.y, cp2a.x, cp2a.y, p2a.x, p2a.y)
|
||||||
for (i in 1 until ptsB.size) {
|
|
||||||
val prev = ptsB.getOrNull(i - 2) ?: ptsB[i - 1]
|
val p0b = ptsB.getOrNull(i - 2) ?: ptsB[i - 1]
|
||||||
val curr = ptsB[i - 1]
|
val p1b = ptsB[i - 1]
|
||||||
val next = ptsB.getOrNull(i + 1) ?: ptsB[i]
|
val p2b = ptsB[i]
|
||||||
val cp1 = Offset(curr.x + (next.x - prev.x) / 6f, curr.y + (next.y - prev.y) / 6f)
|
val p3b = ptsB.getOrNull(i + 1) ?: ptsB[i]
|
||||||
val cp2 = Offset(ptsB[i].x - (ptsB[i].x - curr.x) / 6f, ptsB[i].y - (ptsB[i].y - curr.y) / 6f)
|
val cp1b = Offset(p1b.x + (p2b.x - p0b.x) / 6f, p1b.y + (p2b.y - p0b.y) / 6f)
|
||||||
pathB.cubicTo(cp1.x, cp1.y, cp2.x, cp2.y, ptsB[i].x, ptsB[i].y)
|
val cp2b = Offset(p2b.x - (p3b.x - p1b.x) / 6f, p2b.y - (p3b.y - p1b.y) / 6f)
|
||||||
|
pathB.cubicTo(cp1b.x, cp1b.y, cp2b.x, cp2b.y, p2b.x, p2b.y)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
+334
-184
@@ -17,6 +17,10 @@
|
|||||||
*/
|
*/
|
||||||
package com.rtbishop.look4sat.feature.mutual
|
package com.rtbishop.look4sat.feature.mutual
|
||||||
|
|
||||||
|
import androidx.compose.animation.AnimatedVisibility
|
||||||
|
import androidx.compose.animation.expandVertically
|
||||||
|
import androidx.compose.animation.shrinkVertically
|
||||||
|
import androidx.compose.foundation.clickable
|
||||||
import androidx.compose.foundation.layout.Arrangement
|
import androidx.compose.foundation.layout.Arrangement
|
||||||
import androidx.compose.foundation.layout.Column
|
import androidx.compose.foundation.layout.Column
|
||||||
import androidx.compose.foundation.layout.Row
|
import androidx.compose.foundation.layout.Row
|
||||||
@@ -30,11 +34,14 @@ import androidx.compose.foundation.lazy.LazyColumn
|
|||||||
import androidx.compose.foundation.lazy.itemsIndexed
|
import androidx.compose.foundation.lazy.itemsIndexed
|
||||||
import androidx.compose.foundation.text.KeyboardOptions
|
import androidx.compose.foundation.text.KeyboardOptions
|
||||||
import androidx.compose.material3.Button
|
import androidx.compose.material3.Button
|
||||||
import androidx.compose.material3.Card
|
|
||||||
import androidx.compose.material3.CardDefaults
|
import androidx.compose.material3.CardDefaults
|
||||||
import androidx.compose.material3.CircularProgressIndicator
|
import androidx.compose.material3.CircularProgressIndicator
|
||||||
|
import androidx.compose.material3.ElevatedCard
|
||||||
import androidx.compose.material3.FilterChip
|
import androidx.compose.material3.FilterChip
|
||||||
|
import androidx.compose.material3.HorizontalDivider
|
||||||
|
import androidx.compose.material3.Icon
|
||||||
import androidx.compose.material3.MaterialTheme
|
import androidx.compose.material3.MaterialTheme
|
||||||
|
import androidx.compose.material3.OutlinedButton
|
||||||
import androidx.compose.material3.OutlinedTextField
|
import androidx.compose.material3.OutlinedTextField
|
||||||
import androidx.compose.material3.Slider
|
import androidx.compose.material3.Slider
|
||||||
import androidx.compose.material3.Text
|
import androidx.compose.material3.Text
|
||||||
@@ -42,9 +49,20 @@ import androidx.compose.runtime.Composable
|
|||||||
import androidx.compose.runtime.collectAsState
|
import androidx.compose.runtime.collectAsState
|
||||||
import androidx.compose.runtime.getValue
|
import androidx.compose.runtime.getValue
|
||||||
import androidx.compose.runtime.remember
|
import androidx.compose.runtime.remember
|
||||||
|
import androidx.compose.ui.Alignment
|
||||||
import androidx.compose.ui.Modifier
|
import androidx.compose.ui.Modifier
|
||||||
|
import androidx.compose.ui.res.painterResource
|
||||||
|
import androidx.compose.ui.res.stringResource
|
||||||
|
import androidx.compose.ui.text.font.FontWeight
|
||||||
import androidx.compose.ui.text.input.KeyboardType
|
import androidx.compose.ui.text.input.KeyboardType
|
||||||
import androidx.compose.ui.unit.dp
|
import androidx.compose.ui.unit.dp
|
||||||
|
import androidx.compose.ui.unit.sp
|
||||||
|
import com.rtbishop.look4sat.core.domain.repository.MutualPassData
|
||||||
|
import com.rtbishop.look4sat.core.presentation.IconCard
|
||||||
|
import com.rtbishop.look4sat.core.presentation.R
|
||||||
|
import com.rtbishop.look4sat.core.presentation.ScreenColumn
|
||||||
|
import com.rtbishop.look4sat.core.presentation.TopBar
|
||||||
|
import com.rtbishop.look4sat.core.presentation.isVerticalLayout
|
||||||
import java.text.SimpleDateFormat
|
import java.text.SimpleDateFormat
|
||||||
import java.util.Date
|
import java.util.Date
|
||||||
import java.util.Locale
|
import java.util.Locale
|
||||||
@@ -52,183 +70,275 @@ import java.util.Locale
|
|||||||
@Composable
|
@Composable
|
||||||
fun MutualScreen(
|
fun MutualScreen(
|
||||||
viewModel: MutualViewModel,
|
viewModel: MutualViewModel,
|
||||||
|
navigateUp: () -> Unit = {},
|
||||||
|
navigateToRadar: (Int, Long, MutualPassData?) -> Unit = { _, _, _ -> },
|
||||||
modifier: Modifier = Modifier
|
modifier: Modifier = Modifier
|
||||||
) {
|
) {
|
||||||
val state by viewModel.uiState.collectAsState()
|
val state by viewModel.uiState.collectAsState()
|
||||||
|
|
||||||
|
ScreenColumn(
|
||||||
|
topBar = { isVertical ->
|
||||||
|
TopBar(
|
||||||
|
isVerticalLayout = isVertical,
|
||||||
|
startAction = {
|
||||||
|
IconCard(action = navigateUp, resId = R.drawable.ic_back)
|
||||||
|
},
|
||||||
|
topInfo = {
|
||||||
|
Text(
|
||||||
|
text = "对台过境",
|
||||||
|
style = MaterialTheme.typography.titleMedium,
|
||||||
|
fontWeight = FontWeight.Bold
|
||||||
|
)
|
||||||
|
},
|
||||||
|
bottomInfo = {
|
||||||
|
if (state.mutualPasses.isNotEmpty()) {
|
||||||
|
Text(
|
||||||
|
text = "找到 ${state.mutualPasses.size} 个过境",
|
||||||
|
style = MaterialTheme.typography.bodySmall,
|
||||||
|
color = MaterialTheme.colorScheme.onSurfaceVariant
|
||||||
|
)
|
||||||
|
}
|
||||||
|
},
|
||||||
|
endAction = {
|
||||||
|
if (state.isCalculating) {
|
||||||
|
CircularProgressIndicator(
|
||||||
|
modifier = Modifier
|
||||||
|
.height(24.dp)
|
||||||
|
.width(24.dp),
|
||||||
|
strokeWidth = 2.dp
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
)
|
||||||
|
}
|
||||||
|
) { isVertical ->
|
||||||
|
MutualContent(
|
||||||
|
state = state,
|
||||||
|
isVertical = isVertical,
|
||||||
|
onQuery = viewModel::queryMutualPasses,
|
||||||
|
onSelectPass = viewModel::onSelectPass,
|
||||||
|
onNavigateToRadar = navigateToRadar,
|
||||||
|
onStationALat = viewModel::onStationALat,
|
||||||
|
onStationALon = viewModel::onStationALon,
|
||||||
|
onStationAGrid = viewModel::onStationAGrid,
|
||||||
|
onStationAMinElev = viewModel::onStationAMinElev,
|
||||||
|
onStationBLat = viewModel::onStationBLat,
|
||||||
|
onStationBLon = viewModel::onStationBLon,
|
||||||
|
onStationBGrid = viewModel::onStationBGrid,
|
||||||
|
onStationBMinElev = viewModel::onStationBMinElev,
|
||||||
|
onHoursAhead = viewModel::onHoursAhead,
|
||||||
|
onClearError = viewModel::clearError
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@Composable
|
||||||
|
private fun MutualContent(
|
||||||
|
state: MutualUiState,
|
||||||
|
isVertical: Boolean,
|
||||||
|
onQuery: () -> Unit,
|
||||||
|
onSelectPass: (Int) -> Unit,
|
||||||
|
onNavigateToRadar: (Int, Long, MutualPassData?) -> Unit,
|
||||||
|
onStationALat: (String) -> Unit,
|
||||||
|
onStationALon: (String) -> Unit,
|
||||||
|
onStationAGrid: (String) -> Unit,
|
||||||
|
onStationAMinElev: (Double) -> Unit,
|
||||||
|
onStationBLat: (String) -> Unit,
|
||||||
|
onStationBLon: (String) -> Unit,
|
||||||
|
onStationBGrid: (String) -> Unit,
|
||||||
|
onStationBMinElev: (Double) -> Unit,
|
||||||
|
onHoursAhead: (Int) -> Unit,
|
||||||
|
onClearError: () -> Unit
|
||||||
|
) {
|
||||||
val timeFormat = remember { SimpleDateFormat("MM/dd HH:mm", Locale.getDefault()) }
|
val timeFormat = remember { SimpleDateFormat("MM/dd HH:mm", Locale.getDefault()) }
|
||||||
|
|
||||||
LazyColumn(
|
LazyColumn(
|
||||||
modifier = modifier
|
modifier = Modifier.fillMaxSize(),
|
||||||
.fillMaxSize()
|
verticalArrangement = Arrangement.spacedBy(6.dp)
|
||||||
.padding(horizontal = 12.dp, vertical = 8.dp),
|
|
||||||
verticalArrangement = Arrangement.spacedBy(8.dp)
|
|
||||||
) {
|
) {
|
||||||
// Title
|
|
||||||
item {
|
|
||||||
Text(
|
|
||||||
text = "对台过境查询",
|
|
||||||
style = MaterialTheme.typography.headlineSmall,
|
|
||||||
modifier = Modifier.padding(bottom = 4.dp)
|
|
||||||
)
|
|
||||||
Text(
|
|
||||||
text = "查看双方同时过境的卫星,然后相约 QSO 吧!",
|
|
||||||
style = MaterialTheme.typography.bodySmall,
|
|
||||||
color = MaterialTheme.colorScheme.onSurfaceVariant
|
|
||||||
)
|
|
||||||
}
|
|
||||||
|
|
||||||
// Error message
|
// Error message
|
||||||
val errorMsg = state.errorMessage
|
val errorMsg = state.errorMessage
|
||||||
if (errorMsg != null) {
|
if (errorMsg != null) {
|
||||||
item {
|
item {
|
||||||
Text(
|
ElevatedCard(
|
||||||
text = errorMsg,
|
modifier = Modifier.fillMaxWidth(),
|
||||||
color = MaterialTheme.colorScheme.error,
|
colors = CardDefaults.elevatedCardColors(
|
||||||
style = MaterialTheme.typography.bodyMedium
|
containerColor = MaterialTheme.colorScheme.errorContainer
|
||||||
)
|
)
|
||||||
}
|
) {
|
||||||
}
|
Text(
|
||||||
|
text = errorMsg,
|
||||||
// Station A input
|
color = MaterialTheme.colorScheme.onErrorContainer,
|
||||||
item {
|
modifier = Modifier.padding(12.dp),
|
||||||
Text("你的位置", style = MaterialTheme.typography.titleSmall)
|
style = MaterialTheme.typography.bodyMedium
|
||||||
Row(
|
|
||||||
modifier = Modifier.fillMaxWidth(),
|
|
||||||
horizontalArrangement = Arrangement.spacedBy(8.dp)
|
|
||||||
) {
|
|
||||||
OutlinedTextField(
|
|
||||||
value = state.stationALat,
|
|
||||||
onValueChange = viewModel::onStationALat,
|
|
||||||
label = { Text("纬度") },
|
|
||||||
placeholder = { Text("39.9042") },
|
|
||||||
keyboardOptions = KeyboardOptions(keyboardType = KeyboardType.Decimal),
|
|
||||||
singleLine = true,
|
|
||||||
modifier = Modifier.weight(1f)
|
|
||||||
)
|
|
||||||
OutlinedTextField(
|
|
||||||
value = state.stationALon,
|
|
||||||
onValueChange = viewModel::onStationALon,
|
|
||||||
label = { Text("经度") },
|
|
||||||
placeholder = { Text("116.4074") },
|
|
||||||
keyboardOptions = KeyboardOptions(keyboardType = KeyboardType.Decimal),
|
|
||||||
singleLine = true,
|
|
||||||
modifier = Modifier.weight(1f)
|
|
||||||
)
|
|
||||||
}
|
|
||||||
OutlinedTextField(
|
|
||||||
value = state.stationAGrid,
|
|
||||||
onValueChange = viewModel::onStationAGrid,
|
|
||||||
label = { Text("网格(4/6/8位,填此可省略经纬度)") },
|
|
||||||
placeholder = { Text("ON79uj") },
|
|
||||||
singleLine = true,
|
|
||||||
modifier = Modifier.fillMaxWidth()
|
|
||||||
)
|
|
||||||
Text("最小仰角:${state.stationAMinElev.toInt()}°", style = MaterialTheme.typography.bodySmall)
|
|
||||||
Slider(
|
|
||||||
value = state.stationAMinElev.toFloat(),
|
|
||||||
onValueChange = { viewModel.onStationAMinElev(it.toDouble()) },
|
|
||||||
valueRange = 0f..90f,
|
|
||||||
steps = 17,
|
|
||||||
modifier = Modifier.fillMaxWidth()
|
|
||||||
)
|
|
||||||
}
|
|
||||||
|
|
||||||
// Station B input
|
|
||||||
item {
|
|
||||||
Text("友台位置", style = MaterialTheme.typography.titleSmall)
|
|
||||||
Row(
|
|
||||||
modifier = Modifier.fillMaxWidth(),
|
|
||||||
horizontalArrangement = Arrangement.spacedBy(8.dp)
|
|
||||||
) {
|
|
||||||
OutlinedTextField(
|
|
||||||
value = state.stationBLat,
|
|
||||||
onValueChange = viewModel::onStationBLat,
|
|
||||||
label = { Text("纬度") },
|
|
||||||
placeholder = { Text("34.0522") },
|
|
||||||
keyboardOptions = KeyboardOptions(keyboardType = KeyboardType.Decimal),
|
|
||||||
singleLine = true,
|
|
||||||
modifier = Modifier.weight(1f)
|
|
||||||
)
|
|
||||||
OutlinedTextField(
|
|
||||||
value = state.stationBLon,
|
|
||||||
onValueChange = viewModel::onStationBLon,
|
|
||||||
label = { Text("经度") },
|
|
||||||
placeholder = { Text("-118.2437") },
|
|
||||||
keyboardOptions = KeyboardOptions(keyboardType = KeyboardType.Decimal),
|
|
||||||
singleLine = true,
|
|
||||||
modifier = Modifier.weight(1f)
|
|
||||||
)
|
|
||||||
}
|
|
||||||
OutlinedTextField(
|
|
||||||
value = state.stationBGrid,
|
|
||||||
onValueChange = viewModel::onStationBGrid,
|
|
||||||
label = { Text("网格(4/6/8位,填此可省略经纬度)") },
|
|
||||||
placeholder = { Text("PM01tv") },
|
|
||||||
singleLine = true,
|
|
||||||
modifier = Modifier.fillMaxWidth()
|
|
||||||
)
|
|
||||||
Text("最小仰角:${state.stationBMinElev.toInt()}°", style = MaterialTheme.typography.bodySmall)
|
|
||||||
Slider(
|
|
||||||
value = state.stationBMinElev.toFloat(),
|
|
||||||
onValueChange = { viewModel.onStationBMinElev(it.toDouble()) },
|
|
||||||
valueRange = 0f..90f,
|
|
||||||
steps = 17,
|
|
||||||
modifier = Modifier.fillMaxWidth()
|
|
||||||
)
|
|
||||||
}
|
|
||||||
|
|
||||||
// Time range
|
|
||||||
item {
|
|
||||||
Text("时间范围", style = MaterialTheme.typography.titleSmall)
|
|
||||||
Row(
|
|
||||||
modifier = Modifier.fillMaxWidth(),
|
|
||||||
horizontalArrangement = Arrangement.spacedBy(8.dp)
|
|
||||||
) {
|
|
||||||
listOf(6, 12, 24, 48, 72).forEach { hours ->
|
|
||||||
FilterChip(
|
|
||||||
selected = state.hoursAhead == hours,
|
|
||||||
onClick = { viewModel.onHoursAhead(hours) },
|
|
||||||
label = { Text("${hours}h") }
|
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// Query button
|
// Input form
|
||||||
item {
|
item {
|
||||||
Button(
|
ElevatedCard(modifier = Modifier.fillMaxWidth()) {
|
||||||
onClick = { viewModel.queryMutualPasses() },
|
Column(
|
||||||
enabled = !state.isCalculating,
|
modifier = Modifier
|
||||||
modifier = Modifier.fillMaxWidth()
|
.fillMaxWidth()
|
||||||
) {
|
.padding(12.dp),
|
||||||
if (state.isCalculating) {
|
verticalArrangement = Arrangement.spacedBy(8.dp)
|
||||||
CircularProgressIndicator(
|
) {
|
||||||
modifier = Modifier.height(20.dp).width(20.dp),
|
Text(
|
||||||
strokeWidth = 2.dp
|
text = "你的位置",
|
||||||
|
style = MaterialTheme.typography.titleSmall,
|
||||||
|
fontWeight = FontWeight.Medium
|
||||||
)
|
)
|
||||||
Spacer(Modifier.width(8.dp))
|
Row(
|
||||||
|
modifier = Modifier.fillMaxWidth(),
|
||||||
|
horizontalArrangement = Arrangement.spacedBy(8.dp)
|
||||||
|
) {
|
||||||
|
OutlinedTextField(
|
||||||
|
value = state.stationALat,
|
||||||
|
onValueChange = onStationALat,
|
||||||
|
label = { Text("纬度") },
|
||||||
|
placeholder = { Text("39.9042") },
|
||||||
|
keyboardOptions = KeyboardOptions(keyboardType = KeyboardType.Decimal),
|
||||||
|
singleLine = true,
|
||||||
|
modifier = Modifier.weight(1f)
|
||||||
|
)
|
||||||
|
OutlinedTextField(
|
||||||
|
value = state.stationALon,
|
||||||
|
onValueChange = onStationALon,
|
||||||
|
label = { Text("经度") },
|
||||||
|
placeholder = { Text("116.4074") },
|
||||||
|
keyboardOptions = KeyboardOptions(keyboardType = KeyboardType.Decimal),
|
||||||
|
singleLine = true,
|
||||||
|
modifier = Modifier.weight(1f)
|
||||||
|
)
|
||||||
|
}
|
||||||
|
OutlinedTextField(
|
||||||
|
value = state.stationAGrid,
|
||||||
|
onValueChange = onStationAGrid,
|
||||||
|
label = { Text("网格(4/6/8位,填此可省略经纬度)") },
|
||||||
|
placeholder = { Text("ON79uj") },
|
||||||
|
singleLine = true,
|
||||||
|
modifier = Modifier.fillMaxWidth()
|
||||||
|
)
|
||||||
|
Text(
|
||||||
|
text = "最小仰角:${state.stationAMinElev.toInt()}°",
|
||||||
|
style = MaterialTheme.typography.bodySmall
|
||||||
|
)
|
||||||
|
Slider(
|
||||||
|
value = state.stationAMinElev.toFloat(),
|
||||||
|
onValueChange = { onStationAMinElev(it.toDouble()) },
|
||||||
|
valueRange = 0f..90f,
|
||||||
|
steps = 17
|
||||||
|
)
|
||||||
|
|
||||||
|
HorizontalDivider()
|
||||||
|
|
||||||
|
Text(
|
||||||
|
text = "友台位置",
|
||||||
|
style = MaterialTheme.typography.titleSmall,
|
||||||
|
fontWeight = FontWeight.Medium
|
||||||
|
)
|
||||||
|
Row(
|
||||||
|
modifier = Modifier.fillMaxWidth(),
|
||||||
|
horizontalArrangement = Arrangement.spacedBy(8.dp)
|
||||||
|
) {
|
||||||
|
OutlinedTextField(
|
||||||
|
value = state.stationBLat,
|
||||||
|
onValueChange = onStationBLat,
|
||||||
|
label = { Text("纬度") },
|
||||||
|
placeholder = { Text("34.0522") },
|
||||||
|
keyboardOptions = KeyboardOptions(keyboardType = KeyboardType.Decimal),
|
||||||
|
singleLine = true,
|
||||||
|
modifier = Modifier.weight(1f)
|
||||||
|
)
|
||||||
|
OutlinedTextField(
|
||||||
|
value = state.stationBLon,
|
||||||
|
onValueChange = onStationBLon,
|
||||||
|
label = { Text("经度") },
|
||||||
|
placeholder = { Text("-118.2437") },
|
||||||
|
keyboardOptions = KeyboardOptions(keyboardType = KeyboardType.Decimal),
|
||||||
|
singleLine = true,
|
||||||
|
modifier = Modifier.weight(1f)
|
||||||
|
)
|
||||||
|
}
|
||||||
|
OutlinedTextField(
|
||||||
|
value = state.stationBGrid,
|
||||||
|
onValueChange = onStationBGrid,
|
||||||
|
label = { Text("网格(4/6/8位,填此可省略经纬度)") },
|
||||||
|
placeholder = { Text("PM01tv") },
|
||||||
|
singleLine = true,
|
||||||
|
modifier = Modifier.fillMaxWidth()
|
||||||
|
)
|
||||||
|
Text(
|
||||||
|
text = "最小仰角:${state.stationBMinElev.toInt()}°",
|
||||||
|
style = MaterialTheme.typography.bodySmall
|
||||||
|
)
|
||||||
|
Slider(
|
||||||
|
value = state.stationBMinElev.toFloat(),
|
||||||
|
onValueChange = { onStationBMinElev(it.toDouble()) },
|
||||||
|
valueRange = 0f..90f,
|
||||||
|
steps = 17
|
||||||
|
)
|
||||||
|
|
||||||
|
HorizontalDivider()
|
||||||
|
|
||||||
|
Text(
|
||||||
|
text = "时间范围",
|
||||||
|
style = MaterialTheme.typography.titleSmall,
|
||||||
|
fontWeight = FontWeight.Medium
|
||||||
|
)
|
||||||
|
Row(
|
||||||
|
modifier = Modifier.fillMaxWidth(),
|
||||||
|
horizontalArrangement = Arrangement.spacedBy(8.dp)
|
||||||
|
) {
|
||||||
|
listOf(6, 12, 24, 48, 72).forEach { hours ->
|
||||||
|
FilterChip(
|
||||||
|
selected = state.hoursAhead == hours,
|
||||||
|
onClick = { onHoursAhead(hours) },
|
||||||
|
label = { Text("${hours}h") }
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
Button(
|
||||||
|
onClick = onQuery,
|
||||||
|
enabled = !state.isCalculating,
|
||||||
|
modifier = Modifier.fillMaxWidth()
|
||||||
|
) {
|
||||||
|
if (state.isCalculating) {
|
||||||
|
CircularProgressIndicator(
|
||||||
|
modifier = Modifier
|
||||||
|
.height(18.dp)
|
||||||
|
.width(18.dp),
|
||||||
|
strokeWidth = 2.dp,
|
||||||
|
color = MaterialTheme.colorScheme.onPrimary
|
||||||
|
)
|
||||||
|
Spacer(Modifier.width(8.dp))
|
||||||
|
}
|
||||||
|
Text(if (state.isCalculating) "计算中..." else "查询过境")
|
||||||
|
}
|
||||||
}
|
}
|
||||||
Text(if (state.isCalculating) "计算中..." else "查询过境")
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// Results
|
// Results
|
||||||
if (state.mutualPasses.isNotEmpty()) {
|
itemsIndexed(state.mutualPasses) { index, pass ->
|
||||||
item {
|
val mutualData = MutualPassData(
|
||||||
Text(
|
samples = pass.elevationSamples,
|
||||||
text = "找到 ${state.mutualPasses.size} 个共同过境",
|
startTime = pass.startTime,
|
||||||
style = MaterialTheme.typography.titleMedium,
|
endTime = pass.endTime,
|
||||||
modifier = Modifier.padding(top = 8.dp)
|
maxElev = maxOf(pass.maxElevationA, pass.maxElevationB, 10.0),
|
||||||
)
|
labelA = "你",
|
||||||
}
|
labelB = "友台"
|
||||||
itemsIndexed(state.mutualPasses) { index, pass ->
|
)
|
||||||
MutualPassCard(
|
MutualPassCard(
|
||||||
pass = pass,
|
pass = pass,
|
||||||
isExpanded = state.selectedPassIndex == index,
|
isExpanded = state.selectedPassIndex == index,
|
||||||
timeFormat = timeFormat,
|
timeFormat = timeFormat,
|
||||||
onToggle = { viewModel.onSelectPass(if (state.selectedPassIndex == index) -1 else index) }
|
onClick = { onSelectPass(if (state.selectedPassIndex == index) -1 else index) },
|
||||||
)
|
onNavigateToRadar = { onNavigateToRadar(pass.catNum, pass.startTime, mutualData) }
|
||||||
}
|
)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -238,39 +348,79 @@ private fun MutualPassCard(
|
|||||||
pass: MutualPass,
|
pass: MutualPass,
|
||||||
isExpanded: Boolean,
|
isExpanded: Boolean,
|
||||||
timeFormat: SimpleDateFormat,
|
timeFormat: SimpleDateFormat,
|
||||||
onToggle: () -> Unit
|
onClick: () -> Unit,
|
||||||
|
onNavigateToRadar: () -> Unit
|
||||||
) {
|
) {
|
||||||
Card(
|
ElevatedCard(
|
||||||
onClick = onToggle,
|
modifier = Modifier
|
||||||
modifier = Modifier.fillMaxWidth(),
|
.fillMaxWidth()
|
||||||
colors = CardDefaults.cardColors(
|
.clickable(onClick = onClick)
|
||||||
containerColor = MaterialTheme.colorScheme.surface
|
|
||||||
)
|
|
||||||
) {
|
) {
|
||||||
Column(modifier = Modifier.padding(12.dp)) {
|
Column(modifier = Modifier.padding(12.dp)) {
|
||||||
Text(
|
Row(
|
||||||
text = pass.name,
|
modifier = Modifier.fillMaxWidth(),
|
||||||
style = MaterialTheme.typography.bodyLarge,
|
horizontalArrangement = Arrangement.SpaceBetween,
|
||||||
color = MaterialTheme.colorScheme.primary
|
verticalAlignment = Alignment.CenterVertically
|
||||||
)
|
) {
|
||||||
Text(
|
Text(
|
||||||
text = "${timeFormat.format(Date(pass.startTime))} - ${timeFormat.format(Date(pass.endTime))}",
|
text = pass.name,
|
||||||
style = MaterialTheme.typography.bodySmall,
|
style = MaterialTheme.typography.bodyLarge,
|
||||||
color = MaterialTheme.colorScheme.onSurfaceVariant
|
fontWeight = FontWeight.Medium,
|
||||||
)
|
color = MaterialTheme.colorScheme.primary,
|
||||||
Text(
|
modifier = Modifier.weight(1f)
|
||||||
text = "你: ${pass.maxElevationA}° 友台: ${pass.maxElevationB}°",
|
|
||||||
style = MaterialTheme.typography.bodySmall
|
|
||||||
)
|
|
||||||
|
|
||||||
if (isExpanded && pass.elevationSamples.isNotEmpty()) {
|
|
||||||
Spacer(Modifier.height(8.dp))
|
|
||||||
ElevationCurveChart(
|
|
||||||
samples = pass.elevationSamples,
|
|
||||||
startTime = pass.startTime,
|
|
||||||
endTime = pass.endTime,
|
|
||||||
maxElev = maxOf(pass.maxElevationA, pass.maxElevationB, 10.0)
|
|
||||||
)
|
)
|
||||||
|
Text(
|
||||||
|
text = "${timeFormat.format(Date(pass.startTime))} - ${timeFormat.format(Date(pass.endTime))}",
|
||||||
|
style = MaterialTheme.typography.bodySmall,
|
||||||
|
color = MaterialTheme.colorScheme.onSurfaceVariant
|
||||||
|
)
|
||||||
|
}
|
||||||
|
Spacer(Modifier.height(4.dp))
|
||||||
|
Row(
|
||||||
|
modifier = Modifier.fillMaxWidth(),
|
||||||
|
horizontalArrangement = Arrangement.SpaceBetween
|
||||||
|
) {
|
||||||
|
Text(
|
||||||
|
text = "你: ${pass.maxElevationA}°",
|
||||||
|
style = MaterialTheme.typography.bodySmall,
|
||||||
|
color = MaterialTheme.colorScheme.primary
|
||||||
|
)
|
||||||
|
Text(
|
||||||
|
text = "友台: ${pass.maxElevationB}°",
|
||||||
|
style = MaterialTheme.typography.bodySmall,
|
||||||
|
color = MaterialTheme.colorScheme.tertiary
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
AnimatedVisibility(
|
||||||
|
visible = isExpanded,
|
||||||
|
enter = expandVertically(),
|
||||||
|
exit = shrinkVertically()
|
||||||
|
) {
|
||||||
|
Column {
|
||||||
|
HorizontalDivider(modifier = Modifier.padding(vertical = 8.dp))
|
||||||
|
if (pass.elevationSamples.isNotEmpty()) {
|
||||||
|
ElevationCurveChart(
|
||||||
|
samples = pass.elevationSamples,
|
||||||
|
startTime = pass.startTime,
|
||||||
|
endTime = pass.endTime,
|
||||||
|
maxElev = maxOf(pass.maxElevationA, pass.maxElevationB, 10.0)
|
||||||
|
)
|
||||||
|
}
|
||||||
|
Spacer(Modifier.height(8.dp))
|
||||||
|
OutlinedButton(
|
||||||
|
onClick = onNavigateToRadar,
|
||||||
|
modifier = Modifier.fillMaxWidth()
|
||||||
|
) {
|
||||||
|
Icon(
|
||||||
|
painter = painterResource(id = R.drawable.ic_radar),
|
||||||
|
contentDescription = null,
|
||||||
|
modifier = Modifier.height(16.dp).width(16.dp)
|
||||||
|
)
|
||||||
|
Spacer(Modifier.width(6.dp))
|
||||||
|
Text("查看雷达")
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
+17
-19
@@ -263,36 +263,34 @@ class MutualViewModel(
|
|||||||
minElevADeg: Double, minElevBDeg: Double,
|
minElevADeg: Double, minElevBDeg: Double,
|
||||||
approxTime: Long, step: Long, goingUp: Boolean
|
approxTime: Long, step: Long, goingUp: Boolean
|
||||||
): Long {
|
): Long {
|
||||||
var t = approxTime
|
|
||||||
val limit = 30_000L // search up to 30 seconds
|
|
||||||
if (goingUp) {
|
if (goingUp) {
|
||||||
// Find the point where both stations just go above minElev
|
// AOS: walk backward from approxTime to find the last sample where both are below,
|
||||||
var best = t
|
// then AOS is the next step after that.
|
||||||
t -= limit
|
var t = approxTime
|
||||||
while (t <= approxTime + limit) {
|
while (t > approxTime - 70_000L) {
|
||||||
val eA = elevationDeg(sat, posA, t)
|
val eA = elevationDeg(sat, posA, t)
|
||||||
val eB = elevationDeg(sat, posB, t)
|
val eB = elevationDeg(sat, posB, t)
|
||||||
if (eA > minElevADeg && eB > minElevBDeg) {
|
if (eA > minElevADeg && eB > minElevBDeg) {
|
||||||
best = t
|
t -= step
|
||||||
break
|
} else {
|
||||||
|
return t + step
|
||||||
}
|
}
|
||||||
t += step
|
|
||||||
}
|
}
|
||||||
return best
|
return approxTime - 70_000L + step
|
||||||
} else {
|
} else {
|
||||||
// Find the point where either station just drops below minElev
|
// LOS: walk forward from approxTime to find the first sample where either drops below,
|
||||||
var best = t
|
// then LOS is the step before that.
|
||||||
t -= limit
|
var t = approxTime
|
||||||
while (t <= approxTime + limit) {
|
while (t < approxTime + 70_000L) {
|
||||||
val eA = elevationDeg(sat, posA, t)
|
val eA = elevationDeg(sat, posA, t)
|
||||||
val eB = elevationDeg(sat, posB, t)
|
val eB = elevationDeg(sat, posB, t)
|
||||||
if (eA < minElevADeg || eB < minElevBDeg) {
|
if (eA > minElevADeg && eB > minElevBDeg) {
|
||||||
best = t
|
t += step
|
||||||
break
|
} else {
|
||||||
|
return t - step
|
||||||
}
|
}
|
||||||
t += step
|
|
||||||
}
|
}
|
||||||
return best
|
return approxTime + 70_000L - step
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -5,3 +5,7 @@ plugins {
|
|||||||
android {
|
android {
|
||||||
namespace = "com.rtbishop.look4sat.feature.radar"
|
namespace = "com.rtbishop.look4sat.feature.radar"
|
||||||
}
|
}
|
||||||
|
|
||||||
|
dependencies {
|
||||||
|
implementation(project(":feature:mutual"))
|
||||||
|
}
|
||||||
@@ -59,6 +59,7 @@ import androidx.lifecycle.compose.collectAsStateWithLifecycle
|
|||||||
import androidx.lifecycle.viewmodel.compose.viewModel
|
import androidx.lifecycle.viewmodel.compose.viewModel
|
||||||
import com.rtbishop.look4sat.core.domain.predict.OrbitalPos
|
import com.rtbishop.look4sat.core.domain.predict.OrbitalPos
|
||||||
import com.rtbishop.look4sat.core.domain.repository.IContainerProvider
|
import com.rtbishop.look4sat.core.domain.repository.IContainerProvider
|
||||||
|
import com.rtbishop.look4sat.core.domain.repository.MutualPassData
|
||||||
import com.rtbishop.look4sat.core.domain.utility.toDegrees
|
import com.rtbishop.look4sat.core.domain.utility.toDegrees
|
||||||
import com.rtbishop.look4sat.core.presentation.EmptyListCard
|
import com.rtbishop.look4sat.core.presentation.EmptyListCard
|
||||||
import com.rtbishop.look4sat.core.presentation.IconCard
|
import com.rtbishop.look4sat.core.presentation.IconCard
|
||||||
@@ -70,6 +71,7 @@ import com.rtbishop.look4sat.core.presentation.formatFrequency
|
|||||||
import com.rtbishop.look4sat.core.presentation.getDefaultPass
|
import com.rtbishop.look4sat.core.presentation.getDefaultPass
|
||||||
import com.rtbishop.look4sat.core.presentation.isVerticalLayout
|
import com.rtbishop.look4sat.core.presentation.isVerticalLayout
|
||||||
import com.rtbishop.look4sat.core.presentation.layoutPadding
|
import com.rtbishop.look4sat.core.presentation.layoutPadding
|
||||||
|
import com.rtbishop.look4sat.feature.mutual.ElevationCurveChart
|
||||||
import kotlinx.coroutines.launch
|
import kotlinx.coroutines.launch
|
||||||
|
|
||||||
private enum class RadarPage(val title: String) {
|
private enum class RadarPage(val title: String) {
|
||||||
@@ -83,6 +85,7 @@ fun RadarDestination(navigateUp: () -> Unit) {
|
|||||||
val container = (context.applicationContext as IContainerProvider).getMainContainer()
|
val container = (context.applicationContext as IContainerProvider).getMainContainer()
|
||||||
val viewModel: RadarViewModel = viewModel(factory = RadarViewModel.factory(container))
|
val viewModel: RadarViewModel = viewModel(factory = RadarViewModel.factory(container))
|
||||||
val uiState by viewModel.uiState.collectAsStateWithLifecycle()
|
val uiState by viewModel.uiState.collectAsStateWithLifecycle()
|
||||||
|
val mutualData by container.mutualPassData.collectAsStateWithLifecycle()
|
||||||
// Sync actual permission state on every recomposition so it survives screen re-entry
|
// Sync actual permission state on every recomposition so it survives screen re-entry
|
||||||
val hasPermission = ContextCompat.checkSelfPermission(
|
val hasPermission = ContextCompat.checkSelfPermission(
|
||||||
context, Manifest.permission.RECORD_AUDIO
|
context, Manifest.permission.RECORD_AUDIO
|
||||||
@@ -97,7 +100,7 @@ fun RadarDestination(navigateUp: () -> Unit) {
|
|||||||
viewModel.onAction(RadarAction.SstvPermissionResult(granted))
|
viewModel.onAction(RadarAction.SstvPermissionResult(granted))
|
||||||
viewModel.onAction(RadarAction.CwPermissionResult(granted))
|
viewModel.onAction(RadarAction.CwPermissionResult(granted))
|
||||||
}
|
}
|
||||||
RadarScreen(uiState, viewModel::onAction, navigateUp, requestMicPermission = {
|
RadarScreen(uiState, viewModel::onAction, navigateUp, mutualData, requestMicPermission = {
|
||||||
permissionLauncher.launch(Manifest.permission.RECORD_AUDIO)
|
permissionLauncher.launch(Manifest.permission.RECORD_AUDIO)
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
@@ -107,6 +110,7 @@ private fun RadarScreen(
|
|||||||
uiState: RadarState,
|
uiState: RadarState,
|
||||||
onAction: (RadarAction) -> Unit,
|
onAction: (RadarAction) -> Unit,
|
||||||
navigateUp: () -> Unit,
|
navigateUp: () -> Unit,
|
||||||
|
mutualData: MutualPassData,
|
||||||
requestMicPermission: () -> Unit
|
requestMicPermission: () -> Unit
|
||||||
) {
|
) {
|
||||||
val upcomingPass = uiState.currentPass ?: getDefaultPass()
|
val upcomingPass = uiState.currentPass ?: getDefaultPass()
|
||||||
@@ -137,10 +141,16 @@ private fun RadarScreen(
|
|||||||
}
|
}
|
||||||
if (isVertical) {
|
if (isVertical) {
|
||||||
RadarCard(uiState, Modifier.weight(1f))
|
RadarCard(uiState, Modifier.weight(1f))
|
||||||
|
if (mutualData.samples.isNotEmpty()) {
|
||||||
|
MutualElevationCard(mutualData, Modifier.weight(1f))
|
||||||
|
}
|
||||||
PagerCard(uiState, onAction, requestMicPermission, Modifier.weight(1f))
|
PagerCard(uiState, onAction, requestMicPermission, Modifier.weight(1f))
|
||||||
} else {
|
} else {
|
||||||
Row(horizontalArrangement = Arrangement.spacedBy(6.dp)) {
|
Row(horizontalArrangement = Arrangement.spacedBy(6.dp)) {
|
||||||
RadarCard(uiState, Modifier.weight(1f))
|
RadarCard(uiState, Modifier.weight(1f))
|
||||||
|
if (mutualData.samples.isNotEmpty()) {
|
||||||
|
MutualElevationCard(mutualData, Modifier.weight(1f))
|
||||||
|
}
|
||||||
PagerCard(uiState, onAction, requestMicPermission, Modifier.weight(1f))
|
PagerCard(uiState, onAction, requestMicPermission, Modifier.weight(1f))
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -304,3 +314,19 @@ private fun RadarLabel(
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@Composable
|
||||||
|
private fun MutualElevationCard(mutualData: MutualPassData, modifier: Modifier = Modifier) {
|
||||||
|
ElevatedCard(modifier = modifier) {
|
||||||
|
Column(modifier = Modifier.fillMaxSize().padding(8.dp)) {
|
||||||
|
if (mutualData.samples.isNotEmpty()) {
|
||||||
|
ElevationCurveChart(
|
||||||
|
samples = mutualData.samples,
|
||||||
|
startTime = mutualData.startTime,
|
||||||
|
endTime = mutualData.endTime,
|
||||||
|
maxElev = mutualData.maxElev
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -63,7 +63,13 @@ data class RadarState(
|
|||||||
val transceivers: TransceiverSubState = TransceiverSubState(),
|
val transceivers: TransceiverSubState = TransceiverSubState(),
|
||||||
val radioControl: RadioControlSubState = RadioControlSubState(),
|
val radioControl: RadioControlSubState = RadioControlSubState(),
|
||||||
val sstv: SstvSubState = SstvSubState(),
|
val sstv: SstvSubState = SstvSubState(),
|
||||||
val cw: CwSubState = CwSubState()
|
val cw: CwSubState = CwSubState(),
|
||||||
|
val mutualSamples: List<Pair<Long, Pair<Double, Double>>> = emptyList(),
|
||||||
|
val mutualStartTime: Long = 0L,
|
||||||
|
val mutualEndTime: Long = 0L,
|
||||||
|
val mutualMaxElev: Double = 10.0,
|
||||||
|
val mutualLabelA: String = "你",
|
||||||
|
val mutualLabelB: String = "友台"
|
||||||
)
|
)
|
||||||
|
|
||||||
enum class SstvStatus { Idle, Recording }
|
enum class SstvStatus { Idle, Recording }
|
||||||
|
|||||||
Reference in new issue
Block a user