mirror of
https://github.com/atsunatsu/Look4Sat.git
synced 2026-10-05 03:57:24 +00:00
Compare commits
11
Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
a08a28df39 | ||
|
|
759a6de2c6 | ||
|
|
ba5a984c16 | ||
|
|
083b8a2847 | ||
|
|
e8ebfb1ef3 | ||
|
|
12eed1d9a9 | ||
|
|
b3859fbece | ||
|
|
f40a67e1b7 | ||
|
|
16dce5df2a | ||
|
|
80a58bed61 | ||
|
|
a8afc63bba |
No files matched your search
@@ -18,6 +18,7 @@ out/
|
|||||||
# Gradle files
|
# Gradle files
|
||||||
.gradle/
|
.gradle/
|
||||||
build/
|
build/
|
||||||
|
.cxx/
|
||||||
|
|
||||||
# Local configuration file (sdk path, etc)
|
# Local configuration file (sdk path, etc)
|
||||||
local.properties
|
local.properties
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
# Look4Sat-BA7OPF
|
# Look4Sat-BA7OPF
|
||||||
|
|
||||||
[](https://github.com/atsunatsu/Look4Sat/actions/workflows/release.yml)
|
[](https://github.com/atsunatsu/Look4Sat/releases)
|
||||||
|
|
||||||
**BA7OPF 定制版** — 基于 [rt-bishop/Look4Sat](https://github.com/rt-bishop/Look4Sat) 的业余无线电卫星追踪器,增加了线性卫星频率计算器等功能。
|
**BA7OPF 定制版** — 基于 [rt-bishop/Look4Sat](https://github.com/rt-bishop/Look4Sat) 的业余无线电卫星追踪器,增加了线性卫星频率计算器等功能。
|
||||||
|
|
||||||
|
|||||||
+18
-13
@@ -34,6 +34,7 @@ import kotlinx.coroutines.coroutineScope
|
|||||||
import kotlinx.coroutines.delay
|
import kotlinx.coroutines.delay
|
||||||
import kotlinx.coroutines.flow.MutableStateFlow
|
import kotlinx.coroutines.flow.MutableStateFlow
|
||||||
import kotlinx.coroutines.flow.StateFlow
|
import kotlinx.coroutines.flow.StateFlow
|
||||||
|
import kotlinx.coroutines.flow.combine
|
||||||
import kotlinx.coroutines.flow.update
|
import kotlinx.coroutines.flow.update
|
||||||
import kotlinx.coroutines.withContext
|
import kotlinx.coroutines.withContext
|
||||||
import java.util.TimeZone
|
import java.util.TimeZone
|
||||||
@@ -63,19 +64,23 @@ class SatelliteRepo(
|
|||||||
override suspend fun getRadiosWithId(id: Int) = localStorage.getRadiosWithId(id)
|
override suspend fun getRadiosWithId(id: Int) = localStorage.getRadiosWithId(id)
|
||||||
|
|
||||||
override suspend fun initRepository() = withContext(dispatcher) {
|
override suspend fun initRepository() = withContext(dispatcher) {
|
||||||
settingsRepo.selectedIds.collect { selectedIds ->
|
combine(
|
||||||
_satellites.update { localStorage.getEntriesWithIds(selectedIds) }
|
settingsRepo.selectedIds,
|
||||||
val settings = settingsRepo.passesSettings.value
|
settingsRepo.stationPosition
|
||||||
calculatePasses(
|
) { selectedIds, _ -> selectedIds }
|
||||||
time = System.currentTimeMillis(),
|
.collect { selectedIds ->
|
||||||
hoursAhead = settings.hoursAhead,
|
_satellites.update { localStorage.getEntriesWithIds(selectedIds) }
|
||||||
minElevation = settings.minElevation,
|
val settings = settingsRepo.passesSettings.value
|
||||||
aosStartMinute = settings.aosStartMinute,
|
calculatePasses(
|
||||||
aosEndMinute = settings.aosEndMinute,
|
time = System.currentTimeMillis(),
|
||||||
invertAosTimeWindow = settings.invertAosTimeWindow,
|
hoursAhead = settings.hoursAhead,
|
||||||
modes = settings.selectedModes
|
minElevation = settings.minElevation,
|
||||||
)
|
aosStartMinute = settings.aosStartMinute,
|
||||||
}
|
aosEndMinute = settings.aosEndMinute,
|
||||||
|
invertAosTimeWindow = settings.invertAosTimeWindow,
|
||||||
|
modes = settings.selectedModes
|
||||||
|
)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
override suspend fun getPosition(sat: OrbitalObject, pos: GeoPos, time: Long): OrbitalPos {
|
override suspend fun getPosition(sat: OrbitalObject, pos: GeoPos, time: Long): OrbitalPos {
|
||||||
|
|||||||
+21
-2
@@ -144,11 +144,30 @@ object DopplerFrequencyCalculator {
|
|||||||
val modes = listOfNotNull(transponder.downlinkMode, transponder.uplinkMode)
|
val modes = listOfNotNull(transponder.downlinkMode, transponder.uplinkMode)
|
||||||
.joinToString(separator = " ")
|
.joinToString(separator = " ")
|
||||||
.lowercase(Locale.ENGLISH)
|
.lowercase(Locale.ENGLISH)
|
||||||
val hasLinearName = info.contains("linear")
|
val hasLinearName = info.contains("linear") || info.contains(" lin") || info.startsWith("lin")
|
||||||
val hasTransponderName = info.contains("transponder") || info.contains("transp") ||
|
val hasTransponderName = info.contains("transponder") || info.contains("transp") ||
|
||||||
info.contains("xponder") || info.contains("xpdr")
|
info.contains("xponder") || info.contains("xpdr")
|
||||||
val hasLinearMode = listOf("ssb", "usb", "lsb", "cw").any { modes.contains(it) }
|
val hasLinearMode = listOf("ssb", "usb", "lsb", "cw").any { modes.contains(it) }
|
||||||
|
|
||||||
return (hasLinearName && hasTransponderName) || (hasTransponderName && hasLinearMode)
|
return (hasLinearName && hasTransponderName) || (hasTransponderName && hasLinearMode) ||
|
||||||
|
(hasLinearName && hasLinearMode)
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Removes duplicate transponder entries that describe the same physical
|
||||||
|
* transponder with different mode labels (e.g. SatNOGS lists AO-7's Mode A
|
||||||
|
* as both "Lin SSB" and "Lin CW", and JO-97's U/V transponder as both
|
||||||
|
* "CW Transponder" and "SSB Transponder").
|
||||||
|
*
|
||||||
|
* Entries sharing the same uplink/downlink frequency range are considered
|
||||||
|
* the same transponder. The non-CW entry is preferred because its invert
|
||||||
|
* flag is more reliable (e.g. JO-97's CW entry wrongly has invert=false).
|
||||||
|
*/
|
||||||
|
fun deduplicateTransponders(radios: List<SatRadio>): List<SatRadio> {
|
||||||
|
return radios.groupBy { radio ->
|
||||||
|
listOf(radio.uplinkLow, radio.uplinkHigh, radio.downlinkLow, radio.downlinkHigh)
|
||||||
|
}.values.map { group ->
|
||||||
|
group.firstOrNull { it.downlinkMode?.equals("CW", ignoreCase = true) != true } ?: group.first()
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
+84
-1
@@ -9,6 +9,7 @@ import org.junit.Test
|
|||||||
class DopplerFrequencyCalculatorTest {
|
class DopplerFrequencyCalculatorTest {
|
||||||
|
|
||||||
private fun linearTransponder(
|
private fun linearTransponder(
|
||||||
|
uuid: String = "linear",
|
||||||
upLow: Long = 145_000_000L,
|
upLow: Long = 145_000_000L,
|
||||||
upHigh: Long = 145_500_000L,
|
upHigh: Long = 145_500_000L,
|
||||||
downLow: Long = 435_000_000L,
|
downLow: Long = 435_000_000L,
|
||||||
@@ -18,7 +19,7 @@ class DopplerFrequencyCalculatorTest {
|
|||||||
downlinkMode: String? = "USB",
|
downlinkMode: String? = "USB",
|
||||||
uplinkMode: String? = "LSB"
|
uplinkMode: String? = "LSB"
|
||||||
) = SatRadio(
|
) = SatRadio(
|
||||||
uuid = "linear", info = info, isAlive = true,
|
uuid = uuid, info = info, isAlive = true,
|
||||||
downlinkLow = downLow, downlinkHigh = downHigh,
|
downlinkLow = downLow, downlinkHigh = downHigh,
|
||||||
downlinkMode = downlinkMode, uplinkLow = upLow, uplinkHigh = upHigh,
|
downlinkMode = downlinkMode, uplinkLow = upLow, uplinkHigh = upHigh,
|
||||||
uplinkMode = uplinkMode, isInverted = inverted, catnum = 12345
|
uplinkMode = uplinkMode, isInverted = inverted, catnum = 12345
|
||||||
@@ -68,11 +69,93 @@ class DopplerFrequencyCalculatorTest {
|
|||||||
assertFalse(DopplerFrequencyCalculator.isNamedLinearTransponder(driftingRangeEntry))
|
assertFalse(DopplerFrequencyCalculator.isNamedLinearTransponder(driftingRangeEntry))
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
fun isNamedLinearTransponder_returnsTrueForAbbreviatedLinName() {
|
||||||
|
// AO-7 style: "Mode V/A (A) Lin SSB" — "Lin" abbreviation, no "transponder" word
|
||||||
|
val ao7Entry = linearTransponder(info = "Mode V/A (A) Lin SSB", downlinkMode = "USB", uplinkMode = "USB")
|
||||||
|
assertTrue(DopplerFrequencyCalculator.isNamedLinearTransponder(ao7Entry))
|
||||||
|
val ao7CwEntry = linearTransponder(info = "Mode V/A (A) Lin CW", downlinkMode = "CW", uplinkMode = "CW")
|
||||||
|
assertTrue(DopplerFrequencyCalculator.isNamedLinearTransponder(ao7CwEntry))
|
||||||
|
val ao7ModeBEntry = linearTransponder(info = "Mode U/V (B) Lin", downlinkMode = "USB", uplinkMode = "LSB")
|
||||||
|
assertTrue(DopplerFrequencyCalculator.isNamedLinearTransponder(ao7ModeBEntry))
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
fun isNamedLinearTransponder_returnsTrueForLinearWithoutTransponderWord() {
|
||||||
|
// AO-73 style: "Mode U/V Linear" — has "Linear" but no "transponder"
|
||||||
|
val ao73Entry = linearTransponder(info = "Mode U/V Linear", downlinkMode = "USB", uplinkMode = "LSB")
|
||||||
|
assertTrue(DopplerFrequencyCalculator.isNamedLinearTransponder(ao73Entry))
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
fun isNamedLinearTransponder_returnsFalseForDownlinkContainingLinInsideWord() {
|
||||||
|
// "Downlink" contains "lin" but is not a linear-transponder name
|
||||||
|
val downlinkEntry = linearTransponder(info = "Mode U Downlink", downlinkMode = "FM", uplinkMode = "FM")
|
||||||
|
assertFalse(DopplerFrequencyCalculator.isNamedLinearTransponder(downlinkEntry))
|
||||||
|
}
|
||||||
|
|
||||||
@Test
|
@Test
|
||||||
fun isNamedLinearTransponder_returnsFalseForFmRepeater() {
|
fun isNamedLinearTransponder_returnsFalseForFmRepeater() {
|
||||||
assertFalse(DopplerFrequencyCalculator.isNamedLinearTransponder(fmTransponder()))
|
assertFalse(DopplerFrequencyCalculator.isNamedLinearTransponder(fmTransponder()))
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
fun deduplicateTransponders_mergesSameFrequencyRange() {
|
||||||
|
// AO-7's Mode A: same range, SSB and CW entries
|
||||||
|
val ssb = linearTransponder(
|
||||||
|
uuid = "ssb-uuid", info = "Mode V/A (A) Lin SSB",
|
||||||
|
downlinkMode = "USB", uplinkMode = "USB"
|
||||||
|
)
|
||||||
|
val cw = linearTransponder(
|
||||||
|
uuid = "cw-uuid", info = "Mode V/A (A) Lin CW",
|
||||||
|
downlinkMode = "CW", uplinkMode = "CW"
|
||||||
|
)
|
||||||
|
val modeB = linearTransponder(
|
||||||
|
uuid = "modeb-uuid", info = "Mode U/V (B) Lin",
|
||||||
|
upLow = 432_125_000L, upHigh = 432_175_000L,
|
||||||
|
downLow = 145_925_000L, downHigh = 145_975_000L,
|
||||||
|
downlinkMode = "USB", uplinkMode = "LSB"
|
||||||
|
)
|
||||||
|
val result = DopplerFrequencyCalculator.deduplicateTransponders(listOf(ssb, cw, modeB))
|
||||||
|
assertEquals(2, result.size)
|
||||||
|
// SSB entry should be preferred over CW (same range)
|
||||||
|
assertEquals("ssb-uuid", result[0].uuid)
|
||||||
|
assertEquals("modeb-uuid", result[1].uuid)
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
fun deduplicateTransponders_prefersNonCwEntry() {
|
||||||
|
// JO-97: CW entry has invert=false (wrong), SSB has invert=true (correct)
|
||||||
|
val cw = linearTransponder(
|
||||||
|
uuid = "cw-uuid", info = "U/V CW Transponder",
|
||||||
|
downlinkMode = "CW", uplinkMode = "CW",
|
||||||
|
upLow = 435_100_000L, upHigh = 435_120_000L,
|
||||||
|
downLow = 145_855_000L, downHigh = 145_875_000L
|
||||||
|
)
|
||||||
|
val ssb = linearTransponder(
|
||||||
|
uuid = "ssb-uuid", info = "U/V SSB Transponder",
|
||||||
|
downlinkMode = "USB", uplinkMode = "LSB",
|
||||||
|
upLow = 435_100_000L, upHigh = 435_120_000L,
|
||||||
|
downLow = 145_855_000L, downHigh = 145_875_000L,
|
||||||
|
inverted = true
|
||||||
|
)
|
||||||
|
val result = DopplerFrequencyCalculator.deduplicateTransponders(listOf(cw, ssb))
|
||||||
|
assertEquals(1, result.size)
|
||||||
|
assertEquals("ssb-uuid", result[0].uuid)
|
||||||
|
// Verify the correct invert flag is preserved
|
||||||
|
assertTrue(result[0].isInverted)
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
fun deduplicateTransponders_preservesUniqueEntries() {
|
||||||
|
val t1 = linearTransponder(uuid = "t1", upLow = 145_000_000L, upHigh = 145_500_000L,
|
||||||
|
downLow = 435_000_000L, downHigh = 435_500_000L)
|
||||||
|
val t2 = linearTransponder(uuid = "t2", upLow = 435_000_000L, upHigh = 435_500_000L,
|
||||||
|
downLow = 145_000_000L, downHigh = 145_500_000L)
|
||||||
|
val result = DopplerFrequencyCalculator.deduplicateTransponders(listOf(t1, t2))
|
||||||
|
assertEquals(2, result.size)
|
||||||
|
}
|
||||||
|
|
||||||
@Test
|
@Test
|
||||||
fun computeUplinkFromDownlink_linear_noDoppler() {
|
fun computeUplinkFromDownlink_linear_noDoppler() {
|
||||||
val xpdr = linearTransponder()
|
val xpdr = linearTransponder()
|
||||||
|
|||||||
@@ -199,7 +199,8 @@
|
|||||||
\n• Dave Moten (predict4java)
|
\n• Dave Moten (predict4java)
|
||||||
\n• Alexandru Csete (Gpredict)
|
\n• Alexandru Csete (Gpredict)
|
||||||
\n• Dr T.S. Kelso (Celestrak)
|
\n• Dr T.S. Kelso (Celestrak)
|
||||||
\n• Libre Space Foundation (SatNOGS)</string>
|
\n• Libre Space Foundation (SatNOGS)
|
||||||
|
\n• BG7NTA (CW decoding feature)</string>
|
||||||
<string name="prefs_outro_license">Bu uygulama herhangi bir garanti sunmaz</string>
|
<string name="prefs_outro_license">Bu uygulama herhangi bir garanti sunmaz</string>
|
||||||
|
|
||||||
</resources>
|
</resources>
|
||||||
@@ -175,7 +175,40 @@
|
|||||||
\n• Alexandru Csete (Gpredict)
|
\n• Alexandru Csete (Gpredict)
|
||||||
\n• Dr T.S. Kelso (Celestrak)
|
\n• Dr T.S. Kelso (Celestrak)
|
||||||
\n• Libre Space Foundation (SatNOGS)
|
\n• Libre Space Foundation (SatNOGS)
|
||||||
\n• xdsopl 和 Robot36 贡献者!</string>
|
\n• xdsopl 和 Robot36 贡献者!
|
||||||
|
\n• BG7NTA(CW解码功能)</string>
|
||||||
<string name="prefs_outro_license">该应用程序不提供任何保修.</string>
|
<string name="prefs_outro_license">该应用程序不提供任何保修.</string>
|
||||||
|
|
||||||
|
<!-- 匹配页 Mutual match page -->
|
||||||
|
<string name="mutual_title">过境匹配</string>
|
||||||
|
<string name="mutual_station_you">你的位置</string>
|
||||||
|
<string name="mutual_station_opposite">友台位置</string>
|
||||||
|
<string name="mutual_label_you">你</string>
|
||||||
|
<string name="mutual_label_opposite">友台</string>
|
||||||
|
<string name="mutual_status_calculating">正在计算双方共同可见窗口…</string>
|
||||||
|
<string name="mutual_status_error">请修正提示后重新查询</string>
|
||||||
|
<string name="mutual_status_matches">找到 %d 个匹配 · 点击卡片展开曲线</string>
|
||||||
|
<string name="mutual_status_none">未找到共同可见窗口,调整时间或仰角后重试</string>
|
||||||
|
<string name="mutual_status_idle">输入双方位置后点击查询</string>
|
||||||
|
<string name="mutual_chip_calculating">计算中</string>
|
||||||
|
<string name="mutual_chip_error">有错误</string>
|
||||||
|
<string name="mutual_chip_matches">%d 个匹配</string>
|
||||||
|
<string name="mutual_chip_none">无匹配</string>
|
||||||
|
<string name="mutual_chip_idle">待查询</string>
|
||||||
|
<string name="mutual_use_current">填入当前位置</string>
|
||||||
|
<string name="mutual_lat">纬度</string>
|
||||||
|
<string name="mutual_lon">经度</string>
|
||||||
|
<string name="mutual_grid">网格</string>
|
||||||
|
<string name="mutual_grid_hint">支持 4/6/8 位 Maidenhead 网格;也可直接输入经纬度</string>
|
||||||
|
<string name="mutual_min_elevation">最小仰角:%d°</string>
|
||||||
|
<string name="mutual_time_range">时间范围</string>
|
||||||
|
<string name="mutual_calculating">计算中…</string>
|
||||||
|
<string name="mutual_query">查询匹配</string>
|
||||||
|
<string name="mutual_elevation_you">你:%d°</string>
|
||||||
|
<string name="mutual_elevation_opposite">友台:%d°</string>
|
||||||
|
<string name="mutual_both_tracks">双方轨迹</string>
|
||||||
|
<string name="mutual_open_radar">查看雷达</string>
|
||||||
|
<string name="mutual_station_a">站点A</string>
|
||||||
|
<string name="mutual_station_b">站点B</string>
|
||||||
|
|
||||||
</resources>
|
</resources>
|
||||||
@@ -211,7 +211,40 @@
|
|||||||
\n* Dr T.S. Kelso (Celestrak)
|
\n* Dr T.S. Kelso (Celestrak)
|
||||||
\n* Libre Space Foundation (SatNOGS)
|
\n* Libre Space Foundation (SatNOGS)
|
||||||
\n* xdsopl and Robot36 contributors!
|
\n* xdsopl and Robot36 contributors!
|
||||||
|
\n* BG7NTA (CW decoding feature)
|
||||||
</string>
|
</string>
|
||||||
<string name="prefs_outro_license">The app comes with no warranty</string>
|
<string name="prefs_outro_license">The app comes with no warranty</string>
|
||||||
|
|
||||||
|
<!-- Mutual match page -->
|
||||||
|
<string name="mutual_title">Pass Match</string>
|
||||||
|
<string name="mutual_station_you">Your Station</string>
|
||||||
|
<string name="mutual_station_opposite">Opposite Station</string>
|
||||||
|
<string name="mutual_label_you">You</string>
|
||||||
|
<string name="mutual_label_opposite">Opposite</string>
|
||||||
|
<string name="mutual_status_calculating">Calculating mutual visibility windows…</string>
|
||||||
|
<string name="mutual_status_error">Fix the reported issues and retry</string>
|
||||||
|
<string name="mutual_status_matches">%d matches found · tap a card to expand</string>
|
||||||
|
<string name="mutual_status_none">No mutual windows found. Adjust time or min elevation and retry</string>
|
||||||
|
<string name="mutual_status_idle">Enter both stations, then tap Query</string>
|
||||||
|
<string name="mutual_chip_calculating">Calculating</string>
|
||||||
|
<string name="mutual_chip_error">Error</string>
|
||||||
|
<string name="mutual_chip_matches">%d matches</string>
|
||||||
|
<string name="mutual_chip_none">No matches</string>
|
||||||
|
<string name="mutual_chip_idle">Idle</string>
|
||||||
|
<string name="mutual_use_current">Use current</string>
|
||||||
|
<string name="mutual_lat">Lat</string>
|
||||||
|
<string name="mutual_lon">Lon</string>
|
||||||
|
<string name="mutual_grid">Grid</string>
|
||||||
|
<string name="mutual_grid_hint">Supports 4/6/8-char Maidenhead grid; or enter lat/lon directly</string>
|
||||||
|
<string name="mutual_min_elevation">Min elevation: %d°</string>
|
||||||
|
<string name="mutual_time_range">Time range</string>
|
||||||
|
<string name="mutual_calculating">Calculating…</string>
|
||||||
|
<string name="mutual_query">Query Match</string>
|
||||||
|
<string name="mutual_elevation_you">You: %d°</string>
|
||||||
|
<string name="mutual_elevation_opposite">Opposite: %d°</string>
|
||||||
|
<string name="mutual_both_tracks">Both tracks</string>
|
||||||
|
<string name="mutual_open_radar">Open Radar</string>
|
||||||
|
<string name="mutual_station_a">Station A</string>
|
||||||
|
<string name="mutual_station_b">Station B</string>
|
||||||
|
|
||||||
</resources>
|
</resources>
|
||||||
@@ -139,7 +139,7 @@ public class MainActivity {
|
|||||||
public void onResume() {
|
public void onResume() {
|
||||||
android.content.SharedPreferences prefs = mActivity.getSharedPreferences(
|
android.content.SharedPreferences prefs = mActivity.getSharedPreferences(
|
||||||
mActivity.getPackageName() + "_preferences", Context.MODE_PRIVATE);
|
mActivity.getPackageName() + "_preferences", Context.MODE_PRIVATE);
|
||||||
String messageType = prefs.getString("message_type", "general_text");
|
String messageType = prefs.getString("message_type", "ham_radio_qso");
|
||||||
b bVar = this.f11037F;
|
b bVar = this.f11037F;
|
||||||
if (bVar != null) {
|
if (bVar != null) {
|
||||||
bVar.f623s.set(messageType.equals("ham_radio_qso"));
|
bVar.f623s.set(messageType.equals("ham_radio_qso"));
|
||||||
|
|||||||
+4
-2
@@ -24,6 +24,7 @@ import androidx.compose.foundation.gestures.detectTapGestures
|
|||||||
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
|
||||||
|
import androidx.compose.ui.res.stringResource
|
||||||
import androidx.compose.foundation.layout.fillMaxWidth
|
import androidx.compose.foundation.layout.fillMaxWidth
|
||||||
import androidx.compose.foundation.layout.height
|
import androidx.compose.foundation.layout.height
|
||||||
import androidx.compose.foundation.layout.padding
|
import androidx.compose.foundation.layout.padding
|
||||||
@@ -40,6 +41,7 @@ import androidx.compose.ui.graphics.nativeCanvas
|
|||||||
import androidx.compose.ui.input.pointer.pointerInput
|
import androidx.compose.ui.input.pointer.pointerInput
|
||||||
import androidx.compose.ui.unit.dp
|
import androidx.compose.ui.unit.dp
|
||||||
import androidx.compose.ui.unit.sp
|
import androidx.compose.ui.unit.sp
|
||||||
|
import com.rtbishop.look4sat.core.presentation.R
|
||||||
import java.text.SimpleDateFormat
|
import java.text.SimpleDateFormat
|
||||||
import java.util.Date
|
import java.util.Date
|
||||||
import java.util.Locale
|
import java.util.Locale
|
||||||
@@ -226,8 +228,8 @@ fun ElevationCurveChart(
|
|||||||
modifier = Modifier.fillMaxWidth().padding(horizontal = 8.dp, vertical = 4.dp),
|
modifier = Modifier.fillMaxWidth().padding(horizontal = 8.dp, vertical = 4.dp),
|
||||||
horizontalArrangement = Arrangement.SpaceEvenly
|
horizontalArrangement = Arrangement.SpaceEvenly
|
||||||
) {
|
) {
|
||||||
Text("● 站点A", color = colorA, fontSize = 12.sp)
|
Text(stringResource(R.string.mutual_station_a), color = colorA, fontSize = 12.sp)
|
||||||
Text("● 站点B", color = colorB, fontSize = 12.sp)
|
Text(stringResource(R.string.mutual_station_b), color = colorB, fontSize = 12.sp)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
+2
-2
@@ -57,8 +57,8 @@ private const val STROKE_WIDTH = 2.5f
|
|||||||
fun MutualRadarView(
|
fun MutualRadarView(
|
||||||
trackSamples: List<TrackSample>,
|
trackSamples: List<TrackSample>,
|
||||||
progress: Float = 0.5f,
|
progress: Float = 0.5f,
|
||||||
labelA: String = "站点A",
|
labelA: String = "Station A",
|
||||||
labelB: String = "站点B",
|
labelB: String = "Station B",
|
||||||
modifier: Modifier = Modifier
|
modifier: Modifier = Modifier
|
||||||
) {
|
) {
|
||||||
if (trackSamples.isEmpty()) return
|
if (trackSamples.isEmpty()) return
|
||||||
|
|||||||
@@ -90,7 +90,7 @@ fun MutualScreen(
|
|||||||
},
|
},
|
||||||
topInfo = {
|
topInfo = {
|
||||||
Text(
|
Text(
|
||||||
text = "过境匹配",
|
text = stringResource(R.string.mutual_title),
|
||||||
style = MaterialTheme.typography.titleMedium,
|
style = MaterialTheme.typography.titleMedium,
|
||||||
fontWeight = FontWeight.Bold
|
fontWeight = FontWeight.Bold
|
||||||
)
|
)
|
||||||
@@ -198,7 +198,7 @@ private fun MutualContent(
|
|||||||
) {
|
) {
|
||||||
if (isVertical) {
|
if (isVertical) {
|
||||||
StationInputCard(
|
StationInputCard(
|
||||||
title = "你的位置",
|
title = stringResource(R.string.mutual_station_you),
|
||||||
titleColor = MaterialTheme.colorScheme.primary,
|
titleColor = MaterialTheme.colorScheme.primary,
|
||||||
lat = state.stationALat,
|
lat = state.stationALat,
|
||||||
lon = state.stationALon,
|
lon = state.stationALon,
|
||||||
@@ -214,7 +214,7 @@ private fun MutualContent(
|
|||||||
currentPositionAction = onUseCurrentPosition
|
currentPositionAction = onUseCurrentPosition
|
||||||
)
|
)
|
||||||
StationInputCard(
|
StationInputCard(
|
||||||
title = "友台位置",
|
title = stringResource(R.string.mutual_station_opposite),
|
||||||
titleColor = MaterialTheme.colorScheme.tertiary,
|
titleColor = MaterialTheme.colorScheme.tertiary,
|
||||||
lat = state.stationBLat,
|
lat = state.stationBLat,
|
||||||
lon = state.stationBLon,
|
lon = state.stationBLon,
|
||||||
@@ -234,7 +234,7 @@ private fun MutualContent(
|
|||||||
horizontalArrangement = Arrangement.spacedBy(6.dp)
|
horizontalArrangement = Arrangement.spacedBy(6.dp)
|
||||||
) {
|
) {
|
||||||
StationInputCard(
|
StationInputCard(
|
||||||
title = "你的位置",
|
title = stringResource(R.string.mutual_station_you),
|
||||||
titleColor = MaterialTheme.colorScheme.primary,
|
titleColor = MaterialTheme.colorScheme.primary,
|
||||||
lat = state.stationALat,
|
lat = state.stationALat,
|
||||||
lon = state.stationALon,
|
lon = state.stationALon,
|
||||||
@@ -251,7 +251,7 @@ private fun MutualContent(
|
|||||||
modifier = Modifier.weight(1f)
|
modifier = Modifier.weight(1f)
|
||||||
)
|
)
|
||||||
StationInputCard(
|
StationInputCard(
|
||||||
title = "友台位置",
|
title = stringResource(R.string.mutual_station_opposite),
|
||||||
titleColor = MaterialTheme.colorScheme.tertiary,
|
titleColor = MaterialTheme.colorScheme.tertiary,
|
||||||
lat = state.stationBLat,
|
lat = state.stationBLat,
|
||||||
lon = state.stationBLon,
|
lon = state.stationBLon,
|
||||||
@@ -294,8 +294,8 @@ private fun MutualContent(
|
|||||||
startTime = pass.startTime,
|
startTime = pass.startTime,
|
||||||
endTime = pass.endTime,
|
endTime = pass.endTime,
|
||||||
maxElev = maxOf(pass.maxElevationA, pass.maxElevationB, 10.0),
|
maxElev = maxOf(pass.maxElevationA, pass.maxElevationB, 10.0),
|
||||||
labelA = "你",
|
labelA = stringResource(R.string.mutual_label_you),
|
||||||
labelB = "友台"
|
labelB = stringResource(R.string.mutual_label_opposite)
|
||||||
)
|
)
|
||||||
MutualPassCard(
|
MutualPassCard(
|
||||||
pass = pass,
|
pass = pass,
|
||||||
@@ -341,20 +341,22 @@ private fun MutualStatusChip(state: MutualUiState) {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@Composable
|
||||||
private fun mutualStatusText(state: MutualUiState): String = when {
|
private fun mutualStatusText(state: MutualUiState): String = when {
|
||||||
state.isCalculating -> "正在计算双方共同可见窗口…"
|
state.isCalculating -> stringResource(R.string.mutual_status_calculating)
|
||||||
state.errorMessage != null -> "请修正提示后重新查询"
|
state.errorMessage != null -> stringResource(R.string.mutual_status_error)
|
||||||
state.mutualPasses.isNotEmpty() -> "找到 ${state.mutualPasses.size} 个匹配 · 点击卡片展开曲线"
|
state.mutualPasses.isNotEmpty() -> stringResource(R.string.mutual_status_matches, state.mutualPasses.size)
|
||||||
state.hasSearched -> "未找到共同可见窗口,调整时间或仰角后重试"
|
state.hasSearched -> stringResource(R.string.mutual_status_none)
|
||||||
else -> "输入双方位置后点击查询"
|
else -> stringResource(R.string.mutual_status_idle)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@Composable
|
||||||
private fun mutualStatusChipText(state: MutualUiState): String = when {
|
private fun mutualStatusChipText(state: MutualUiState): String = when {
|
||||||
state.isCalculating -> "计算中"
|
state.isCalculating -> stringResource(R.string.mutual_chip_calculating)
|
||||||
state.errorMessage != null -> "有错误"
|
state.errorMessage != null -> stringResource(R.string.mutual_chip_error)
|
||||||
state.mutualPasses.isNotEmpty() -> "${state.mutualPasses.size}个匹配"
|
state.mutualPasses.isNotEmpty() -> stringResource(R.string.mutual_chip_matches, state.mutualPasses.size)
|
||||||
state.hasSearched -> "无匹配"
|
state.hasSearched -> stringResource(R.string.mutual_chip_none)
|
||||||
else -> "待查询"
|
else -> stringResource(R.string.mutual_chip_idle)
|
||||||
}
|
}
|
||||||
|
|
||||||
@Composable
|
@Composable
|
||||||
@@ -393,7 +395,7 @@ private fun StationInputCard(
|
|||||||
)
|
)
|
||||||
if (currentPositionAction != null) {
|
if (currentPositionAction != null) {
|
||||||
OutlinedButton(onClick = currentPositionAction) {
|
OutlinedButton(onClick = currentPositionAction) {
|
||||||
Text("填入当前位置", style = MaterialTheme.typography.bodySmall)
|
Text(stringResource(R.string.mutual_use_current), style = MaterialTheme.typography.bodySmall)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -405,7 +407,7 @@ private fun StationInputCard(
|
|||||||
OutlinedTextField(
|
OutlinedTextField(
|
||||||
value = lat,
|
value = lat,
|
||||||
onValueChange = onLatChange,
|
onValueChange = onLatChange,
|
||||||
label = { Text("纬度") },
|
label = { Text(stringResource(R.string.mutual_lat)) },
|
||||||
placeholder = { Text(latPlaceholder) },
|
placeholder = { Text(latPlaceholder) },
|
||||||
keyboardOptions = KeyboardOptions(keyboardType = KeyboardType.Decimal),
|
keyboardOptions = KeyboardOptions(keyboardType = KeyboardType.Decimal),
|
||||||
singleLine = true,
|
singleLine = true,
|
||||||
@@ -414,7 +416,7 @@ private fun StationInputCard(
|
|||||||
OutlinedTextField(
|
OutlinedTextField(
|
||||||
value = lon,
|
value = lon,
|
||||||
onValueChange = onLonChange,
|
onValueChange = onLonChange,
|
||||||
label = { Text("经度") },
|
label = { Text(stringResource(R.string.mutual_lon)) },
|
||||||
placeholder = { Text(lonPlaceholder) },
|
placeholder = { Text(lonPlaceholder) },
|
||||||
keyboardOptions = KeyboardOptions(keyboardType = KeyboardType.Decimal),
|
keyboardOptions = KeyboardOptions(keyboardType = KeyboardType.Decimal),
|
||||||
singleLine = true,
|
singleLine = true,
|
||||||
@@ -425,15 +427,15 @@ private fun StationInputCard(
|
|||||||
OutlinedTextField(
|
OutlinedTextField(
|
||||||
value = grid,
|
value = grid,
|
||||||
onValueChange = onGridChange,
|
onValueChange = onGridChange,
|
||||||
label = { Text("网格") },
|
label = { Text(stringResource(R.string.mutual_grid)) },
|
||||||
placeholder = { Text(gridPlaceholder) },
|
placeholder = { Text(gridPlaceholder) },
|
||||||
supportingText = { Text("支持 4/6/8 位 Maidenhead 网格;也可直接输入经纬度") },
|
supportingText = { Text(stringResource(R.string.mutual_grid_hint)) },
|
||||||
singleLine = true,
|
singleLine = true,
|
||||||
modifier = Modifier.fillMaxWidth()
|
modifier = Modifier.fillMaxWidth()
|
||||||
)
|
)
|
||||||
|
|
||||||
Text(
|
Text(
|
||||||
text = "最小仰角:${minElev.toInt()}°",
|
text = stringResource(R.string.mutual_min_elevation, minElev.toInt()),
|
||||||
style = MaterialTheme.typography.bodySmall,
|
style = MaterialTheme.typography.bodySmall,
|
||||||
color = MaterialTheme.colorScheme.onSurfaceVariant
|
color = MaterialTheme.colorScheme.onSurfaceVariant
|
||||||
)
|
)
|
||||||
@@ -461,7 +463,7 @@ private fun MatchSearchCard(
|
|||||||
verticalArrangement = Arrangement.spacedBy(10.dp)
|
verticalArrangement = Arrangement.spacedBy(10.dp)
|
||||||
) {
|
) {
|
||||||
Text(
|
Text(
|
||||||
text = "时间范围",
|
text = stringResource(R.string.mutual_time_range),
|
||||||
style = MaterialTheme.typography.titleSmall,
|
style = MaterialTheme.typography.titleSmall,
|
||||||
fontWeight = FontWeight.Medium
|
fontWeight = FontWeight.Medium
|
||||||
)
|
)
|
||||||
@@ -492,7 +494,7 @@ private fun MatchSearchCard(
|
|||||||
)
|
)
|
||||||
Spacer(Modifier.width(8.dp))
|
Spacer(Modifier.width(8.dp))
|
||||||
}
|
}
|
||||||
Text(if (isCalculating) "计算中..." else "查询匹配")
|
Text(if (isCalculating) stringResource(R.string.mutual_calculating) else stringResource(R.string.mutual_query))
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -558,12 +560,12 @@ private fun MutualPassCard(
|
|||||||
horizontalArrangement = Arrangement.SpaceBetween
|
horizontalArrangement = Arrangement.SpaceBetween
|
||||||
) {
|
) {
|
||||||
Text(
|
Text(
|
||||||
text = "你: ${pass.maxElevationA}°",
|
text = stringResource(R.string.mutual_elevation_you, pass.maxElevationA.toInt()),
|
||||||
style = MaterialTheme.typography.bodySmall,
|
style = MaterialTheme.typography.bodySmall,
|
||||||
color = MaterialTheme.colorScheme.primary
|
color = MaterialTheme.colorScheme.primary
|
||||||
)
|
)
|
||||||
Text(
|
Text(
|
||||||
text = "友台: ${pass.maxElevationB}°",
|
text = stringResource(R.string.mutual_elevation_opposite, pass.maxElevationB.toInt()),
|
||||||
style = MaterialTheme.typography.bodySmall,
|
style = MaterialTheme.typography.bodySmall,
|
||||||
color = MaterialTheme.colorScheme.tertiary
|
color = MaterialTheme.colorScheme.tertiary
|
||||||
)
|
)
|
||||||
@@ -579,7 +581,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 = stringResource(R.string.mutual_both_tracks),
|
||||||
style = MaterialTheme.typography.titleSmall,
|
style = MaterialTheme.typography.titleSmall,
|
||||||
fontWeight = FontWeight.Medium,
|
fontWeight = FontWeight.Medium,
|
||||||
color = MaterialTheme.colorScheme.onSurfaceVariant
|
color = MaterialTheme.colorScheme.onSurfaceVariant
|
||||||
@@ -587,8 +589,8 @@ private fun MutualPassCard(
|
|||||||
MutualRadarView(
|
MutualRadarView(
|
||||||
trackSamples = visibleTracks,
|
trackSamples = visibleTracks,
|
||||||
progress = dragProgress,
|
progress = dragProgress,
|
||||||
labelA = "你",
|
labelA = stringResource(R.string.mutual_label_you),
|
||||||
labelB = "友台"
|
labelB = stringResource(R.string.mutual_label_opposite)
|
||||||
)
|
)
|
||||||
Spacer(Modifier.height(8.dp))
|
Spacer(Modifier.height(8.dp))
|
||||||
}
|
}
|
||||||
@@ -613,7 +615,7 @@ private fun MutualPassCard(
|
|||||||
modifier = Modifier.height(16.dp).width(16.dp)
|
modifier = Modifier.height(16.dp).width(16.dp)
|
||||||
)
|
)
|
||||||
Spacer(Modifier.width(6.dp))
|
Spacer(Modifier.width(6.dp))
|
||||||
Text("查看雷达")
|
Text(stringResource(R.string.mutual_open_radar))
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
+5
-5
@@ -158,13 +158,13 @@ class MutualViewModel(
|
|||||||
var posB = resolvePosition(state.stationBLat, state.stationBLon, state.stationBGrid)
|
var posB = resolvePosition(state.stationBLat, state.stationBLon, state.stationBGrid)
|
||||||
|
|
||||||
if (posA == null || posB == null) {
|
if (posA == null || posB == null) {
|
||||||
_uiState.update { it.copy(errorMessage = "请输入有效的位置坐标或网格(4/6/8位)") }
|
_uiState.update { it.copy(errorMessage = "Please enter valid coordinates or a Maidenhead grid (4/6/8 chars)") }
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
val satellites = satelliteRepo.satellites.value
|
val satellites = satelliteRepo.satellites.value
|
||||||
if (satellites.isEmpty()) {
|
if (satellites.isEmpty()) {
|
||||||
_uiState.update { it.copy(errorMessage = "没有卫星数据,请先在卫星列表中选择卫星") }
|
_uiState.update { it.copy(errorMessage = "No satellite data. Select satellites from the list first.") }
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -193,9 +193,9 @@ class MutualViewModel(
|
|||||||
val passCount = satelliteRepo.passes.value.size
|
val passCount = satelliteRepo.passes.value.size
|
||||||
val satCount = satellites.size
|
val satCount = satellites.size
|
||||||
when {
|
when {
|
||||||
passCount == 0 -> "过境列表为空,请先在卫星列表中选择卫星"
|
passCount == 0 -> "Pass list is empty. Select satellites from the list first."
|
||||||
satCount == 0 -> "没有已选中的卫星"
|
satCount == 0 -> "No satellites selected."
|
||||||
else -> "未找到共同过境(${passCount}个过境,${satCount}颗卫星)"
|
else -> "No mutual passes found (${passCount} passes, ${satCount} satellites)."
|
||||||
}
|
}
|
||||||
} else null
|
} else null
|
||||||
|
|
||||||
|
|||||||
@@ -148,6 +148,7 @@ fun CalculatorPage(
|
|||||||
) {
|
) {
|
||||||
val calculatorTransceivers = remember(transceivers) {
|
val calculatorTransceivers = remember(transceivers) {
|
||||||
transceivers.filter(DopplerFrequencyCalculator::isNamedLinearTransponder)
|
transceivers.filter(DopplerFrequencyCalculator::isNamedLinearTransponder)
|
||||||
|
.let(DopplerFrequencyCalculator::deduplicateTransponders)
|
||||||
}
|
}
|
||||||
val selectedTransceiver = remember(calculatorTransceivers, selectedUuid) {
|
val selectedTransceiver = remember(calculatorTransceivers, selectedUuid) {
|
||||||
calculatorTransceivers.firstOrNull { it.uuid == selectedUuid }
|
calculatorTransceivers.firstOrNull { it.uuid == selectedUuid }
|
||||||
|
|||||||
Reference in new issue
Block a user