Compare commits
1
Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
1813e1bfa4 |
No files matched your search
@@ -72,6 +72,7 @@ import androidx.navigation3.runtime.rememberNavBackStack
|
|||||||
import androidx.navigation3.runtime.rememberSaveableStateHolderNavEntryDecorator
|
import androidx.navigation3.runtime.rememberSaveableStateHolderNavEntryDecorator
|
||||||
import androidx.navigation3.ui.NavDisplay
|
import androidx.navigation3.ui.NavDisplay
|
||||||
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.presentation.DeeplinkResolver
|
import com.rtbishop.look4sat.core.presentation.DeeplinkResolver
|
||||||
import com.rtbishop.look4sat.core.presentation.ElevationThresholds
|
import com.rtbishop.look4sat.core.presentation.ElevationThresholds
|
||||||
import com.rtbishop.look4sat.core.presentation.LocalElevationThresholds
|
import com.rtbishop.look4sat.core.presentation.LocalElevationThresholds
|
||||||
@@ -200,6 +201,7 @@ fun MainScreen(navigateToRadar: () -> Unit = {}) {
|
|||||||
}
|
}
|
||||||
entry<Screen.Passes> {
|
entry<Screen.Passes> {
|
||||||
PassesDestination { catNum, aosTime ->
|
PassesDestination { catNum, aosTime ->
|
||||||
|
container.setMutualPassData(MutualPassData())
|
||||||
container.satelliteRepo.selectPass(catNum, aosTime)
|
container.satelliteRepo.selectPass(catNum, aosTime)
|
||||||
backStack.add(Screen.Radar)
|
backStack.add(Screen.Radar)
|
||||||
// navigateToRadar()
|
// navigateToRadar()
|
||||||
@@ -216,7 +218,7 @@ fun MainScreen(navigateToRadar: () -> Unit = {}) {
|
|||||||
viewModel = mutualViewModel,
|
viewModel = mutualViewModel,
|
||||||
navigateUp = navigateBack,
|
navigateUp = navigateBack,
|
||||||
navigateToRadar = { catNum, aosTime, pass ->
|
navigateToRadar = { catNum, aosTime, pass ->
|
||||||
pass?.let { container.setMutualPassData(it) }
|
container.setMutualPassData(pass ?: MutualPassData())
|
||||||
container.satelliteRepo.selectPass(catNum, aosTime)
|
container.satelliteRepo.selectPass(catNum, aosTime)
|
||||||
backStack.add(Screen.Radar)
|
backStack.add(Screen.Radar)
|
||||||
}
|
}
|
||||||
@@ -245,6 +247,7 @@ fun MainScreen(navigateToRadar: () -> Unit = {}) {
|
|||||||
.clickable {
|
.clickable {
|
||||||
val pass = trackingState.currentPass
|
val pass = trackingState.currentPass
|
||||||
if (pass != null) {
|
if (pass != null) {
|
||||||
|
container.setMutualPassData(MutualPassData())
|
||||||
container.satelliteRepo.selectPass(pass.catNum, pass.aosTime)
|
container.satelliteRepo.selectPass(pass.catNum, pass.aosTime)
|
||||||
backStack.add(Screen.Radar)
|
backStack.add(Screen.Radar)
|
||||||
}
|
}
|
||||||
|
|||||||
+1
-1
@@ -36,7 +36,7 @@ sealed class Screen(val iconResId: Int, val titleResId: Int) : NavKey {
|
|||||||
data object Map : Screen(R.drawable.ic_map, R.string.nav_map)
|
data object Map : Screen(R.drawable.ic_map, R.string.nav_map)
|
||||||
|
|
||||||
@Serializable
|
@Serializable
|
||||||
data object Mutual : Screen(R.drawable.ic_radar, R.string.nav_mutual)
|
data object Mutual : Screen(R.drawable.ic_match, R.string.nav_mutual)
|
||||||
|
|
||||||
@Serializable
|
@Serializable
|
||||||
data object Settings : Screen(R.drawable.ic_settings, R.string.nav_prefs)
|
data object Settings : Screen(R.drawable.ic_settings, R.string.nav_prefs)
|
||||||
|
|||||||
@@ -0,0 +1,22 @@
|
|||||||
|
<vector xmlns:android="http://schemas.android.com/apk/res/android"
|
||||||
|
android:width="24dp"
|
||||||
|
android:height="24dp"
|
||||||
|
android:viewportWidth="96"
|
||||||
|
android:viewportHeight="96">
|
||||||
|
|
||||||
|
<path
|
||||||
|
android:fillColor="@android:color/transparent"
|
||||||
|
android:strokeColor="@android:color/white"
|
||||||
|
android:strokeWidth="6"
|
||||||
|
android:strokeLineCap="round"
|
||||||
|
android:strokeLineJoin="round"
|
||||||
|
android:pathData="M 8 76 C 8 44, 21 24, 39 24 C 57 24, 70 44, 70 76" />
|
||||||
|
|
||||||
|
<path
|
||||||
|
android:fillColor="@android:color/transparent"
|
||||||
|
android:strokeColor="@android:color/white"
|
||||||
|
android:strokeWidth="6"
|
||||||
|
android:strokeLineCap="round"
|
||||||
|
android:strokeLineJoin="round"
|
||||||
|
android:pathData="M 30 76 C 30 54, 44 42, 62 42 C 80 42, 94 54, 94 76" />
|
||||||
|
</vector>
|
||||||
@@ -9,7 +9,7 @@
|
|||||||
<string name="nav_sat">卫星</string>
|
<string name="nav_sat">卫星</string>
|
||||||
<string name="nav_pass">过境</string>
|
<string name="nav_pass">过境</string>
|
||||||
<string name="nav_radar">雷达</string>
|
<string name="nav_radar">雷达</string>
|
||||||
<string name="nav_mutual">对台</string>
|
<string name="nav_mutual">匹配</string>
|
||||||
<string name="nav_map">地图</string>
|
<string name="nav_map">地图</string>
|
||||||
<string name="nav_prefs">设置</string>
|
<string name="nav_prefs">设置</string>
|
||||||
|
|
||||||
|
|||||||
@@ -10,7 +10,7 @@
|
|||||||
<string name="nav_sat">Satellites</string>
|
<string name="nav_sat">Satellites</string>
|
||||||
<string name="nav_pass">Passes</string>
|
<string name="nav_pass">Passes</string>
|
||||||
<string name="nav_radar">Radar</string>
|
<string name="nav_radar">Radar</string>
|
||||||
<string name="nav_mutual">Mutual</string>
|
<string name="nav_mutual">Match</string>
|
||||||
<string name="nav_map">Map</string>
|
<string name="nav_map">Map</string>
|
||||||
<string name="nav_prefs">Settings</string>
|
<string name="nav_prefs">Settings</string>
|
||||||
|
|
||||||
|
|||||||
+241
-141
@@ -88,7 +88,7 @@ fun MutualScreen(
|
|||||||
},
|
},
|
||||||
topInfo = {
|
topInfo = {
|
||||||
Text(
|
Text(
|
||||||
text = "对台过境",
|
text = "过境匹配",
|
||||||
style = MaterialTheme.typography.titleMedium,
|
style = MaterialTheme.typography.titleMedium,
|
||||||
fontWeight = FontWeight.Bold
|
fontWeight = FontWeight.Bold
|
||||||
)
|
)
|
||||||
@@ -96,7 +96,7 @@ fun MutualScreen(
|
|||||||
bottomInfo = {
|
bottomInfo = {
|
||||||
if (state.mutualPasses.isNotEmpty()) {
|
if (state.mutualPasses.isNotEmpty()) {
|
||||||
Text(
|
Text(
|
||||||
text = "找到 ${state.mutualPasses.size} 个过境",
|
text = "找到 ${state.mutualPasses.size} 个匹配",
|
||||||
style = MaterialTheme.typography.bodySmall,
|
style = MaterialTheme.typography.bodySmall,
|
||||||
color = MaterialTheme.colorScheme.onSurfaceVariant
|
color = MaterialTheme.colorScheme.onSurfaceVariant
|
||||||
)
|
)
|
||||||
@@ -183,153 +183,91 @@ private fun MutualContent(
|
|||||||
|
|
||||||
// Input form
|
// Input form
|
||||||
item {
|
item {
|
||||||
ElevatedCard(modifier = Modifier.fillMaxWidth()) {
|
Column(
|
||||||
Column(
|
modifier = Modifier.fillMaxWidth(),
|
||||||
modifier = Modifier
|
verticalArrangement = Arrangement.spacedBy(6.dp)
|
||||||
.fillMaxWidth()
|
) {
|
||||||
.padding(12.dp),
|
MatchIntroCard()
|
||||||
verticalArrangement = Arrangement.spacedBy(8.dp)
|
|
||||||
) {
|
if (isVertical) {
|
||||||
Text(
|
StationInputCard(
|
||||||
text = "你的位置",
|
title = "你的位置",
|
||||||
style = MaterialTheme.typography.titleSmall,
|
titleColor = MaterialTheme.colorScheme.primary,
|
||||||
fontWeight = FontWeight.Medium
|
lat = state.stationALat,
|
||||||
|
lon = state.stationALon,
|
||||||
|
grid = state.stationAGrid,
|
||||||
|
minElev = state.stationAMinElev,
|
||||||
|
latPlaceholder = "39.9042",
|
||||||
|
lonPlaceholder = "116.4074",
|
||||||
|
gridPlaceholder = "ON79uj",
|
||||||
|
onLatChange = onStationALat,
|
||||||
|
onLonChange = onStationALon,
|
||||||
|
onGridChange = onStationAGrid,
|
||||||
|
onMinElevChange = onStationAMinElev,
|
||||||
|
currentPositionAction = onUseCurrentPosition
|
||||||
)
|
)
|
||||||
|
StationInputCard(
|
||||||
|
title = "友台位置",
|
||||||
|
titleColor = MaterialTheme.colorScheme.tertiary,
|
||||||
|
lat = state.stationBLat,
|
||||||
|
lon = state.stationBLon,
|
||||||
|
grid = state.stationBGrid,
|
||||||
|
minElev = state.stationBMinElev,
|
||||||
|
latPlaceholder = "34.0522",
|
||||||
|
lonPlaceholder = "-118.2437",
|
||||||
|
gridPlaceholder = "PM01tv",
|
||||||
|
onLatChange = onStationBLat,
|
||||||
|
onLonChange = onStationBLon,
|
||||||
|
onGridChange = onStationBGrid,
|
||||||
|
onMinElevChange = onStationBMinElev
|
||||||
|
)
|
||||||
|
} else {
|
||||||
Row(
|
Row(
|
||||||
modifier = Modifier.fillMaxWidth(),
|
modifier = Modifier.fillMaxWidth(),
|
||||||
horizontalArrangement = Arrangement.spacedBy(8.dp)
|
horizontalArrangement = Arrangement.spacedBy(6.dp)
|
||||||
) {
|
) {
|
||||||
OutlinedTextField(
|
StationInputCard(
|
||||||
value = state.stationALat,
|
title = "你的位置",
|
||||||
onValueChange = onStationALat,
|
titleColor = MaterialTheme.colorScheme.primary,
|
||||||
label = { Text("纬度") },
|
lat = state.stationALat,
|
||||||
placeholder = { Text("39.9042") },
|
lon = state.stationALon,
|
||||||
keyboardOptions = KeyboardOptions(keyboardType = KeyboardType.Decimal),
|
grid = state.stationAGrid,
|
||||||
singleLine = true,
|
minElev = state.stationAMinElev,
|
||||||
|
latPlaceholder = "39.9042",
|
||||||
|
lonPlaceholder = "116.4074",
|
||||||
|
gridPlaceholder = "ON79uj",
|
||||||
|
onLatChange = onStationALat,
|
||||||
|
onLonChange = onStationALon,
|
||||||
|
onGridChange = onStationAGrid,
|
||||||
|
onMinElevChange = onStationAMinElev,
|
||||||
|
currentPositionAction = onUseCurrentPosition,
|
||||||
modifier = Modifier.weight(1f)
|
modifier = Modifier.weight(1f)
|
||||||
)
|
)
|
||||||
OutlinedTextField(
|
StationInputCard(
|
||||||
value = state.stationALon,
|
title = "友台位置",
|
||||||
onValueChange = onStationALon,
|
titleColor = MaterialTheme.colorScheme.tertiary,
|
||||||
label = { Text("经度") },
|
lat = state.stationBLat,
|
||||||
placeholder = { Text("116.4074") },
|
lon = state.stationBLon,
|
||||||
keyboardOptions = KeyboardOptions(keyboardType = KeyboardType.Decimal),
|
grid = state.stationBGrid,
|
||||||
singleLine = true,
|
minElev = state.stationBMinElev,
|
||||||
|
latPlaceholder = "34.0522",
|
||||||
|
lonPlaceholder = "-118.2437",
|
||||||
|
gridPlaceholder = "PM01tv",
|
||||||
|
onLatChange = onStationBLat,
|
||||||
|
onLonChange = onStationBLon,
|
||||||
|
onGridChange = onStationBGrid,
|
||||||
|
onMinElevChange = onStationBMinElev,
|
||||||
modifier = Modifier.weight(1f)
|
modifier = Modifier.weight(1f)
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
OutlinedTextField(
|
|
||||||
value = state.stationAGrid,
|
|
||||||
onValueChange = onStationAGrid,
|
|
||||||
label = { Text("网格(4/6/8位,填此可省略经纬度)") },
|
|
||||||
placeholder = { Text("ON79uj") },
|
|
||||||
singleLine = true,
|
|
||||||
modifier = Modifier.fillMaxWidth()
|
|
||||||
)
|
|
||||||
OutlinedButton(
|
|
||||||
onClick = onUseCurrentPosition,
|
|
||||||
modifier = Modifier.align(Alignment.End)
|
|
||||||
) {
|
|
||||||
Text("当前精确位置", style = MaterialTheme.typography.bodySmall)
|
|
||||||
}
|
|
||||||
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 "查询过境")
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
MatchSearchCard(
|
||||||
|
hoursAhead = state.hoursAhead,
|
||||||
|
isCalculating = state.isCalculating,
|
||||||
|
onHoursAhead = onHoursAhead,
|
||||||
|
onQuery = onQuery
|
||||||
|
)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -365,6 +303,169 @@ private fun MutualContent(
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@Composable
|
||||||
|
private fun MatchIntroCard(modifier: Modifier = Modifier) {
|
||||||
|
ElevatedCard(modifier = modifier.fillMaxWidth()) {
|
||||||
|
Column(
|
||||||
|
modifier = Modifier.padding(12.dp),
|
||||||
|
verticalArrangement = Arrangement.spacedBy(4.dp)
|
||||||
|
) {
|
||||||
|
Text(
|
||||||
|
text = "位置匹配",
|
||||||
|
style = MaterialTheme.typography.titleSmall,
|
||||||
|
fontWeight = FontWeight.Medium,
|
||||||
|
color = MaterialTheme.colorScheme.primary
|
||||||
|
)
|
||||||
|
Text(
|
||||||
|
text = "输入双方位置,查找共同可见的卫星过境窗口",
|
||||||
|
style = MaterialTheme.typography.bodySmall,
|
||||||
|
color = MaterialTheme.colorScheme.onSurfaceVariant
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@Composable
|
||||||
|
private fun StationInputCard(
|
||||||
|
title: String,
|
||||||
|
titleColor: androidx.compose.ui.graphics.Color,
|
||||||
|
lat: String,
|
||||||
|
lon: String,
|
||||||
|
grid: String,
|
||||||
|
minElev: Double,
|
||||||
|
latPlaceholder: String,
|
||||||
|
lonPlaceholder: String,
|
||||||
|
gridPlaceholder: String,
|
||||||
|
onLatChange: (String) -> Unit,
|
||||||
|
onLonChange: (String) -> Unit,
|
||||||
|
onGridChange: (String) -> Unit,
|
||||||
|
onMinElevChange: (Double) -> Unit,
|
||||||
|
modifier: Modifier = Modifier,
|
||||||
|
currentPositionAction: (() -> Unit)? = null
|
||||||
|
) {
|
||||||
|
ElevatedCard(modifier = modifier.fillMaxWidth()) {
|
||||||
|
Column(
|
||||||
|
modifier = Modifier.padding(12.dp),
|
||||||
|
verticalArrangement = Arrangement.spacedBy(8.dp)
|
||||||
|
) {
|
||||||
|
Row(
|
||||||
|
modifier = Modifier.fillMaxWidth(),
|
||||||
|
horizontalArrangement = Arrangement.SpaceBetween,
|
||||||
|
verticalAlignment = Alignment.CenterVertically
|
||||||
|
) {
|
||||||
|
Text(
|
||||||
|
text = title,
|
||||||
|
style = MaterialTheme.typography.titleSmall,
|
||||||
|
fontWeight = FontWeight.Medium,
|
||||||
|
color = titleColor
|
||||||
|
)
|
||||||
|
if (currentPositionAction != null) {
|
||||||
|
OutlinedButton(onClick = currentPositionAction) {
|
||||||
|
Text("填入当前位置", style = MaterialTheme.typography.bodySmall)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
Row(
|
||||||
|
modifier = Modifier.fillMaxWidth(),
|
||||||
|
horizontalArrangement = Arrangement.spacedBy(8.dp)
|
||||||
|
) {
|
||||||
|
OutlinedTextField(
|
||||||
|
value = lat,
|
||||||
|
onValueChange = onLatChange,
|
||||||
|
label = { Text("纬度") },
|
||||||
|
placeholder = { Text(latPlaceholder) },
|
||||||
|
keyboardOptions = KeyboardOptions(keyboardType = KeyboardType.Decimal),
|
||||||
|
singleLine = true,
|
||||||
|
modifier = Modifier.weight(1f)
|
||||||
|
)
|
||||||
|
OutlinedTextField(
|
||||||
|
value = lon,
|
||||||
|
onValueChange = onLonChange,
|
||||||
|
label = { Text("经度") },
|
||||||
|
placeholder = { Text(lonPlaceholder) },
|
||||||
|
keyboardOptions = KeyboardOptions(keyboardType = KeyboardType.Decimal),
|
||||||
|
singleLine = true,
|
||||||
|
modifier = Modifier.weight(1f)
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
OutlinedTextField(
|
||||||
|
value = grid,
|
||||||
|
onValueChange = onGridChange,
|
||||||
|
label = { Text("网格") },
|
||||||
|
placeholder = { Text(gridPlaceholder) },
|
||||||
|
supportingText = { Text("支持 4/6/8 位 Maidenhead 网格;也可直接输入经纬度") },
|
||||||
|
singleLine = true,
|
||||||
|
modifier = Modifier.fillMaxWidth()
|
||||||
|
)
|
||||||
|
|
||||||
|
Text(
|
||||||
|
text = "最小仰角:${minElev.toInt()}°",
|
||||||
|
style = MaterialTheme.typography.bodySmall,
|
||||||
|
color = MaterialTheme.colorScheme.onSurfaceVariant
|
||||||
|
)
|
||||||
|
Slider(
|
||||||
|
value = minElev.toFloat(),
|
||||||
|
onValueChange = { onMinElevChange(it.toDouble()) },
|
||||||
|
valueRange = 0f..90f,
|
||||||
|
steps = 17
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@Composable
|
||||||
|
private fun MatchSearchCard(
|
||||||
|
hoursAhead: Int,
|
||||||
|
isCalculating: Boolean,
|
||||||
|
onHoursAhead: (Int) -> Unit,
|
||||||
|
onQuery: () -> Unit,
|
||||||
|
modifier: Modifier = Modifier
|
||||||
|
) {
|
||||||
|
ElevatedCard(modifier = modifier.fillMaxWidth()) {
|
||||||
|
Column(
|
||||||
|
modifier = Modifier.padding(12.dp),
|
||||||
|
verticalArrangement = Arrangement.spacedBy(10.dp)
|
||||||
|
) {
|
||||||
|
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 = hoursAhead == hours,
|
||||||
|
onClick = { onHoursAhead(hours) },
|
||||||
|
label = { Text("${hours}h") }
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
Button(
|
||||||
|
onClick = onQuery,
|
||||||
|
enabled = !isCalculating,
|
||||||
|
modifier = Modifier.fillMaxWidth()
|
||||||
|
) {
|
||||||
|
if (isCalculating) {
|
||||||
|
CircularProgressIndicator(
|
||||||
|
modifier = Modifier
|
||||||
|
.height(18.dp)
|
||||||
|
.width(18.dp),
|
||||||
|
strokeWidth = 2.dp,
|
||||||
|
color = MaterialTheme.colorScheme.onPrimary
|
||||||
|
)
|
||||||
|
Spacer(Modifier.width(8.dp))
|
||||||
|
}
|
||||||
|
Text(if (isCalculating) "计算中..." else "查询匹配")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
@Composable
|
@Composable
|
||||||
private fun MutualPassCard(
|
private fun MutualPassCard(
|
||||||
pass: MutualPass,
|
pass: MutualPass,
|
||||||
@@ -380,7 +481,6 @@ private fun MutualPassCard(
|
|||||||
var dragProgress by remember(pass) { mutableFloatStateOf(0.5f) }
|
var dragProgress by remember(pass) { mutableFloatStateOf(0.5f) }
|
||||||
|
|
||||||
// Filter to only show the portion where both stations are above their minElev
|
// Filter to only show the portion where both stations are above their minElev
|
||||||
// (satlover.de style — only the usable common window)
|
|
||||||
val visibleSamples = remember(pass, minElevA, minElevB) {
|
val visibleSamples = remember(pass, minElevA, minElevB) {
|
||||||
pass.elevationSamples.filter { (_, elev) ->
|
pass.elevationSamples.filter { (_, elev) ->
|
||||||
elev.first >= minElevA && elev.second >= minElevB
|
elev.first >= minElevA && elev.second >= minElevB
|
||||||
@@ -447,7 +547,7 @@ private fun MutualPassCard(
|
|||||||
// Dual-station radar track (polar plot)
|
// Dual-station radar track (polar plot)
|
||||||
if (visibleTracks.isNotEmpty()) {
|
if (visibleTracks.isNotEmpty()) {
|
||||||
Text(
|
Text(
|
||||||
text = "双方轨迹(雷达图)",
|
text = "双方轨迹",
|
||||||
style = MaterialTheme.typography.titleSmall,
|
style = MaterialTheme.typography.titleSmall,
|
||||||
fontWeight = FontWeight.Medium,
|
fontWeight = FontWeight.Medium,
|
||||||
color = MaterialTheme.colorScheme.onSurfaceVariant
|
color = MaterialTheme.colorScheme.onSurfaceVariant
|
||||||
|
|||||||
+20
-15
@@ -352,20 +352,23 @@ class MutualViewModel(
|
|||||||
tSample += sampleInterval
|
tSample += sampleInterval
|
||||||
}
|
}
|
||||||
|
|
||||||
// The pass list already filters by maxElev > minElevation, so we trust
|
// Search uses the 0° horizon as the pass boundary, but the user sliders are
|
||||||
// the pass is valid. The mutual pass just needs both stations' curves.
|
// a final visibility filter. Both stations must exceed their own threshold;
|
||||||
results.add(
|
// otherwise the result would expand to an empty filtered chart.
|
||||||
MutualPass(
|
if (maxElevA > minElevADeg && maxElevB > minElevBDeg) {
|
||||||
catNum = pass.catNum,
|
results.add(
|
||||||
name = pass.orbitalObject.data.name,
|
MutualPass(
|
||||||
startTime = refinedAos,
|
catNum = pass.catNum,
|
||||||
endTime = refinedLos,
|
name = pass.orbitalObject.data.name,
|
||||||
maxElevationA = (maxElevA * 10).roundToInt() / 10.0,
|
startTime = refinedAos,
|
||||||
maxElevationB = (maxElevB * 10).roundToInt() / 10.0,
|
endTime = refinedLos,
|
||||||
elevationSamples = samples,
|
maxElevationA = (maxElevA * 10).roundToInt() / 10.0,
|
||||||
trackSamples = tracks
|
maxElevationB = (maxElevB * 10).roundToInt() / 10.0,
|
||||||
|
elevationSamples = samples,
|
||||||
|
trackSamples = tracks
|
||||||
|
)
|
||||||
)
|
)
|
||||||
)
|
}
|
||||||
}
|
}
|
||||||
return results
|
return results
|
||||||
}
|
}
|
||||||
@@ -413,10 +416,12 @@ class MutualViewModel(
|
|||||||
|
|
||||||
var tSample = refinedAos
|
var tSample = refinedAos
|
||||||
while (tSample <= refinedLos) {
|
while (tSample <= refinedLos) {
|
||||||
|
// Use getElevation for elevation, matching the main pass predictor; use
|
||||||
|
// getFullPosition only for azimuth needed by the radar plot.
|
||||||
|
val elevA = elevationDeg(sat, posA, tSample)
|
||||||
|
val elevB = elevationDeg(sat, posB, tSample)
|
||||||
val fullA = sat.getFullPosition(posA, tSample)
|
val fullA = sat.getFullPosition(posA, tSample)
|
||||||
val fullB = sat.getFullPosition(posB, tSample)
|
val fullB = sat.getFullPosition(posB, tSample)
|
||||||
val elevA = fullA.elevation * 180.0 / PI
|
|
||||||
val elevB = fullB.elevation * 180.0 / PI
|
|
||||||
if (elevA > maxElevA) maxElevA = elevA
|
if (elevA > maxElevA) maxElevA = elevA
|
||||||
if (elevB > maxElevB) maxElevB = elevB
|
if (elevB > maxElevB) maxElevB = elevB
|
||||||
samples.add(tSample to (elevA to elevB))
|
samples.add(tSample to (elevA to elevB))
|
||||||
|
|||||||
@@ -73,6 +73,7 @@ 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 kotlinx.coroutines.launch
|
import kotlinx.coroutines.launch
|
||||||
|
import kotlinx.coroutines.delay
|
||||||
import kotlin.math.PI
|
import kotlin.math.PI
|
||||||
|
|
||||||
private enum class RadarPage(val title: String) {
|
private enum class RadarPage(val title: String) {
|
||||||
@@ -87,6 +88,23 @@ fun RadarDestination(navigateUp: () -> Unit) {
|
|||||||
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()
|
val mutualData by container.mutualPassData.collectAsStateWithLifecycle()
|
||||||
|
val navigateUpAndClearMutual = {
|
||||||
|
if (container.mutualPassData.value.endTime > 0L) {
|
||||||
|
container.setMutualPassData(MutualPassData())
|
||||||
|
}
|
||||||
|
navigateUp()
|
||||||
|
}
|
||||||
|
LaunchedEffect(mutualData.endTime) {
|
||||||
|
if (mutualData.endTime <= 0L) return@LaunchedEffect
|
||||||
|
while (true) {
|
||||||
|
val remainingMs = mutualData.endTime - System.currentTimeMillis()
|
||||||
|
if (remainingMs <= 0L) {
|
||||||
|
navigateUpAndClearMutual()
|
||||||
|
return@LaunchedEffect
|
||||||
|
}
|
||||||
|
delay(remainingMs.coerceAtMost(1000L))
|
||||||
|
}
|
||||||
|
}
|
||||||
// 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
|
||||||
@@ -101,7 +119,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, mutualData, requestMicPermission = {
|
RadarScreen(uiState, viewModel::onAction, navigateUpAndClearMutual, mutualData, requestMicPermission = {
|
||||||
permissionLauncher.launch(Manifest.permission.RECORD_AUDIO)
|
permissionLauncher.launch(Manifest.permission.RECORD_AUDIO)
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -139,6 +139,8 @@ class RadarViewModel(
|
|||||||
val passes = satelliteRepo.passes.value
|
val passes = satelliteRepo.passes.value
|
||||||
val (catNum, aosTime) = satelliteRepo.selectedPass.value
|
val (catNum, aosTime) = satelliteRepo.selectedPass.value
|
||||||
return passes.find { it.catNum == catNum && it.aosTime == aosTime }
|
return passes.find { it.catNum == catNum && it.aosTime == aosTime }
|
||||||
|
?: passes.find { it.catNum == catNum && aosTime in it.aosTime..it.losTime }
|
||||||
|
?: passes.find { it.catNum == catNum }
|
||||||
?: passes.firstOrNull()
|
?: passes.firstOrNull()
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
Reference in new issue
Block a user