Added colored elevation and decay check to satellite passes

This commit is contained in:
Arty Bishop committed 2026-05-30 15:35:52 +01:00
1 parent 18c8ac1822
commit f7f5a73c4c
8 files changed
+213 -28

No files matched your search

@@ -146,17 +146,18 @@ class SatelliteRepo(
val passes = mutableListOf<OrbitalPass>() val passes = mutableListOf<OrbitalPass>()
val endDate = time + hours * 60L * 60L * 1000L val endDate = time + hours * 60L * 60L * 1000L
val quarterOrbitMin = (this.data.orbitalPeriod / 4.0).toInt() val quarterOrbitMin = (this.data.orbitalPeriod / 4.0).toInt()
val decayed = this.data.hasDecayed(time)
var startDate = time var startDate = time
var shouldRewind = true var shouldRewind = true
var lastAosDate: Long var lastAosDate: Long
var count = 0 var count = 0
if (this.willBeSeen(pos)) { if (this.willBeSeen(pos)) {
if (this.data.isDeepSpace) { if (this.data.isDeepSpace) {
passes.add(getGeoPass(this, pos, time)) passes.add(getGeoPass(this, pos, time, decayed))
} else { } else {
do { do {
if (count > 0) shouldRewind = false if (count > 0) shouldRewind = false
val pass = getLeoPass(this, pos, startDate, shouldRewind) val pass = getLeoPass(this, pos, startDate, shouldRewind, decayed)
lastAosDate = pass.aosTime lastAosDate = pass.aosTime
passes.add(pass) passes.add(pass)
startDate = pass.losTime + (quarterOrbitMin * 3) * 60L * 1000L startDate = pass.losTime + (quarterOrbitMin * 3) * 60L * 1000L
@@ -167,17 +168,17 @@ class SatelliteRepo(
return passes return passes
} }
private fun getGeoPass(sat: OrbitalObject, pos: GeoPos, time: Long): OrbitalPass { private fun getGeoPass(sat: OrbitalObject, pos: GeoPos, time: Long, decayed: Boolean): OrbitalPass {
val satPos = sat.getPosition(pos, time) val satPos = sat.getPosition(pos, time)
val aos = time - 24 * 60L * 60L * 1000L val aos = time - 24 * 60L * 60L * 1000L
val los = time + 24 * 60L * 60L * 1000L // val tca = (aos + los) / 2 val los = time + 24 * 60L * 60L * 1000L // val tca = (aos + los) / 2
val az = satPos.azimuth.toDegrees().round(1) val az = satPos.azimuth.toDegrees().round(1)
val elev = satPos.elevation.toDegrees().round(1) val elev = satPos.elevation.toDegrees().round(1)
val alt = satPos.altitude val alt = satPos.altitude
return OrbitalPass(aos, az, los, az, alt.toInt(), elev, sat) return OrbitalPass(aos, az, los, az, alt.toInt(), elev, sat, hasDecayed = decayed)
} }
private fun getLeoPass(sat: OrbitalObject, pos: GeoPos, time: Long, rewind: Boolean): OrbitalPass { private fun getLeoPass(sat: OrbitalObject, pos: GeoPos, time: Long, rewind: Boolean, decayed: Boolean): OrbitalPass {
val quarterOrbitMin = (sat.data.orbitalPeriod / 4.0).toInt() val quarterOrbitMin = (sat.data.orbitalPeriod / 4.0).toInt()
var calendarTimeMillis = time var calendarTimeMillis = time
var elevation: Double var elevation: Double
@@ -241,6 +242,6 @@ class SatelliteRepo(
val alt = tcaPos.altitude val alt = tcaPos.altitude
val elev = maxElevation.toDegrees().round(1) val elev = maxElevation.toDegrees().round(1)
return OrbitalPass(aos, aosAz, los, losAz, alt.toInt(), elev, sat) return OrbitalPass(aos, aosAz, los, losAz, alt.toInt(), elev, sat, hasDecayed = decayed)
} }
} }
@@ -27,7 +27,8 @@ data class OrbitalData(
val argper: Double, val argper: Double,
val meanan: Double, val meanan: Double,
val catnum: Int, val catnum: Int,
val bstar: Double val bstar: Double,
val ndot: Double = 0.0
) { ) {
val xincl: Double = incl * DEG2RAD val xincl: Double = incl * DEG2RAD
val xnodeo: Double = raan * DEG2RAD val xnodeo: Double = raan * DEG2RAD
@@ -37,4 +38,29 @@ data class OrbitalData(
val orbitalPeriod: Double = MIN_PER_DAY / meanmo val orbitalPeriod: Double = MIN_PER_DAY / meanmo
val isDeepSpace: Boolean = orbitalPeriod >= 225.0 // NearEarth (period < 225 min) or DeepSpace (period >= 225 min) val isDeepSpace: Boolean = orbitalPeriod >= 225.0 // NearEarth (period < 225 min) or DeepSpace (period >= 225 min)
fun getObject(): OrbitalObject = if (isDeepSpace) DeepSpaceObject(this) else NearEarthObject(this) fun getObject(): OrbitalObject = if (isDeepSpace) DeepSpaceObject(this) else NearEarthObject(this)
/** Check if satellite has likely decayed by the given time. */
fun hasDecayed(currentTimeMillis: Long): Boolean {
if (ndot == 0.0) return false
val currentDaynum = (currentTimeMillis - 315446400000L) / 86400000.0
val epochDaynum = epochToDaynum(epoch)
return CelestialComputer.hasDecayed(meanmo, ndot, epochDaynum, currentDaynum)
}
private fun epochToDaynum(epoch: Double): Double {
var year = kotlin.math.floor(epoch * 1E-3)
val day = (epoch * 1E-3 - year) * 1000.0
year = if (year < 57) year + 2000 else year + 1900
// daynum = days since 31 Dec 1979, Julian date of 31Dec79 = 2444238.5
val jan1Jd = julianDateOfYear(year)
return jan1Jd + day - 2444238.5
}
private fun julianDateOfYear(theYear: Double): Double {
val aYear = theYear - 1
val a = kotlin.math.floor(aYear / 100).toLong()
val b = 2 - a + a / 4
val i = kotlin.math.floor(365.25 * aYear).toLong()
return i + (30.6001 * 14).toLong() + 1720994.5 + b
}
} }
@@ -25,9 +25,10 @@ data class OrbitalPass(
val altitude: Int = 1000, val altitude: Int = 1000,
val maxElevation: Double = 75.0, val maxElevation: Double = 75.0,
val orbitalObject: OrbitalObject, val orbitalObject: OrbitalObject,
val progress: Float = 0.0f val progress: Float = 0.0f,
val hasDecayed: Boolean = false
) { ) {
val catNum: Int = orbitalObject.data.catnum val catNum: Int = orbitalObject.data.catnum
val name: String = orbitalObject.data.name val name: String = if (hasDecayed) "${orbitalObject.data.name} (decayed?)" else orbitalObject.data.name
val isDeepSpace: Boolean = orbitalObject.data.isDeepSpace val isDeepSpace: Boolean = orbitalObject.data.isDeepSpace
} }
@@ -82,7 +82,8 @@ class DataParser(private val dispatcher: CoroutineDispatcher) {
argper = values[7].toDouble(), argper = values[7].toDouble(),
meanan = values[8].toDouble(), meanan = values[8].toDouble(),
catnum = values[11].toInt(), catnum = values[11].toInt(),
bstar = values[14].toDouble() bstar = values[14].toDouble(),
ndot = values[15].toDouble()
) )
}.onFailure { println("CSV parsing exception: $it") }.getOrNull() }.onFailure { println("CSV parsing exception: $it") }.getOrNull()
@@ -99,13 +100,15 @@ class DataParser(private val dispatcher: CoroutineDispatcher) {
argper = line2.substring(34, 42).toDouble(), argper = line2.substring(34, 42).toDouble(),
meanan = line2.substring(43, 51).toDouble(), meanan = line2.substring(43, 51).toDouble(),
catnum = line1.substring(2, 7).trim().toInt(), catnum = line1.substring(2, 7).trim().toInt(),
bstar = 1e-5 * line1.substring(53, 59).toDouble() / 10.0.pow(line1.substring(60, 61).toDouble()) bstar = 1e-5 * line1.substring(53, 59).toDouble() / 10.0.pow(line1.substring(60, 61).toDouble()),
ndot = line1.substring(33, 43).trim().toDouble()
) )
}.onFailure { println("TLE parsing exception: $it") }.getOrNull() }.onFailure { println("TLE parsing exception: $it") }.getOrNull()
private fun getDayOfYear(year: Int, month: Int, dayOfMonth: Int): Int { fun isLeapYear(year: Int): Boolean = (year % 4 == 0 && year % 100 != 0) || year % 400 == 0
val isLeapYear = (year % 4 == 0 && year % 100 != 0) || year % 400 == 0
val daysInMonth = intArrayOf(31, if (isLeapYear) 29 else 28, 31, 30, 31, 30, 31, 31, 30, 31, 30, 31) fun getDayOfYear(year: Int, month: Int, dayOfMonth: Int): Int {
val daysInMonth = intArrayOf(31, if (isLeapYear(year)) 29 else 28, 31, 30, 31, 30, 31, 31, 30, 31, 30, 31)
return daysInMonth.take(month - 1).sum() + dayOfMonth return daysInMonth.take(month - 1).sum() + dayOfMonth
} }
} }
@@ -59,10 +59,53 @@ class DataParserTest {
@Test @Test
fun `Given valid CSV stream returns valid data`() = runTest(testDispatcher) { fun `Given valid CSV stream returns valid data`() = runTest(testDispatcher) {
val parsedList = dataParser.parseCSVStream(validCSVStream) val parsedList = dataParser.parseCSVStream(validCSVStream)
assert(parsedList.size == 2)
assert(parsedList[0].epoch == 21320.51955234) assert(parsedList[0].epoch == 21320.51955234)
assert(parsedList[1].epoch == 24069.23963816) assert(parsedList[1].epoch == 24069.23963816)
} }
@Test
fun `Given valid CSV stream all orbital fields are parsed correctly`() = runTest(testDispatcher) {
val csvStream = """
OBJECT_NAME,OBJECT_ID,EPOCH,MEAN_MOTION,ECCENTRICITY,INCLINATION,RA_OF_ASC_NODE,ARG_OF_PERICENTER,MEAN_ANOMALY,EPHEMERIS_TYPE,CLASSIFICATION_TYPE,NORAD_CAT_ID,ELEMENT_SET_NO,REV_AT_EPOCH,BSTAR,MEAN_MOTION_DOT,MEAN_MOTION_DDOT
ISS (ZARYA),1998-067A,2021-11-16T12:28:09.322176,15.48582035,.0004694,51.6447,309.4881,203.6966,299.8876,0,U,25544,999,31220,.31985E-4,.1288E-4,0
""".trimIndent().byteInputStream()
val sat = dataParser.parseCSVStream(csvStream)[0]
assert(sat.name == "ISS (ZARYA)")
assert(sat.catnum == 25544)
assert(sat.meanmo == 15.48582035)
assert(sat.eccn == 0.0004694)
assert(sat.incl == 51.6447)
assert(sat.raan == 309.4881)
assert(sat.argper == 203.6966)
assert(sat.meanan == 299.8876)
assert(sat.bstar == 0.31985E-4)
assert(sat.ndot == 0.1288E-4)
}
@Test
fun `Given valid CSV stream ndot is parsed for decay detection`() = runTest(testDispatcher) {
val csvStream = """
OBJECT_NAME,OBJECT_ID,EPOCH,MEAN_MOTION,ECCENTRICITY,INCLINATION,RA_OF_ASC_NODE,ARG_OF_PERICENTER,MEAN_ANOMALY,EPHEMERIS_TYPE,CLASSIFICATION_TYPE,NORAD_CAT_ID,ELEMENT_SET_NO,REV_AT_EPOCH,BSTAR,MEAN_MOTION_DOT,MEAN_MOTION_DDOT
ISS (ZARYA),1998-067A,2021-11-16T12:28:09.322176,15.48582035,.0004694,51.6447,309.4881,203.6966,299.8876,0,U,25544,999,31220,.31985E-4,.1288E-4,0
""".trimIndent().byteInputStream()
val sat = dataParser.parseCSVStream(csvStream)[0]
// ISS is healthy, should not be decayed even years later
assert(!sat.hasDecayed(System.currentTimeMillis()))
}
@Test
fun `Given CSV with high drag satellite detects decay`() = runTest(testDispatcher) {
// Simulate a satellite with high drag and old epoch that should have decayed
val csvStream = """
OBJECT_NAME,OBJECT_ID,EPOCH,MEAN_MOTION,ECCENTRICITY,INCLINATION,RA_OF_ASC_NODE,ARG_OF_PERICENTER,MEAN_ANOMALY,EPHEMERIS_TYPE,CLASSIFICATION_TYPE,NORAD_CAT_ID,ELEMENT_SET_NO,REV_AT_EPOCH,BSTAR,MEAN_MOTION_DOT,MEAN_MOTION_DDOT
DEBRIS,2020-001A,2020-01-15T00:00:00.000000,15.9,.001,51.0,100.0,200.0,300.0,0,U,99999,1,100,.5E-3,.05,0
""".trimIndent().byteInputStream()
val sat = dataParser.parseCSVStream(csvStream)[0]
// High mean motion (15.9) + high drag (.05) + old epoch → should be decayed by now
assert(sat.hasDecayed(System.currentTimeMillis()))
}
@Test @Test
fun `Given invalid CSV stream returns empty list`() = runTest(testDispatcher) { fun `Given invalid CSV stream returns empty list`() = runTest(testDispatcher) {
assert(dataParser.parseCSVStream(invalidCSVStream).isEmpty()) assert(dataParser.parseCSVStream(invalidCSVStream).isEmpty())
@@ -71,10 +114,41 @@ class DataParserTest {
@Test @Test
fun `Given valid TLE stream returns valid data`() = runTest(testDispatcher) { fun `Given valid TLE stream returns valid data`() = runTest(testDispatcher) {
val parsedList = dataParser.parseTLEStream(validTLEStream) val parsedList = dataParser.parseTLEStream(validTLEStream)
assert(parsedList.size == 2)
assert(parsedList[0].epoch == 21320.51955234) assert(parsedList[0].epoch == 21320.51955234)
assert(parsedList[1].epoch == 24069.23963816) assert(parsedList[1].epoch == 24069.23963816)
} }
@Test
fun `Given valid TLE stream all orbital fields are parsed correctly`() = runTest(testDispatcher) {
val tleStream = """
ISS (ZARYA)
1 25544U 98067A 21320.51955234 .00001288 00000+0 31985-4 0 9990
2 25544 51.6447 309.4881 0004694 203.6966 299.8876 15.48582035312205
""".trimIndent().byteInputStream()
val sat = dataParser.parseTLEStream(tleStream)[0]
assert(sat.name == "ISS (ZARYA)")
assert(sat.catnum == 25544)
assert(sat.meanmo == 15.48582035)
assert(sat.eccn == 0.0004694)
assert(sat.incl == 51.6447)
assert(sat.raan == 309.4881)
assert(sat.argper == 203.6966)
assert(sat.meanan == 299.8876)
assert(sat.ndot == 0.00001288)
}
@Test
fun `Given valid TLE stream ndot is parsed for decay detection`() = runTest(testDispatcher) {
val tleStream = """
ISS (ZARYA)
1 25544U 98067A 21320.51955234 .00001288 00000+0 31985-4 0 9990
2 25544 51.6447 309.4881 0004694 203.6966 299.8876 15.48582035312205
""".trimIndent().byteInputStream()
val sat = dataParser.parseTLEStream(tleStream)[0]
assert(!sat.hasDecayed(System.currentTimeMillis()))
}
@Test @Test
fun `Given invalid TLE stream returns empty list`() = runTest(testDispatcher) { fun `Given invalid TLE stream returns empty list`() = runTest(testDispatcher) {
assert(dataParser.parseTLEStream(invalidTLEStream).isEmpty()) assert(dataParser.parseTLEStream(invalidTLEStream).isEmpty())
@@ -85,6 +159,43 @@ class DataParserTest {
assert(dataParser.parseJSONStream(validJSONStream)[0].downlinkLow == 136658500L) assert(dataParser.parseJSONStream(validJSONStream)[0].downlinkLow == 136658500L)
} }
@Test
fun `Given valid JSON stream all radio fields are parsed correctly`() = runTest(testDispatcher) {
val jsonStream = """
[{"uuid":"UzPz4gcsNBPKPKAFPmer7g","description":"Upper side band (drifting)","alive":true,"type":"Transmitter","uplink_low":145900000,"uplink_high":146000000,"uplink_drift":null,"downlink_low":136658500,"downlink_high":136700000,"downlink_drift":null,"mode":"USB","mode_id":9,"uplink_mode":"FM","invert":true,"baud":null,"sat_id":"SCHX-0895-2361-9925-0309","norad_cat_id":965,"status":"active","updated":"2019-04-18T05:39:53.343316Z","citation":"CITATION NEEDED","service":"Unknown","coordination":"","coordination_url":""}]
""".trimIndent().byteInputStream()
val radio = dataParser.parseJSONStream(jsonStream)[0]
assert(radio.uuid == "UzPz4gcsNBPKPKAFPmer7g")
assert(radio.info == "Upper side band (drifting)")
assert(radio.isAlive)
assert(radio.downlinkLow == 136658500L)
assert(radio.downlinkHigh == 136700000L)
assert(radio.downlinkMode == "USB")
assert(radio.uplinkLow == 145900000L)
assert(radio.uplinkHigh == 146000000L)
assert(radio.uplinkMode == "FM")
assert(radio.isInverted)
assert(radio.catnum == 965)
}
@Test
fun `Given JSON with null optional fields parses without error`() = runTest(testDispatcher) {
val jsonStream = """
[{"uuid":"abc123","description":"Beacon","alive":false,"type":"Transmitter","uplink_low":null,"uplink_high":null,"uplink_drift":null,"downlink_low":145800000,"downlink_high":null,"downlink_drift":null,"mode":null,"mode_id":null,"uplink_mode":null,"invert":false,"baud":null,"sat_id":"TEST","norad_cat_id":12345,"status":"active","updated":"2024-01-01T00:00:00Z","citation":"","service":"Unknown","coordination":"","coordination_url":""}]
""".trimIndent().byteInputStream()
val radio = dataParser.parseJSONStream(jsonStream)[0]
assert(radio.uuid == "abc123")
assert(!radio.isAlive)
assert(radio.downlinkLow == 145800000L)
assert(radio.downlinkHigh == null)
assert(radio.downlinkMode == null)
assert(radio.uplinkLow == null)
assert(radio.uplinkHigh == null)
assert(radio.uplinkMode == null)
assert(!radio.isInverted)
assert(radio.catnum == 12345)
}
@Test @Test
fun `Given invalid JSON stream returns empty list`() = runTest(testDispatcher) { fun `Given invalid JSON stream returns empty list`() = runTest(testDispatcher) {
assert(dataParser.parseJSONStream(invalidJSONStream).isEmpty()) assert(dataParser.parseJSONStream(invalidJSONStream).isEmpty())
@@ -95,11 +206,37 @@ class DataParserTest {
assert(dataParser.parseCSVStream(validCSVStream) == dataParser.parseTLEStream(validTLEStream)) assert(dataParser.parseCSVStream(validCSVStream) == dataParser.parseTLEStream(validTLEStream))
} }
// @Test @Test
// fun `Function isLeapYear returns correct data`() = runTest(testDispatcher) { fun `isLeapYear returns correct results`() {
// val years = listOf(1900, 1984, 1994, 2016, 2022, 2024, 2042, 2048) val years = listOf(1900, 1984, 1994, 2000, 2016, 2022, 2024, 2042, 2048, 2100)
// val answers = listOf(false, true, false, true, false, true, false, true) val expected = listOf(false, true, false, true, true, false, true, false, true, false)
// val results = years.map { dataParser.isLeapYear(it) } val results = years.map { dataParser.isLeapYear(it) }
// assert(results == answers) assert(results == expected)
// } }
@Test
fun `getDayOfYear returns correct day for January 1st`() {
assert(dataParser.getDayOfYear(2024, 1, 1) == 1)
assert(dataParser.getDayOfYear(2023, 1, 1) == 1)
}
@Test
fun `getDayOfYear returns correct day for March 1st in leap and non-leap years`() {
// 2024 is leap: Jan(31) + Feb(29) + 1 = 61
assert(dataParser.getDayOfYear(2024, 3, 1) == 61)
// 2023 is not leap: Jan(31) + Feb(28) + 1 = 60
assert(dataParser.getDayOfYear(2023, 3, 1) == 60)
}
@Test
fun `getDayOfYear returns correct day for December 31st`() {
assert(dataParser.getDayOfYear(2024, 12, 31) == 366) // leap year
assert(dataParser.getDayOfYear(2023, 12, 31) == 365) // non-leap year
}
@Test
fun `getDayOfYear returns correct day for November 16th`() {
// Matches the CSV test data epoch: 2021-11-16 → day 320
assert(dataParser.getDayOfYear(2021, 11, 16) == 320)
}
} }
@@ -48,6 +48,7 @@ import androidx.compose.runtime.Composable
import androidx.compose.runtime.remember import androidx.compose.runtime.remember
import androidx.compose.ui.Alignment import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier import androidx.compose.ui.Modifier
import androidx.compose.ui.graphics.Color
import androidx.compose.ui.res.painterResource import androidx.compose.ui.res.painterResource
import androidx.compose.ui.res.stringResource import androidx.compose.ui.res.stringResource
import androidx.compose.ui.text.font.FontWeight import androidx.compose.ui.text.font.FontWeight
@@ -151,16 +152,17 @@ fun RowScope.NextPassRow(pass: OrbitalPass, modifier: Modifier = Modifier, isUtc
maxLines = 1, maxLines = 1,
overflow = TextOverflow.Ellipsis overflow = TextOverflow.Ellipsis
) )
val elevColor = elevationColor(pass.maxElevation)
Icon( Icon(
painter = painterResource(R.drawable.ic_elevation), painter = painterResource(R.drawable.ic_elevation),
contentDescription = null, contentDescription = null,
tint = MaterialTheme.colorScheme.primary, tint = elevColor,
modifier = Modifier.size(16.dp) modifier = Modifier.size(16.dp)
) )
Spacer(modifier = Modifier.width(4.dp)) Spacer(modifier = Modifier.width(4.dp))
Text( Text(
text = "${pass.maxElevation}°", text = "${pass.maxElevation}°",
color = MaterialTheme.colorScheme.primary color = elevColor
) )
} }
Row( Row(
@@ -379,3 +381,12 @@ fun TopBar(
TopBar { startAction(); topInfo(); bottomInfo(); endAction() } TopBar { startAction(); topInfo(); bottomInfo(); endAction() }
} }
} }
@Composable
fun elevationColor(elevation: Double): Color {
return when {
elevation < 15.0 -> Color(0xFFEF5350) // soft red for low elevation
elevation < 45.0 -> MaterialTheme.colorScheme.primary // accent yellow for normal
else -> Color(0xFF66BB6A) // soft green for high elevation
}
}
@@ -43,6 +43,7 @@ import androidx.compose.runtime.remember
import androidx.compose.runtime.setValue import androidx.compose.runtime.setValue
import androidx.compose.ui.Alignment import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier import androidx.compose.ui.Modifier
import androidx.compose.ui.graphics.Color
import androidx.compose.ui.res.painterResource import androidx.compose.ui.res.painterResource
import androidx.compose.ui.res.stringResource import androidx.compose.ui.res.stringResource
import androidx.compose.ui.text.font.FontWeight import androidx.compose.ui.text.font.FontWeight
@@ -54,6 +55,7 @@ import com.rtbishop.look4sat.core.presentation.LocalSpacing
import com.rtbishop.look4sat.core.presentation.MainTheme import com.rtbishop.look4sat.core.presentation.MainTheme
import com.rtbishop.look4sat.core.presentation.R import com.rtbishop.look4sat.core.presentation.R
import com.rtbishop.look4sat.core.presentation.SharedDialog import com.rtbishop.look4sat.core.presentation.SharedDialog
import com.rtbishop.look4sat.core.presentation.elevationColor
private val allModes = listOf( private val allModes = listOf(
"AFSK", "AFSK S-Net", "AFSK SALSAT", "AHRPT", "AM", "APT", "BPSK", "BPSK PMT-A3", "AFSK", "AFSK S-Net", "AFSK SALSAT", "AHRPT", "AM", "APT", "BPSK", "BPSK PMT-A3",
@@ -96,7 +98,8 @@ internal fun PassesDialog(
value = elevationValueNew.doubleValue, value = elevationValueNew.doubleValue,
displayValue = "${elevationValueNew.doubleValue.toInt()}°", displayValue = "${elevationValueNew.doubleValue.toInt()}°",
valueResId = R.drawable.ic_elevation, valueResId = R.drawable.ic_elevation,
valueRange = 0f..60f valueRange = 0f..60f,
accentColor = elevationColor(elevationValueNew.doubleValue)
) { elevationValueNew.doubleValue = it.toDouble() } ) { elevationValueNew.doubleValue = it.toDouble() }
SliderRow( SliderRow(
title = stringResource(R.string.pass_filter_hours), title = stringResource(R.string.pass_filter_hours),
@@ -117,6 +120,7 @@ private fun SliderRow(
valueResId: Int, valueResId: Int,
valueRange: ClosedFloatingPointRange<Float>, valueRange: ClosedFloatingPointRange<Float>,
steps: Int = 0, steps: Int = 0,
accentColor: Color = MaterialTheme.colorScheme.primary,
onChange: (Float) -> Unit onChange: (Float) -> Unit
) { ) {
Column( Column(
@@ -137,14 +141,14 @@ private fun SliderRow(
Icon( Icon(
painter = painterResource(id = valueResId), painter = painterResource(id = valueResId),
contentDescription = null, contentDescription = null,
tint = MaterialTheme.colorScheme.primary, tint = accentColor,
modifier = Modifier.size(20.dp) modifier = Modifier.size(20.dp)
) )
Text( Text(
text = displayValue, text = displayValue,
fontSize = 18.sp, fontSize = 18.sp,
fontWeight = FontWeight.Medium, fontWeight = FontWeight.Medium,
color = MaterialTheme.colorScheme.primary color = accentColor
) )
} }
Slider(value = value.toFloat(), onValueChange = onChange, valueRange = valueRange, steps = steps) Slider(value = value.toFloat(), onValueChange = onChange, valueRange = valueRange, steps = steps)
@@ -75,6 +75,7 @@ import com.rtbishop.look4sat.core.presentation.ScreenColumn
import com.rtbishop.look4sat.core.presentation.SwipeableItem import com.rtbishop.look4sat.core.presentation.SwipeableItem
import com.rtbishop.look4sat.core.presentation.TimerRow import com.rtbishop.look4sat.core.presentation.TimerRow
import com.rtbishop.look4sat.core.presentation.TopBar import com.rtbishop.look4sat.core.presentation.TopBar
import com.rtbishop.look4sat.core.presentation.elevationColor
import com.rtbishop.look4sat.core.presentation.infiniteMarquee import com.rtbishop.look4sat.core.presentation.infiniteMarquee
import com.rtbishop.look4sat.core.presentation.isVerticalLayout import com.rtbishop.look4sat.core.presentation.isVerticalLayout
import java.text.SimpleDateFormat import java.text.SimpleDateFormat
@@ -331,16 +332,17 @@ private fun PassItem(
overflow = TextOverflow.Ellipsis, overflow = TextOverflow.Ellipsis,
color = MaterialTheme.colorScheme.onSurface color = MaterialTheme.colorScheme.onSurface
) )
val elevColor = elevationColor(pass.maxElevation)
Icon( Icon(
painter = painterResource(id = R.drawable.ic_elevation), painter = painterResource(id = R.drawable.ic_elevation),
contentDescription = null, contentDescription = null,
tint = MaterialTheme.colorScheme.primary, tint = elevColor,
modifier = Modifier.size(16.dp) modifier = Modifier.size(16.dp)
) )
Spacer(modifier = Modifier.width(4.dp)) Spacer(modifier = Modifier.width(4.dp))
Text( Text(
text = "${pass.maxElevation}°", text = "${pass.maxElevation}°",
color = MaterialTheme.colorScheme.primary color = elevColor
) )
} }
Row( Row(