Compare commits
14
Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
213425379c | ||
|
|
c3444aed24 | ||
|
|
368bd199e5 | ||
|
|
2298d8eea4 | ||
|
|
7bb0bbbb46 | ||
|
|
deec5581ae | ||
|
|
1de5632428 | ||
|
|
2dfd76825b | ||
|
|
06fc8bc2fb | ||
|
|
75322390b8 | ||
|
|
d1312f1f09 | ||
|
|
75acbbd633 | ||
|
|
a37fb8f773 | ||
|
|
9d0d971500 |
No files matched your search
@@ -4,10 +4,15 @@ on:
|
||||
push:
|
||||
tags:
|
||||
- v**
|
||||
workflow_dispatch:
|
||||
inputs:
|
||||
tag_name:
|
||||
description: 'Release tag name (e.g. v4.4.3)'
|
||||
required: false
|
||||
default: ''
|
||||
|
||||
env:
|
||||
TAG_NAME: ${{ github.ref_name }}
|
||||
GITHUB_TOKEN: ${{ secrets.RELEASE_TOKEN }}
|
||||
TAG_NAME: ${{ github.event.inputs.tag_name || github.ref_name }}
|
||||
|
||||
jobs:
|
||||
release:
|
||||
@@ -16,19 +21,19 @@ jobs:
|
||||
contents: write
|
||||
steps:
|
||||
- name: Checkout Repository
|
||||
uses: actions/checkout@v7
|
||||
uses: actions/checkout@v4
|
||||
|
||||
- name: Setup Java
|
||||
uses: actions/setup-java@v5
|
||||
uses: actions/setup-java@v4
|
||||
with:
|
||||
distribution: 'temurin'
|
||||
java-version: '21'
|
||||
|
||||
- name: Setup Gradle
|
||||
uses: gradle/actions/setup-gradle@v6
|
||||
uses: gradle/actions/setup-gradle@v4
|
||||
|
||||
- name: Assemble Artifacts
|
||||
run: ./gradlew assembleRelease bundleRelease
|
||||
- name: Assemble APK
|
||||
run: ./gradlew assembleRelease
|
||||
|
||||
- name: Sign APK
|
||||
run: |
|
||||
@@ -44,38 +49,9 @@ jobs:
|
||||
"$APK"
|
||||
rm keystore.jks
|
||||
|
||||
- name: Sign Bundle
|
||||
run: |
|
||||
echo "${{ secrets.KEY_STORE }}" | base64 -d > keystore.jks
|
||||
AAB=$(find app/build/outputs/bundle/release -name "*.aab" | head -1)
|
||||
jarsigner -verbose -sigalg SHA256withRSA -digestalg SHA-256 \
|
||||
-keystore keystore.jks \
|
||||
-storepass ${{ secrets.KEY_STORE_PASSWORD }} \
|
||||
-keypass ${{ secrets.KEY_PASSWORD }} \
|
||||
"$AAB" ${{ secrets.KEY_ALIAS }}
|
||||
rm keystore.jks
|
||||
|
||||
- name: Setup Ruby
|
||||
uses: ruby/setup-ruby@v1
|
||||
with:
|
||||
ruby-version: '3.4'
|
||||
|
||||
- name: Deploy Bundle to Google Play
|
||||
run: |
|
||||
gem install multi_json
|
||||
gem install fastlane --no-document
|
||||
AAB=$(find app/build/outputs/bundle/release -name "*.aab" | head -1)
|
||||
echo '${{ secrets.SERVICE_ACCOUNT_JSON }}' > service_account.json
|
||||
fastlane supply \
|
||||
--aab "$AAB" \
|
||||
--json_key service_account.json \
|
||||
--package_name com.rtbishop.look4sat \
|
||||
--track production \
|
||||
--skip_upload_images true \
|
||||
--skip_upload_screenshots true \
|
||||
rm service_account.json
|
||||
|
||||
- name: Create Release
|
||||
env:
|
||||
GH_TOKEN: ${{ github.token }}
|
||||
run: |
|
||||
gh release create $TAG_NAME --title=$TAG_NAME --generate-notes
|
||||
gh release upload $TAG_NAME app/build/outputs/apk/release/look4sat.apk
|
||||
@@ -1,3 +1,29 @@
|
||||
import java.util.Properties
|
||||
|
||||
plugins {
|
||||
alias(libs.plugins.convention.applicationPlugin)
|
||||
}
|
||||
|
||||
// Load signing config from keystore.properties (gitignored, never commit credentials)
|
||||
val keystoreProperties = Properties().apply {
|
||||
val propsFile = rootProject.file("keystore.properties")
|
||||
if (propsFile.exists()) propsFile.inputStream().use { load(it) }
|
||||
}
|
||||
|
||||
android {
|
||||
signingConfigs {
|
||||
if (keystoreProperties["storeFile"] != null) {
|
||||
create("release") {
|
||||
storeFile = rootProject.file(keystoreProperties["storeFile"] as String)
|
||||
storePassword = keystoreProperties["storePassword"] as String
|
||||
keyAlias = keystoreProperties["keyAlias"] as String
|
||||
keyPassword = keystoreProperties["keyPassword"] as String
|
||||
}
|
||||
}
|
||||
}
|
||||
buildTypes {
|
||||
release {
|
||||
signingConfig = signingConfigs.findByName("release")
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -43,7 +43,7 @@
|
||||
|
||||
<meta-data
|
||||
android:name="android.telephony.PROPERTY_SATELLITE_DATA_OPTIMIZED"
|
||||
android:value="com.rtbishop.look4sat" />
|
||||
android:value="com.rtbishop.look4sat.bg7nta" />
|
||||
|
||||
</application>
|
||||
</manifest>
|
||||
+1
-1
@@ -57,7 +57,7 @@ internal fun Project.setupAndroidApp() {
|
||||
namespace = libs.versions.packageName.get()
|
||||
compileSdk = libs.versions.compileSdk.get().toInt()
|
||||
defaultConfig {
|
||||
applicationId = libs.versions.packageName.get()
|
||||
applicationId = libs.versions.applicationId.get()
|
||||
minSdk = libs.versions.minSdk.get().toInt()
|
||||
versionCode = libs.versions.appVersionCode.get().toInt()
|
||||
versionName = libs.versions.appVersionName.get()
|
||||
|
||||
+19
-21
@@ -95,14 +95,8 @@ class SatelliteRepo(
|
||||
}
|
||||
}
|
||||
|
||||
override suspend fun getRadios(
|
||||
sat: OrbitalObject,
|
||||
pos: GeoPos,
|
||||
radios: List<SatRadio>,
|
||||
time: Long
|
||||
): List<SatRadio> {
|
||||
override suspend fun getRadios(satPos: OrbitalPos, radios: List<SatRadio>): List<SatRadio> {
|
||||
return withContext(dispatcher) {
|
||||
val satPos = sat.getPosition(pos, time)
|
||||
radios.map { transmitter ->
|
||||
transmitter.copy(
|
||||
downlinkLow = transmitter.downlinkLow?.let { satPos.getDownlinkFreq(it) },
|
||||
@@ -242,13 +236,16 @@ class SatelliteRepo(
|
||||
if (elevation > maxElevation) maxElevation = elevation
|
||||
} while (elevation < 0.0)
|
||||
|
||||
// refine AOS to ~500ms precision
|
||||
calendarTimeMillis -= 60L * 1000L
|
||||
do {
|
||||
calendarTimeMillis += 500L
|
||||
elevation = sat.getElevation(pos, calendarTimeMillis)
|
||||
if (elevation > maxElevation) maxElevation = elevation
|
||||
} while (elevation < 0.0)
|
||||
// refine AOS to ~500ms precision via binary search.
|
||||
// Elevation is monotonic across the horizon crossing, so binary search
|
||||
// finds the crossing in ~8 SGP4 calls instead of up to 120 linear steps.
|
||||
var aosLo = calendarTimeMillis - 60L * 1000L // elevation < 0 (below horizon)
|
||||
var aosHi = calendarTimeMillis // elevation >= 0 (above horizon)
|
||||
while (aosHi - aosLo > 500L) {
|
||||
val mid = (aosLo + aosHi) / 2
|
||||
if (sat.getElevation(pos, mid) < 0.0) aosLo = mid else aosHi = mid
|
||||
}
|
||||
calendarTimeMillis = aosHi
|
||||
|
||||
// Get full position for AOS data (azimuth, altitude)
|
||||
val aosPos = sat.getFullPosition(pos, calendarTimeMillis)
|
||||
@@ -262,13 +259,14 @@ class SatelliteRepo(
|
||||
if (elevation > maxElevation) maxElevation = elevation
|
||||
} while (elevation > 0.0)
|
||||
|
||||
// refine LOS to ~500ms precision
|
||||
calendarTimeMillis -= 30L * 1000L
|
||||
do {
|
||||
calendarTimeMillis += 500L
|
||||
elevation = sat.getElevation(pos, calendarTimeMillis)
|
||||
if (elevation > maxElevation) maxElevation = elevation
|
||||
} while (elevation > 0.0)
|
||||
// refine LOS to ~500ms precision via binary search (same monotonic argument)
|
||||
var losLo = calendarTimeMillis - 30L * 1000L // elevation > 0 (above horizon)
|
||||
var losHi = calendarTimeMillis // elevation <= 0 (below horizon)
|
||||
while (losHi - losLo > 500L) {
|
||||
val mid = (losLo + losHi) / 2
|
||||
if (sat.getElevation(pos, mid) > 0.0) losLo = mid else losHi = mid
|
||||
}
|
||||
calendarTimeMillis = losHi
|
||||
|
||||
// Get full position for LOS data (azimuth, altitude)
|
||||
val losPos = sat.getFullPosition(pos, calendarTimeMillis)
|
||||
|
||||
@@ -213,8 +213,8 @@ class SettingsRepo(
|
||||
}
|
||||
|
||||
private fun setStationPosition(latitude: Double, longitude: Double, altitude: Double, locator: String) {
|
||||
val newLat = latitude.round(4)
|
||||
val newLon = longitude.round(4)
|
||||
val newLat = latitude.round(5)
|
||||
val newLon = longitude.round(5)
|
||||
val newAlt = altitude.round(1)
|
||||
val timestamp = System.currentTimeMillis()
|
||||
println("Received new Position($newLat, $newLon, $newAlt) & Locator $locator")
|
||||
|
||||
+1
-1
@@ -61,7 +61,7 @@ interface ISatelliteRepo {
|
||||
suspend fun getTrack(sat: OrbitalObject, pos: GeoPos, start: Long, end: Long): List<OrbitalPos>
|
||||
|
||||
/** Get Doppler-shifted radio frequencies for a satellite at the given time. */
|
||||
suspend fun getRadios(sat: OrbitalObject, pos: GeoPos, radios: List<SatRadio>, time: Long): List<SatRadio>
|
||||
suspend fun getRadios(satPos: OrbitalPos, radios: List<SatRadio>): List<SatRadio>
|
||||
|
||||
/** Fetch radio transceivers for a satellite by its catalog number. */
|
||||
suspend fun getRadiosWithId(id: Int): List<SatRadio>
|
||||
|
||||
+62
-15
@@ -19,31 +19,78 @@ package com.rtbishop.look4sat.core.domain.utility
|
||||
|
||||
import com.rtbishop.look4sat.core.domain.predict.GeoPos
|
||||
|
||||
/**
|
||||
* Converts a Maidenhead locator (QTH grid square) to a GeoPos.
|
||||
* Supports 6-char (3 pair), 8-char (4 pair) and 10-char (5 pair) locators.
|
||||
* The returned position is the center of the finest cell encoded by the locator:
|
||||
* - 6 char: 5' lon x 2.5' lat cell center
|
||||
* - 8 char: 30" lon x 15" lat cell center
|
||||
* - 10 char: 1.25" lon x 0.625" lat cell center
|
||||
*/
|
||||
fun qthToPosition(locator: String): GeoPos? {
|
||||
val trimmedQth = locator.take(6)
|
||||
val trimmedQth = locator.trim().uppercase()
|
||||
if (!isValidLocator(trimmedQth)) return null
|
||||
val lonFirst = (trimmedQth[0].uppercaseChar().code - 65) * 20
|
||||
val latFirst = (trimmedQth[1].uppercaseChar().code - 65) * 10
|
||||
val lonFirst = (trimmedQth[0].code - 65) * 20
|
||||
val latFirst = (trimmedQth[1].code - 65) * 10
|
||||
val lonSecond = trimmedQth[2].toString().toInt() * 2
|
||||
val latSecond = trimmedQth[3].toString().toInt()
|
||||
val lonThird = (((trimmedQth[4].lowercaseChar().code - 97) / 12.0) + (1.0 / 24.0)) - 180
|
||||
val latThird = (((trimmedQth[5].lowercaseChar().code - 97) / 24.0) + (1.0 / 48.0)) - 90
|
||||
val longitude = (lonFirst + lonSecond + lonThird).round(4)
|
||||
val latitude = (latFirst + latSecond + latThird).round(4)
|
||||
return GeoPos(latitude, longitude)
|
||||
val lonThird = (trimmedQth[4].lowercaseChar().code - 97) / 12.0
|
||||
val latThird = (trimmedQth[5].lowercaseChar().code - 97) / 24.0
|
||||
var longitude = lonFirst + lonSecond + lonThird - 180
|
||||
var latitude = latFirst + latSecond + latThird - 90
|
||||
// 8-char extension: 4th pair, digits, 30" lon x 15" lat cells
|
||||
if (trimmedQth.length >= 8) {
|
||||
longitude += trimmedQth[6].toString().toInt() / 120.0
|
||||
latitude += trimmedQth[7].toString().toInt() / 240.0
|
||||
}
|
||||
// 10-char extension: 5th pair, letters, 1.25" lon x 0.625" lat cells
|
||||
if (trimmedQth.length >= 10) {
|
||||
longitude += (trimmedQth[8].lowercaseChar().code - 97) / 2880.0
|
||||
latitude += (trimmedQth[9].lowercaseChar().code - 97) / 5760.0
|
||||
}
|
||||
// Offset to the center of the finest encoded cell
|
||||
when (trimmedQth.length) {
|
||||
8 -> {
|
||||
longitude += 1.0 / 240.0
|
||||
latitude += 1.0 / 480.0
|
||||
}
|
||||
10 -> {
|
||||
longitude += 1.0 / 5760.0
|
||||
latitude += 1.0 / 11520.0
|
||||
}
|
||||
else -> {
|
||||
longitude += 1.0 / 24.0
|
||||
latitude += 1.0 / 48.0
|
||||
}
|
||||
}
|
||||
return GeoPos(latitude.round(6), longitude.round(6))
|
||||
}
|
||||
|
||||
fun positionToQth(latitude: Double, longitude: Double): String? {
|
||||
/**
|
||||
* Converts a GeoPos to a Maidenhead locator (QTH grid square).
|
||||
* Default precision is 8 characters (4 pairs) giving 30" lon x 15" lat resolution,
|
||||
* matching common 8-char grid square tools. Pass precision = 6 for the classic
|
||||
* 5' x 2.5' resolution, or precision = 10 for the finest 1.25" x 0.625" resolution.
|
||||
*/
|
||||
fun positionToQth(latitude: Double, longitude: Double, precision: Int = 8): String? {
|
||||
if (!isValidPosition(latitude, longitude)) return null
|
||||
val newLongitude = if (longitude > 180.0) longitude else longitude + 180
|
||||
val newLongitude = longitude + 180
|
||||
val newLatitude = latitude + 90
|
||||
val lonFirst = (65 + (newLongitude / 20)).toInt().toChar()
|
||||
val latFirst = (65 + (newLatitude / 10)).toInt().toChar()
|
||||
val lonSecond = ((newLongitude / 2) % 10).toInt()
|
||||
val lonFirst = (65 + (newLongitude / 20).toInt().coerceIn(0, 17)).toChar()
|
||||
val latFirst = (65 + (newLatitude / 10).toInt().coerceIn(0, 17)).toChar()
|
||||
val lonSecond = ((newLongitude % 20) / 2).toInt()
|
||||
val latSecond = (newLatitude % 10).toInt()
|
||||
val lonThird = (65 + (newLongitude % 2) * 12).toInt().toChar().lowercaseChar()
|
||||
val latThird = (65 + (newLatitude % 1) * 24).toInt().toChar().lowercaseChar()
|
||||
return "$lonFirst$latFirst$lonSecond$latSecond$lonThird$latThird"
|
||||
val qth = "$lonFirst$latFirst$lonSecond$latSecond$lonThird$latThird"
|
||||
if (precision < 8) return qth
|
||||
val lonFourth = ((newLongitude % (1.0 / 12.0)) * 120).toInt()
|
||||
val latFourth = ((newLatitude % (1.0 / 24.0)) * 240).toInt()
|
||||
val qth8 = "$qth$lonFourth$latFourth"
|
||||
if (precision < 10) return qth8
|
||||
val lonFifth = (65 + (newLongitude % (1.0 / 120.0)) * 2880).toInt().toChar().lowercaseChar()
|
||||
val latFifth = (65 + (newLatitude % (1.0 / 240.0)) * 5760).toInt().toChar().lowercaseChar()
|
||||
return "$qth8$lonFifth$latFifth"
|
||||
}
|
||||
|
||||
private fun isValidPosition(lat: Double, lon: Double): Boolean {
|
||||
@@ -51,5 +98,5 @@ private fun isValidPosition(lat: Double, lon: Double): Boolean {
|
||||
}
|
||||
|
||||
private fun isValidLocator(locator: String): Boolean {
|
||||
return locator.matches("[a-xA-X][a-xA-X]\\d\\d[a-xA-X][a-xA-X]".toRegex())
|
||||
return locator.matches("[a-xA-X]{2}\\d{2}[a-xA-X]{2}(?:\\d{2}(?:[a-xA-X]{2})?)?".toRegex())
|
||||
}
|
||||
@@ -26,21 +26,36 @@ class QthConverterTest {
|
||||
@Test
|
||||
fun `Given valid QTH returns correct POS`() {
|
||||
var result = qthToPosition("io91VL39FX")
|
||||
assert(result?.latitude == 51.4792 && result.longitude == -0.2083)
|
||||
assert(result?.latitude == 51.499913 && result.longitude == -0.22309)
|
||||
result = qthToPosition("gf15vc")
|
||||
assert(result?.latitude == -34.8958 && result.longitude == -56.2083)
|
||||
assert(result?.latitude == -34.895833 && result.longitude == -56.208333)
|
||||
// 8-char locators: finer 30" x 15" cell center
|
||||
result = qthToPosition("io91vl47")
|
||||
assert(result?.latitude == 51.489583 && result.longitude == -0.2125)
|
||||
result = qthToPosition("jn58td25")
|
||||
assert(result?.latitude == 48.147917 && result.longitude == 11.604167)
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `Given invalid QTH returns null`() {
|
||||
assert(qthToPosition("ZZ00zz") == null)
|
||||
assert(qthToPosition("JN58") == null)
|
||||
assert(qthToPosition("io9") == null)
|
||||
assert(qthToPosition("IO91VL7") == null)
|
||||
assert(qthToPosition("IO91VL4X") == null)
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `Given valid POS returns correct QTH`() {
|
||||
assert(positionToQth(51.4878, -0.2146) == "IO91vl")
|
||||
assert(positionToQth(48.1466, 11.6083) == "JN58td")
|
||||
// default precision is 8 chars
|
||||
assert(positionToQth(51.4878, -0.2146) == "IO91vl47")
|
||||
assert(positionToQth(48.1466, 11.6083) == "JN58td25")
|
||||
// 6-char precision still available for backwards compatibility
|
||||
assert(positionToQth(51.4878, -0.2146, 6) == "IO91vl")
|
||||
assert(positionToQth(48.1466, 11.6083, 6) == "JN58td")
|
||||
// 10-char precision
|
||||
assert(positionToQth(51.4878, -0.2146, 10) == "IO91vl47fb")
|
||||
assert(positionToQth(48.1466, 11.6083, 10) == "JN58td25xe")
|
||||
}
|
||||
|
||||
@Test
|
||||
@@ -48,4 +63,23 @@ class QthConverterTest {
|
||||
assert(positionToQth(91.0542, -170.1142) == null)
|
||||
assert(positionToQth(89.0542, -240.1142) == null)
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `Given boundary POS stays in valid grid`() {
|
||||
// antipodal / edge cases must not overflow the A-R / 0-9 / a-x alphabet
|
||||
assert(positionToQth(-90.0, -180.0, 8) == "AA00aa00")
|
||||
assert(positionToQth(90.0, 180.0, 8) == "RR00aa00")
|
||||
assert(positionToQth(0.0, 0.0, 8) == "JJ00aa00")
|
||||
// roundtrip stability: 8-char roundtrip is stable across a sample of positions
|
||||
val positions = listOf(
|
||||
Pair(51.4878, -0.2146), Pair(48.1466, 11.6083), Pair(-33.8688, 151.2093),
|
||||
Pair(39.9042, 116.4074), Pair(35.6895, 139.6917), Pair(41.714, -72.727)
|
||||
)
|
||||
positions.forEach { (lat, lon) ->
|
||||
val qth = positionToQth(lat, lon, 8)
|
||||
val pos = qthToPosition(qth!!)
|
||||
val qth2 = positionToQth(pos!!.latitude, pos.longitude, 8)
|
||||
assert(qth == qth2) { "Roundtrip failed for ($lat, $lon): $qth -> $qth2" }
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -199,7 +199,9 @@
|
||||
\n• Dave Moten (predict4java)
|
||||
\n• Alexandru Csete (Gpredict)
|
||||
\n• Dr T.S. Kelso (Celestrak)
|
||||
\n• Libre Space Foundation (SatNOGS)</string>
|
||||
\n• Libre Space Foundation (SatNOGS)
|
||||
\n• BA7OPF (pass matching feature)
|
||||
\n• BG7NTA</string>
|
||||
<string name="prefs_outro_license">Bu uygulama herhangi bir garanti sunmaz</string>
|
||||
|
||||
</resources>
|
||||
@@ -175,7 +175,9 @@
|
||||
\n• Alexandru Csete (Gpredict)
|
||||
\n• Dr T.S. Kelso (Celestrak)
|
||||
\n• Libre Space Foundation (SatNOGS)
|
||||
\n• xdsopl 和 Robot36 贡献者!</string>
|
||||
\n• xdsopl 和 Robot36 贡献者!
|
||||
\n• BA7OPF(过境匹配功能)
|
||||
\n• BG7NTA</string>
|
||||
<string name="prefs_outro_license">该应用程序不提供任何保修.</string>
|
||||
|
||||
</resources>
|
||||
@@ -1,7 +1,7 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<resources>
|
||||
<!-- Commmon -->
|
||||
<string name="app_name" translatable="false">Look4Sat</string>
|
||||
<string name="app_name" translatable="false">Look4Sat Pro</string>
|
||||
<string name="btn_accept">Accept</string>
|
||||
<string name="btn_cancel">Cancel</string>
|
||||
<string name="empty_list_message">No data to show</string>
|
||||
@@ -211,6 +211,8 @@
|
||||
\n* Dr T.S. Kelso (Celestrak)
|
||||
\n* Libre Space Foundation (SatNOGS)
|
||||
\n* xdsopl and Robot36 contributors!
|
||||
\n* BA7OPF (pass matching feature)
|
||||
\n* BG7NTA
|
||||
</string>
|
||||
<string name="prefs_outro_license">The app comes with no warranty</string>
|
||||
|
||||
|
||||
@@ -94,14 +94,21 @@ fun RadarViewCompose(
|
||||
animationSpec = infiniteRepeatable(tween(1000)),
|
||||
label = "animScale"
|
||||
)
|
||||
// Drive the sweep from the animation framework to eliminate state mutation inside the draw block
|
||||
val sweepTransition = rememberInfiniteTransition(label = "sweep")
|
||||
val sweepDegrees by sweepTransition.animateFloat(
|
||||
initialValue = 0f,
|
||||
targetValue = 360f,
|
||||
animationSpec = infiniteRepeatable(tween(SWEEP_DURATION_MS, easing = LinearEasing)),
|
||||
label = "sweepDegrees"
|
||||
)
|
||||
// Drive the sweep from the animation framework to eliminate state mutation inside the draw block.
|
||||
// Only create the infinite transition when the sweep is enabled — otherwise the canvas would
|
||||
// be invalidated every frame for an effect that isn't visible, wasting GPU and battery.
|
||||
val sweepDegrees = if (shouldShowSweep) {
|
||||
val sweepTransition = rememberInfiniteTransition(label = "sweep")
|
||||
val animatedSweep by sweepTransition.animateFloat(
|
||||
initialValue = 0f,
|
||||
targetValue = 360f,
|
||||
animationSpec = infiniteRepeatable(tween(SWEEP_DURATION_MS, easing = LinearEasing)),
|
||||
label = "sweepDegrees"
|
||||
)
|
||||
animatedSweep
|
||||
} else {
|
||||
0f
|
||||
}
|
||||
val measurer = rememberTextMeasurer()
|
||||
val sunPainter = painterResource(R.drawable.ic_sun)
|
||||
val moonPainter = painterResource(R.drawable.ic_moon)
|
||||
@@ -112,9 +119,10 @@ fun RadarViewCompose(
|
||||
var cachedSweepColor by remember { mutableStateOf(Color.Unspecified) }
|
||||
var trackPath by remember { mutableStateOf(Path()) }
|
||||
var trackEffect by remember { mutableStateOf(PathEffect.cornerPathEffect(0f)) }
|
||||
// Station-B overlay track (dashed)
|
||||
// Station-B overlay track (dashed, with the same directional arrow stamp as station A)
|
||||
var cachedTrackBRef by remember { mutableStateOf<List<OrbitalPos>?>(null) }
|
||||
var trackBPath by remember { mutableStateOf(Path()) }
|
||||
var trackBEffect by remember { mutableStateOf(PathEffect.cornerPathEffect(0f)) }
|
||||
// ShaderBrush is cached to avoid allocating a new GPU shader object every frame
|
||||
var cachedSweepBrush by remember { mutableStateOf<ShaderBrush?>(null) }
|
||||
|
||||
@@ -124,7 +132,10 @@ fun RadarViewCompose(
|
||||
if (radius != cachedRadius || items !== cachedItemsRef || trackB !== cachedTrackBRef) {
|
||||
trackPath = createTrackPath(items, radius)
|
||||
trackEffect = createTrackEffect(trackPath)
|
||||
trackBPath = trackB?.let { createTrackPath(it, radius) } ?: Path()
|
||||
val newTrackBPath = trackB?.let { createTrackPath(it, radius) } ?: Path()
|
||||
trackBPath = newTrackBPath
|
||||
trackBEffect = if (!newTrackBPath.isEmpty) createTrackEffect(newTrackBPath)
|
||||
else PathEffect.cornerPathEffect(0f)
|
||||
cachedSweepBrush = makeSweepBrush(center, primaryColor)
|
||||
cachedRadius = radius
|
||||
cachedItemsRef = items
|
||||
@@ -141,13 +152,14 @@ fun RadarViewCompose(
|
||||
drawElevationLabels(radius, primaryColor, measurer)
|
||||
translate(center.x, center.y) {
|
||||
drawTrack(trackPath, trackEffect, aimColor, primaryColor)
|
||||
// Station-B overlay: full dashed track + live position dot
|
||||
// (same display mode as the local station: track line + pulsing dot)
|
||||
// Station-B overlay: full dashed track + directional arrows + live position dot
|
||||
// (same display mode as the local station: track line/arrow + pulsing dot)
|
||||
if (trackB != null && trackB.isNotEmpty() && !trackBPath.isEmpty) {
|
||||
drawPath(
|
||||
trackBPath, trackBColor,
|
||||
style = Stroke(STROKE_WIDTH, pathEffect = PathEffect.dashPathEffect(floatArrayOf(18f, 12f)))
|
||||
)
|
||||
drawPath(trackBPath, trackBColor, style = Stroke(pathEffect = trackBEffect))
|
||||
}
|
||||
trackBPosition?.let { posB ->
|
||||
if (posB.elevation > 0) {
|
||||
|
||||
@@ -181,7 +181,7 @@ class RadarViewModel(
|
||||
orbitalPos = pos, sunPosition = cachedSunPos, moonPosition = cachedMoonPos
|
||||
)
|
||||
}
|
||||
processRadios(allRadios, pass.orbitalObject, timeNow)
|
||||
processRadios(allRadios, pos)
|
||||
sendPassData(pos)
|
||||
}
|
||||
|
||||
@@ -342,8 +342,8 @@ class RadarViewModel(
|
||||
}
|
||||
}
|
||||
|
||||
private suspend fun processRadios(radios: List<SatRadio>, orbitalObject: OrbitalObject, time: Long) {
|
||||
val transmitters = satelliteRepo.getRadios(orbitalObject, stationPos, radios, time)
|
||||
private suspend fun processRadios(radios: List<SatRadio>, satPos: OrbitalPos) {
|
||||
val transmitters = satelliteRepo.getRadios(satPos, radios)
|
||||
_uiState.update { state ->
|
||||
val freq = if (state.transceivers.selectedUuid != null) {
|
||||
val selectedRadio = transmitters.firstOrNull { it.uuid == state.transceivers.selectedUuid }
|
||||
|
||||
+2
@@ -339,6 +339,8 @@ private fun LocationCard(
|
||||
Row(horizontalArrangement = Arrangement.SpaceBetween, modifier = Modifier.fillMaxWidth()) {
|
||||
Text(text = "Lat: ${settings.stationPos.latitude}°")
|
||||
Text(text = "Lon: ${settings.stationPos.longitude}°")
|
||||
}
|
||||
Row(horizontalArrangement = Arrangement.SpaceBetween, modifier = Modifier.fillMaxWidth()) {
|
||||
Text(text = "Qth: ${settings.stationPos.qthLocator}")
|
||||
}
|
||||
Spacer(modifier = Modifier.height(1.dp))
|
||||
|
||||
@@ -1,8 +1,8 @@
|
||||
[versions]
|
||||
#noinspection UnusedVersionCatalogEntry
|
||||
appVersionCode = "443"
|
||||
appVersionCode = "445"
|
||||
#noinspection UnusedVersionCatalogEntry
|
||||
appVersionName = "4.4.3"
|
||||
appVersionName = "4.4.5"
|
||||
#noinspection UnusedVersionCatalogEntry
|
||||
compileSdk = "37"
|
||||
#noinspection UnusedVersionCatalogEntry
|
||||
@@ -11,6 +11,11 @@ minSdk = "24"
|
||||
jdkVersion = "21"
|
||||
#noinspection UnusedVersionCatalogEntry
|
||||
packageName = "com.rtbishop.look4sat"
|
||||
# Fork application ID: official app uses com.rtbishop.look4sat with a different
|
||||
# signing cert. Installing a fork with the same applicationId would conflict
|
||||
# with the official build (signature mismatch / cannot overwrite), so the fork
|
||||
# ships under its own applicationId suffix.
|
||||
applicationId = "com.rtbishop.look4sat.bg7nta"
|
||||
|
||||
android-gradle-plugin = "9.3.1"
|
||||
|
||||
|
||||
Reference in new issue
Block a user