fix: default station A grid, find all mutual passes, color toArgb()

- Pre-fill station A grid from settingsRepo.stationPosition
- Find ALL mutual passes per satellite (not just first)
- Fix Color.hashCode() -> Color.toArgb() for native canvas paint
- Use 2min gap between pass searches to avoid duplicates
This commit is contained in:
atsunatsu committed 2026-08-02 21:44:31 +08:00
1 parent 65f61a5147
commit 0bae306312
3 files changed
+76 -39

No files matched your search

@@ -36,7 +36,7 @@ import androidx.compose.runtime.remember
import androidx.compose.runtime.setValue
import androidx.compose.ui.Modifier
import androidx.compose.ui.geometry.Offset
import androidx.compose.ui.graphics.Color
import androidx.compose.ui.graphics.toArgb
import androidx.compose.ui.graphics.Path
import androidx.compose.ui.graphics.drawscope.Stroke
import androidx.compose.ui.graphics.nativeCanvas
@@ -66,6 +66,11 @@ fun ElevationCurveChart(
val gridColor = MaterialTheme.colorScheme.outlineVariant
val textColor = MaterialTheme.colorScheme.onSurfaceVariant
val onSurfaceColor = MaterialTheme.colorScheme.onSurface
val colorAArgb = colorA.toArgb()
val colorBArgb = colorB.toArgb()
val textColorArgb = textColor.toArgb()
val onSurfaceArgb = onSurfaceColor.toArgb()
val gridColorArgb = gridColor.toArgb()
val timeFormat = remember { SimpleDateFormat("HH:mm", Locale.getDefault()) }
var dragProgress by remember { mutableFloatStateOf(0.5f) }
@@ -113,7 +118,7 @@ fun ElevationCurveChart(
drawContext.canvas.nativeCanvas.drawText(
"$elev°", 2f, y + 4f,
android.graphics.Paint().apply {
color = textColor.hashCode()
color = textColorArgb
textSize = 24f
textAlign = android.graphics.Paint.Align.LEFT
}
@@ -128,7 +133,7 @@ fun ElevationCurveChart(
drawContext.canvas.nativeCanvas.drawText(
timeFormat.format(Date(t)), x - 20f, chartHeight - 2f,
android.graphics.Paint().apply {
color = textColor.hashCode()
color = textColorArgb
textSize = 22f
textAlign = android.graphics.Paint.Align.LEFT
}
@@ -176,7 +181,7 @@ fun ElevationCurveChart(
timeFormat.format(Date(dragTime)),
dragX - 24f, plotBottom + 16f,
android.graphics.Paint().apply {
color = onSurfaceColor.hashCode()
color = onSurfaceArgb
textSize = 24f
textAlign = android.graphics.Paint.Align.LEFT
}
@@ -185,7 +190,7 @@ fun ElevationCurveChart(
"A:${dragElevA}°",
dragX + 6f, plotTop + 16f,
android.graphics.Paint().apply {
color = colorA.hashCode()
color = colorAArgb
textSize = 24f
textAlign = android.graphics.Paint.Align.LEFT
}
@@ -194,14 +199,14 @@ fun ElevationCurveChart(
"B:${dragElevB}°",
dragX + 6f, plotTop + 42f,
android.graphics.Paint().apply {
color = colorB.hashCode()
color = colorBArgb
textSize = 24f
textAlign = android.graphics.Paint.Align.LEFT
}
)
}
// Legend (outside Canvas, in Composable context)
// Legend
Row(
modifier = Modifier.fillMaxWidth().padding(horizontal = 8.dp, vertical = 4.dp),
horizontalArrangement = Arrangement.SpaceEvenly
@@ -24,6 +24,7 @@ import com.rtbishop.look4sat.core.domain.predict.GeoPos
import com.rtbishop.look4sat.core.domain.predict.OrbitalObject
import com.rtbishop.look4sat.core.domain.repository.IMainContainer
import com.rtbishop.look4sat.core.domain.repository.ISatelliteRepo
import com.rtbishop.look4sat.core.domain.repository.ISettingsRepo
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.flow.MutableStateFlow
import kotlinx.coroutines.flow.StateFlow
@@ -32,6 +33,7 @@ import kotlinx.coroutines.flow.update
import kotlinx.coroutines.launch
import kotlinx.coroutines.withContext
import kotlin.math.PI
import kotlin.math.floor
import kotlin.math.roundToInt
data class MutualUiState(
@@ -51,12 +53,20 @@ data class MutualUiState(
)
class MutualViewModel(
private val satelliteRepo: ISatelliteRepo
private val satelliteRepo: ISatelliteRepo,
private val settingsRepo: ISettingsRepo
) : ViewModel() {
private val _uiState = MutableStateFlow(MutualUiState())
val uiState: StateFlow<MutualUiState> = _uiState.asStateFlow()
init {
// Pre-fill station A with the user's current station position (as grid)
val pos = settingsRepo.stationPosition.value
val grid = latLonToGrid(pos.latitude, pos.longitude)
_uiState.update { it.copy(stationAGrid = grid) }
}
fun onStationALat(value: String) = _uiState.update { it.copy(stationALat = value) }
fun onStationALon(value: String) = _uiState.update { it.copy(stationALon = value) }
fun onStationAGrid(value: String) = _uiState.update { it.copy(stationAGrid = value) }
@@ -110,27 +120,51 @@ class MutualViewModel(
/** Resolve a position from lat/lon text or grid square text. */
private fun resolvePosition(latText: String, lonText: String, gridText: String): GeoPos? {
// Try grid square first
val trimmed = gridText.trim().uppercase()
if (trimmed.length in listOf(4, 6, 8) && trimmed.all { it.isLetterOrDigit() }) {
return gridToLatLon(trimmed)
}
// Fall back to lat/lon
val lat = latText.toDoubleOrNull()
val lon = lonText.toDoubleOrNull()
return if (lat != null && lon != null) GeoPos(lat, lon) else null
}
/** Convert lat/lon to Maidenhead grid (6-char). */
private fun latLonToGrid(lat: Double, lon: Double): String {
var adjLon = (lon + 180.0) % 360.0
var adjLat = (lat + 90.0) % 180.0
val fieldLon = (adjLon / 20.0).toInt()
val fieldLat = (adjLat / 10.0).toInt()
adjLon -= fieldLon * 20.0
adjLat -= fieldLat * 10.0
val squareLon = (adjLon / 2.0).toInt()
val squareLat = (adjLat / 1.0).toInt()
adjLon -= squareLon * 2.0
adjLat -= squareLat * 1.0
val subLon = (adjLon * 60.0 / 5.0).toInt()
val subLat = (adjLat * 60.0 / 2.5).toInt()
return buildString {
append('A' + fieldLon)
append('A' + fieldLat)
append('0' + squareLon)
append('0' + squareLat)
append('A' + subLon)
append('A' + subLat)
}
}
/** Convert Maidenhead grid (4, 6, or 8 chars) to lat/lon center of the square. */
private fun gridToLatLon(grid: String): GeoPos? {
val g = grid.uppercase()
if (g.length < 4) return null
// First pair: letters A-R
val lonField = (g[0] - 'A').toDouble() * 20.0
val latField = (g[1] - 'A').toDouble() * 10.0
if (lonField < 0 || lonField > 340 || latField < 0 || latField > 170) return null
// Second pair: digits 0-9
val lonSquare = (g[2] - '0').toDouble() * 2.0
val latSquare = (g[3] - '0').toDouble() * 1.0
if (lonSquare < 0 || lonSquare > 18 || latSquare < 0 || latSquare > 9) return null
@@ -139,30 +173,27 @@ class MutualViewModel(
var lat = latField + latSquare
if (g.length >= 6) {
// Third pair: letters A-X
val lonSub = (g[4] - 'A').toDouble() * 5.0 / 60.0 // 5 minutes
val latSub = (g[5] - 'A').toDouble() * 2.5 / 60.0 // 2.5 minutes
val lonSub = (g[4] - 'A').toDouble() * 5.0 / 60.0
val latSub = (g[5] - 'A').toDouble() * 2.5 / 60.0
if (lonSub < 0 || lonSub > 115.0 / 60.0 || latSub < 0 || latSub > 57.5 / 60.0) return null
lon += lonSub
lat += latSub
if (g.length >= 8) {
// Fourth pair: digits 0-9
val lonExt = (g[6] - '0').toDouble() * 30.0 / 3600.0 // 30 seconds
val latExt = (g[7] - '0').toDouble() * 15.0 / 3600.0 // 15 seconds
val lonExt = (g[6] - '0').toDouble() * 30.0 / 3600.0
val latExt = (g[7] - '0').toDouble() * 15.0 / 3600.0
if (lonExt < 0 || lonExt > 270.0 / 3600.0 || latExt < 0 || latExt > 135.0 / 3600.0) return null
lon += lonExt + 15.0 / 3600.0 // center of the 30" square
lat += latExt + 7.5 / 3600.0 // center of the 15" square
lon += lonExt + 15.0 / 3600.0
lat += latExt + 7.5 / 3600.0
} else {
lon += 2.5 / 60.0 // center of the 5' square
lat += 1.25 / 60.0 // center of the 2.5' square
lon += 2.5 / 60.0
lat += 1.25 / 60.0
}
} else {
lon += 1.0 // center of the 2° square
lat += 0.5 // center of the 1° square
lon += 1.0
lat += 0.5
}
// Convert to signed lat/lon
lon = (lon + 180.0) % 360.0 - 180.0
lat = (lat + 90.0) % 180.0 - 90.0
return GeoPos(lat, lon)
@@ -176,13 +207,17 @@ class MutualViewModel(
): List<MutualPass> {
val results = mutableListOf<MutualPass>()
val endTime = time + hours * 60L * 60L * 1000L
val sampleInterval = 10_000L // 10 seconds between samples
val sampleInterval = 10_000L
for (sat in satellites) {
if (sat.data.meanmo < 1e-8) continue
val t = findNextMutualPass(sat, posA, posB, minElevADeg, minElevBDeg, time, endTime)
if (t != null) {
var searchStart = time
// Find ALL mutual passes for this satellite
while (true) {
val t = findNextMutualPass(sat, posA, posB, minElevADeg, minElevBDeg, searchStart, endTime)
if (t == null) break
val (start, end) = t
val samples = mutableListOf<Pair<Long, Pair<Double, Double>>>()
var maxElevA = 0.0
var maxElevB = 0.0
@@ -210,20 +245,17 @@ class MutualViewModel(
)
)
}
// Advance past this pass and keep searching
searchStart = end + 120_000L
}
}
return results
}
/** Get elevation in degrees. */
private fun elevationDeg(sat: OrbitalObject, pos: GeoPos, time: Long): Double {
return sat.getElevation(pos, time) * 180.0 / PI
}
/**
* Find the first common time window where a satellite is above both stations'
* minimum elevations. Uses 60-second coarse stepping.
*/
private fun findNextMutualPass(
sat: OrbitalObject,
posA: GeoPos, posB: GeoPos,
@@ -231,15 +263,13 @@ class MutualViewModel(
startTime: Long, endTime: Long
): Pair<Long, Long>? {
var t = startTime
val step = 60_000L // 60s coarse search step
val step = 60_000L
while (t < endTime) {
val elevA = elevationDeg(sat, posA, t)
val elevB = elevationDeg(sat, posB, t)
if (elevA > minElevADeg && elevB > minElevBDeg) {
// Both above horizon — find the common pass window
// Rewind to find AOS (either station drops below)
var aos = t
var rew = t
while (rew > startTime - 600_000L) {
@@ -252,7 +282,6 @@ class MutualViewModel(
rew -= step
}
// Fast-forward to find LOS (either station drops below)
var los = t
var fwd = t
while (fwd < endTime + 600_000L) {
@@ -278,7 +307,10 @@ class MutualViewModel(
fun factory(container: IMainContainer) = object : ViewModelProvider.Factory {
@Suppress("UNCHECKED_CAST")
override fun <T : ViewModel> create(modelClass: Class<T>): T =
MutualViewModel(satelliteRepo = container.satelliteRepo) as T
MutualViewModel(
satelliteRepo = container.satelliteRepo,
settingsRepo = container.settingsRepo
) as T
}
}
}