Fixes and refactoring for LocationSource

This commit is contained in:
Arty Bishop committed 2025-12-27 15:01:33 +00:00
1 parent 12fabc8f23
commit 542cfcfd50
19 files changed
+125 -95

No files matched your search

@@ -18,73 +18,67 @@
package com.rtbishop.look4sat.framework package com.rtbishop.look4sat.framework
import android.location.Criteria import android.location.Criteria
import android.location.Location
import android.location.LocationManager import android.location.LocationManager
import androidx.core.location.LocationListenerCompat
import androidx.core.location.LocationManagerCompat import androidx.core.location.LocationManagerCompat
import androidx.core.os.CancellationSignal
import com.rtbishop.look4sat.domain.ILocationSource import com.rtbishop.look4sat.domain.ILocationSource
import com.rtbishop.look4sat.domain.ISettingsSource import com.rtbishop.look4sat.domain.ISettingsSource
import com.rtbishop.look4sat.model.StationPos import com.rtbishop.look4sat.model.GeoPos
import com.rtbishop.look4sat.utility.QthConverter import com.rtbishop.look4sat.utility.QthConverter
import com.rtbishop.look4sat.utility.round import com.rtbishop.look4sat.utility.round
import kotlinx.coroutines.flow.MutableStateFlow import kotlinx.coroutines.flow.MutableStateFlow
import kotlinx.coroutines.flow.StateFlow import kotlinx.coroutines.flow.StateFlow
import timber.log.Timber import timber.log.Timber
import java.util.concurrent.Executors
class LocationSource( class LocationSource(
private val manager: LocationManager, private val manager: LocationManager,
private val settings: ISettingsSource, private val settings: ISettingsSource,
) : ILocationSource, LocationListenerCompat { ) : ILocationSource {
private val defaultProvider = LocationManager.PASSIVE_PROVIDER
private val _stationPosition = MutableStateFlow(settings.loadStationPosition()) private val _stationPosition = MutableStateFlow(settings.loadStationPosition())
override val stationPosition: StateFlow<StationPos> = _stationPosition private val defaultProvider = LocationManager.PASSIVE_PROVIDER
private val executor = Executors.newSingleThreadExecutor()
private val timeoutSignal = CancellationSignal().apply {
setOnCancelListener { _stationPosition.value = _stationPosition.value }
}
override val stationPosition: StateFlow<GeoPos> = _stationPosition
override fun setGpsPosition(): Boolean { override fun setGpsPosition(): Boolean {
if (!LocationManagerCompat.isLocationEnabled(manager)) return false if (!LocationManagerCompat.isLocationEnabled(manager)) return false
val criteria = Criteria().apply { isCostAllowed = true } try {
val bestProvider = manager.getBestProvider(criteria, true) ?: defaultProvider val criteria = Criteria().apply { isCostAllowed = true }
forceLocationUpdate(bestProvider) val provider = manager.getBestProvider(criteria, true) ?: defaultProvider
Timber.d("Requesting location for $provider provider")
LocationManagerCompat.getCurrentLocation(manager, provider, timeoutSignal, executor) {
it?.let { setGeoPosition(it.latitude, it.longitude, it.altitude) }
}
} catch (exception: SecurityException) {
Timber.d("No permissions were given")
}
return true return true
} }
override fun setGeoPosition(latitude: Double, longitude: Double): Boolean { override fun setGeoPosition(latitude: Double, longitude: Double, altitude: Double): Boolean {
manager.removeUpdates(this)
val newLongitude = if (longitude > 180.0) longitude - 180 else longitude val newLongitude = if (longitude > 180.0) longitude - 180 else longitude
val locator = QthConverter.positionToQth(latitude, newLongitude) ?: return false val locator = QthConverter.positionToQth(latitude, newLongitude) ?: return false
saveStationPosition(latitude, newLongitude, locator) saveGeoPos(latitude, newLongitude, altitude, locator)
return true return true
} }
override fun setQthPosition(locator: String): Boolean { override fun setQthPosition(locator: String): Boolean {
val position = QthConverter.qthToPosition(locator) ?: return false val position = QthConverter.qthToPosition(locator) ?: return false
saveStationPosition(position.lat, position.lon, locator) saveGeoPos(position.latitude, position.longitude, 0.0, locator)
return true return true
} }
override fun onLocationChanged(location: Location) { private fun saveGeoPos(latitude: Double, longitude: Double, altitude: Double, locator: String) {
setGeoPosition(location.latitude, location.longitude)
}
private fun forceLocationUpdate(provider: String) {
try {
val location = manager.getLastKnownLocation(defaultProvider)
if (location == null || System.currentTimeMillis() - location.time > 600000) {
Timber.d("Requesting $provider location update")
manager.requestLocationUpdates(provider, 0L, 0f, this)
} else {
setGeoPosition(location.latitude, location.longitude)
}
} catch (exception: SecurityException) {
Timber.d("No permissions were given")
}
}
private fun saveStationPosition(latitude: Double, longitude: Double, locator: String) {
val newLat = latitude.round(4) val newLat = latitude.round(4)
val newLon = longitude.round(4) val newLon = longitude.round(4)
Timber.d("Received new Position($newLat, $newLon) & Locator $locator") val newAlt = altitude.round(1)
_stationPosition.value = StationPos(newLat, newLon, locator, System.currentTimeMillis()) val timestamp = System.currentTimeMillis()
Timber.d("Received new Position($newLat, $newLon, $newAlt) & Locator $locator")
_stationPosition.value = GeoPos(newLat, newLon, newAlt, locator, timestamp)
settings.saveStationPosition(stationPosition.value) settings.saveStationPosition(stationPosition.value)
} }
} }
@@ -20,7 +20,10 @@ package com.rtbishop.look4sat.framework
import android.content.SharedPreferences import android.content.SharedPreferences
import androidx.core.content.edit import androidx.core.content.edit
import com.rtbishop.look4sat.domain.ISettingsSource import com.rtbishop.look4sat.domain.ISettingsSource
import com.rtbishop.look4sat.model.StationPos import com.rtbishop.look4sat.model.GeoPos
import kotlinx.coroutines.flow.Flow
import kotlinx.coroutines.flow.MutableStateFlow
import kotlinx.coroutines.flow.onCompletion
class SettingsSource(private val prefs: SharedPreferences) : ISettingsSource { class SettingsSource(private val prefs: SharedPreferences) : ISettingsSource {
@@ -42,22 +45,25 @@ class SettingsSource(private val prefs: SharedPreferences) : ISettingsSource {
const val keyBTFormat = "BTFormat" const val keyBTFormat = "BTFormat"
const val keyLatitude = "stationLat" const val keyLatitude = "stationLat"
const val keyLongitude = "stationLon" const val keyLongitude = "stationLon"
const val keyAltitude = "stationAlt"
const val keyLocator = "stationQTH" const val keyLocator = "stationQTH"
const val keyLocTimestamp = "locTimestamp" const val keyLocTimestamp = "locTimestamp"
const val keySelection = "selection" const val keySelection = "selection"
} }
override fun loadStationPosition(): StationPos { override fun loadStationPosition(): GeoPos {
val latitude = prefs.getString(keyLatitude, null) ?: "0.0" val latitude = (prefs.getString(keyLatitude, null) ?: "0.0").toDouble()
val longitude = prefs.getString(keyLongitude, null) ?: "0.0" val longitude = (prefs.getString(keyLongitude, null) ?: "0.0").toDouble()
val altitude = (prefs.getString(keyAltitude, null) ?: "0.0").toDouble()
val qthLocator = prefs.getString(keyLocator, null) ?: "null" val qthLocator = prefs.getString(keyLocator, null) ?: "null"
val timestamp = prefs.getLong(keyLocTimestamp, 0L) val timestamp = prefs.getLong(keyLocTimestamp, 0L)
return StationPos(latitude.toDouble(), longitude.toDouble(), qthLocator, timestamp) return GeoPos(latitude, longitude, altitude, qthLocator, timestamp)
} }
override fun saveStationPosition(stationPos: StationPos) = prefs.edit { override fun saveStationPosition(stationPos: GeoPos) = prefs.edit {
putString(keyLatitude, stationPos.latitude.toString()) putString(keyLatitude, stationPos.latitude.toString())
putString(keyLongitude, stationPos.longitude.toString()) putString(keyLongitude, stationPos.longitude.toString())
putString(keyAltitude, stationPos.altitude.toString())
putString(keyLocator, stationPos.qthLocator) putString(keyLocator, stationPos.qthLocator)
putLong(keyLocTimestamp, stationPos.timestamp) putLong(keyLocTimestamp, stationPos.timestamp)
} }
@@ -209,4 +215,24 @@ class SettingsSource(private val prefs: SharedPreferences) : ISettingsSource {
private fun SharedPreferences.Editor.putDouble(key: String, double: Double) { private fun SharedPreferences.Editor.putDouble(key: String, double: Double) {
putLong(key, double.toRawBits()) putLong(key, double.toRawBits())
} }
inline fun <reified T> SharedPreferences.observeKey(key: String, default: T): Flow<T> {
val flow = MutableStateFlow(getItem(key, default))
val listener = SharedPreferences.OnSharedPreferenceChangeListener { _, k ->
if (key == k) flow.value = getItem(key, default)
}
registerOnSharedPreferenceChangeListener(listener)
return flow.onCompletion { unregisterOnSharedPreferenceChangeListener(listener) }
}
inline fun <reified T> SharedPreferences.getItem(key: String, default: T): T {
return when (default) {
is Boolean -> getBoolean(key, default) as T
is Int -> getInt(key, default) as T
is Long -> getLong(key, default) as T
is Float -> getFloat(key, default) as T
is String -> getString(key, default) as T
else -> throw IllegalArgumentException("Could not handle ${T::class.java.name}")
}
}
} }
@@ -6,10 +6,6 @@ import androidx.compose.foundation.clickable
import androidx.compose.foundation.layout.* import androidx.compose.foundation.layout.*
import androidx.compose.foundation.lazy.LazyColumn import androidx.compose.foundation.lazy.LazyColumn
import androidx.compose.foundation.lazy.items import androidx.compose.foundation.lazy.items
import androidx.compose.material.icons.Icons
import androidx.compose.material.icons.outlined.Close
import androidx.compose.material.icons.outlined.PlayArrow
import androidx.compose.material.icons.outlined.Search
import androidx.compose.material3.* import androidx.compose.material3.*
import androidx.compose.runtime.Composable import androidx.compose.runtime.Composable
import androidx.compose.runtime.collectAsState import androidx.compose.runtime.collectAsState
@@ -96,7 +92,8 @@ private fun TopBar(setQuery: (String) -> Unit, saveSelection: () -> Unit) {
@Composable @Composable
private fun SearchBar(setQuery: (String) -> Unit, modifier: Modifier = Modifier) { private fun SearchBar(setQuery: (String) -> Unit, modifier: Modifier = Modifier) {
val currentQuery = rememberSaveable { mutableStateOf("") } val currentQuery = rememberSaveable { mutableStateOf("") }
OutlinedTextField(value = currentQuery.value, OutlinedTextField(
value = currentQuery.value,
onValueChange = { newValue -> onValueChange = { newValue ->
currentQuery.value = newValue currentQuery.value = newValue
setQuery(newValue) setQuery(newValue)
@@ -104,9 +101,14 @@ private fun SearchBar(setQuery: (String) -> Unit, modifier: Modifier = Modifier)
maxLines = 1, maxLines = 1,
// label = { Text(text = stringResource(id = R.string.entries_search_hint)) }, // label = { Text(text = stringResource(id = R.string.entries_search_hint)) },
placeholder = { Text(text = stringResource(id = R.string.entries_search_hint)) }, placeholder = { Text(text = stringResource(id = R.string.entries_search_hint)) },
leadingIcon = { Icon(imageVector = Icons.Outlined.Search, contentDescription = null) }, leadingIcon = {
Icon(
painter = painterResource(id = R.drawable.ic_search),
contentDescription = null
)
},
trailingIcon = { trailingIcon = {
Icon(imageVector = Icons.Outlined.Close, Icon(painter = painterResource(id = R.drawable.ic_close),
contentDescription = null, contentDescription = null,
modifier = Modifier.onClick { modifier = Modifier.onClick {
currentQuery.value = "" currentQuery.value = ""
@@ -150,7 +152,7 @@ private fun EntryTypeCard(type: String, onClick: () -> Unit, modifier: Modifier
.height(48.dp) .height(48.dp)
.clickable { onClick() }) { .clickable { onClick() }) {
Icon( Icon(
imageVector = Icons.Outlined.PlayArrow, painter = painterResource(id = R.drawable.ic_next),
contentDescription = null, contentDescription = null,
modifier = Modifier modifier = Modifier
.size(36.dp) .size(36.dp)
@@ -89,7 +89,7 @@ private fun setStationPosition(stationPos: GeoPos, mapView: MapView) {
setInfoWindow(null) setInfoWindow(null)
setAnchor(Marker.ANCHOR_CENTER, Marker.ANCHOR_BOTTOM) setAnchor(Marker.ANCHOR_CENTER, Marker.ANCHOR_BOTTOM)
icon = ContextCompat.getDrawable(mapView.context, R.drawable.ic_position) icon = ContextCompat.getDrawable(mapView.context, R.drawable.ic_position)
position = GeoPoint(stationPos.lat, stationPos.lon) position = GeoPoint(stationPos.latitude, stationPos.longitude)
mapView.overlays[0] = this mapView.overlays[0] = this
mapView.invalidate() mapView.invalidate()
} }
@@ -109,7 +109,7 @@ private fun setPositions(
setAnchor(Marker.ANCHOR_CENTER, Marker.ANCHOR_CENTER) setAnchor(Marker.ANCHOR_CENTER, Marker.ANCHOR_CENTER)
icon = getCustomTextIcon(it.key.data.name, mapView) icon = getCustomTextIcon(it.key.data.name, mapView)
try { try {
position = GeoPoint(it.value.lat, it.value.lon) position = GeoPoint(it.value.latitude, it.value.longitude)
} catch (exception: IllegalArgumentException) { } catch (exception: IllegalArgumentException) {
println(exception.stackTraceToString()) println(exception.stackTraceToString())
} }
@@ -145,7 +145,7 @@ private fun setSatelliteTrack(satTrack: List<List<GeoPos>>, mapView: MapView) {
val trackOverlay = FolderOverlay() val trackOverlay = FolderOverlay()
try { try {
satTrack.forEach { track -> satTrack.forEach { track ->
val trackPoints = track.map { GeoPoint(it.lat, it.lon) } val trackPoints = track.map { GeoPoint(it.latitude, it.longitude) }
Polyline().apply { Polyline().apply {
setPoints(trackPoints) setPoints(trackPoints)
outlinePaint.set(trackPaint) outlinePaint.set(trackPaint)
@@ -160,7 +160,7 @@ private fun setSatelliteTrack(satTrack: List<List<GeoPos>>, mapView: MapView) {
} }
private fun setFootprint(satPos: SatPos, mapView: MapView) { private fun setFootprint(satPos: SatPos, mapView: MapView) {
val footprintPoints = satPos.getRangeCircle().map { GeoPoint(it.lat, it.lon) } val footprintPoints = satPos.getRangeCircle().map { GeoPoint(it.latitude, it.longitude) }
try { try {
val footprintOverlay = Polyline().apply { val footprintOverlay = Polyline().apply {
outlinePaint.set(footprintPaint) outlinePaint.set(footprintPaint)
@@ -53,8 +53,8 @@ class MapViewModel @Inject constructor(
private var selectedSatellite: Satellite? = null private var selectedSatellite: Satellite? = null
val stationPos = flow { val stationPos = flow {
val osmLat = clipLat(stationPosition.lat) val osmLat = clipLat(stationPosition.latitude)
val osmLon = clipLon(stationPosition.lon) val osmLon = clipLon(stationPosition.longitude)
emit(GeoPos(osmLat, osmLon)) emit(GeoPos(osmLat, osmLon))
} }
@@ -127,20 +127,20 @@ class MapViewModel @Inject constructor(
val osmLat = clipLat(satPos.latitude.toDegrees()) val osmLat = clipLat(satPos.latitude.toDegrees())
val osmLon = clipLon(satPos.longitude.toDegrees()) val osmLon = clipLon(satPos.longitude.toDegrees())
val currentPosition = GeoPos(osmLat, osmLon) val currentPosition = GeoPos(osmLat, osmLon)
if (oldLongitude < -170.0 && currentPosition.lon > 170.0) { if (oldLongitude < -170.0 && currentPosition.longitude > 170.0) {
// adding left terminal position // adding left terminal position
currentTrack.add(GeoPos(osmLat, -180.0)) currentTrack.add(GeoPos(osmLat, -180.0))
val finishedTrack = mutableListOf<GeoPos>().apply { addAll(currentTrack) } val finishedTrack = mutableListOf<GeoPos>().apply { addAll(currentTrack) }
satTracks.add(finishedTrack) satTracks.add(finishedTrack)
currentTrack.clear() currentTrack.clear()
} else if (oldLongitude > 170.0 && currentPosition.lon < -170.0) { } else if (oldLongitude > 170.0 && currentPosition.longitude < -170.0) {
// adding right terminal position // adding right terminal position
currentTrack.add(GeoPos(osmLat, 180.0)) currentTrack.add(GeoPos(osmLat, 180.0))
val finishedTrack = mutableListOf<GeoPos>().apply { addAll(currentTrack) } val finishedTrack = mutableListOf<GeoPos>().apply { addAll(currentTrack) }
satTracks.add(finishedTrack) satTracks.add(finishedTrack)
currentTrack.clear() currentTrack.clear()
} }
oldLongitude = currentPosition.lon oldLongitude = currentPosition.longitude
currentTrack.add(currentPosition) currentTrack.add(currentPosition)
} }
satTracks.add(currentTrack) satTracks.add(currentTrack)
@@ -183,7 +183,7 @@ class MapViewModel @Inject constructor(
val osmLat = clipLat(satPos.latitude.toDegrees()) val osmLat = clipLat(satPos.latitude.toDegrees())
val osmLon = clipLon(satPos.longitude.toDegrees()) val osmLon = clipLon(satPos.longitude.toDegrees())
val osmPos = GeoPos(osmLat, osmLon) val osmPos = GeoPos(osmLat, osmLon)
val qthLoc = QthConverter.positionToQth(osmPos.lat, osmPos.lon) ?: "-- --" val qthLoc = QthConverter.positionToQth(osmPos.latitude, osmPos.longitude) ?: "-- --"
val velocity = satPos.getOrbitalVelocity() val velocity = satPos.getOrbitalVelocity()
val phase = satPos.phase.toDegrees() val phase = satPos.phase.toDegrees()
val visibility = satPos.eclipsed val visibility = satPos.eclipsed
@@ -9,6 +9,7 @@ import androidx.compose.runtime.Composable
import androidx.compose.runtime.collectAsState import androidx.compose.runtime.collectAsState
import androidx.compose.ui.Alignment import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier import androidx.compose.ui.Modifier
import androidx.compose.ui.draw.rotate
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
@@ -94,8 +95,8 @@ private fun TransmitterItem(radio: SatRadio) {
) { ) {
Row(verticalAlignment = Alignment.CenterVertically, modifier = Modifier.fillMaxWidth()) { Row(verticalAlignment = Alignment.CenterVertically, modifier = Modifier.fillMaxWidth()) {
Icon( Icon(
painter = painterResource(id = R.drawable.ic_prev), painter = painterResource(id = R.drawable.ic_next),
contentDescription = null contentDescription = null, modifier = Modifier.rotate(180f)
) )
Text( Text(
text = radio.info, text = radio.info,
@@ -52,7 +52,7 @@ class RadarViewModel @Inject constructor(
private val locationSource: ILocationSource private val locationSource: ILocationSource
) : ViewModel() { ) : ViewModel() {
private val stationPos = locationSource.stationPosition.value.toGeoPos() private val stationPos = locationSource.stationPosition.value
private val magDeclination = getMagDeclination(stationPos) private val magDeclination = getMagDeclination(stationPos)
val transmitters = mutableStateOf<List<SatRadio>>(emptyList()) val transmitters = mutableStateOf<List<SatRadio>>(emptyList())
val radarData = mutableStateOf<RadarData?>(null) val radarData = mutableStateOf<RadarData?>(null)
@@ -102,8 +102,8 @@ class RadarViewModel @Inject constructor(
fun getShowSweep(): Boolean = settings.isSweepEnabled() fun getShowSweep(): Boolean = settings.isSweepEnabled()
private fun getMagDeclination(geoPos: GeoPos, time: Long = System.currentTimeMillis()): Float { private fun getMagDeclination(geoPos: GeoPos, time: Long = System.currentTimeMillis()): Float {
val latitude = geoPos.lat.toFloat() val latitude = geoPos.latitude.toFloat()
val longitude = geoPos.lon.toFloat() val longitude = geoPos.longitude.toFloat()
return GeomagneticField(latitude, longitude, 0f, time).declination return GeomagneticField(latitude, longitude, 0f, time).declination
} }
@@ -23,7 +23,7 @@ import androidx.compose.ui.unit.sp
import androidx.hilt.navigation.compose.hiltViewModel import androidx.hilt.navigation.compose.hiltViewModel
import com.rtbishop.look4sat.BuildConfig import com.rtbishop.look4sat.BuildConfig
import com.rtbishop.look4sat.R import com.rtbishop.look4sat.R
import com.rtbishop.look4sat.model.StationPos import com.rtbishop.look4sat.model.GeoPos
import com.rtbishop.look4sat.presentation.CardButton import com.rtbishop.look4sat.presentation.CardButton
import com.rtbishop.look4sat.presentation.MainTheme import com.rtbishop.look4sat.presentation.MainTheme
import com.rtbishop.look4sat.presentation.dialogs.LocatorDialog import com.rtbishop.look4sat.presentation.dialogs.LocatorDialog
@@ -164,7 +164,7 @@ private fun CardAbout(version: String, modifier: Modifier = Modifier) {
@Preview(showBackground = true) @Preview(showBackground = true)
@Composable @Composable
private fun LocationCardPreview() = MainTheme { private fun LocationCardPreview() = MainTheme {
val stationPos = StationPos(0.0, 0.0, "IO91vl", 0L) val stationPos = GeoPos(0.0, 0.0, 0.0, "IO91vl", 0L)
LocationCard( LocationCard(
settings = LocationSettings(true, stationPos, {}, { _, _ -> }, { }), settings = LocationSettings(true, stationPos, {}, { _, _ -> }, { }),
setGpsLoc = {}, togglePosDialog = {}) {} setGpsLoc = {}, togglePosDialog = {}) {}
@@ -1,10 +1,10 @@
package com.rtbishop.look4sat.presentation.settings package com.rtbishop.look4sat.presentation.settings
import com.rtbishop.look4sat.model.StationPos import com.rtbishop.look4sat.model.GeoPos
data class LocationSettings( data class LocationSettings(
val isUpdating: Boolean, val isUpdating: Boolean,
val stationPos: StationPos, val stationPos: GeoPos,
val setGpsLoc: () -> Unit, val setGpsLoc: () -> Unit,
val setManualLoc: (Double, Double) -> Unit, val setManualLoc: (Double, Double) -> Unit,
val setQthLoc: (String) -> Unit val setQthLoc: (String) -> Unit
@@ -25,7 +25,6 @@ import com.rtbishop.look4sat.domain.ILocationSource
import com.rtbishop.look4sat.domain.ISettingsSource import com.rtbishop.look4sat.domain.ISettingsSource
import com.rtbishop.look4sat.model.DataState import com.rtbishop.look4sat.model.DataState
import dagger.hilt.android.lifecycle.HiltViewModel import dagger.hilt.android.lifecycle.HiltViewModel
import kotlinx.coroutines.delay
import kotlinx.coroutines.flow.combine import kotlinx.coroutines.flow.combine
import kotlinx.coroutines.launch import kotlinx.coroutines.launch
import javax.inject.Inject import javax.inject.Inject
@@ -66,7 +65,6 @@ class SettingsViewModel @Inject constructor(
init { init {
viewModelScope.launch { viewModelScope.launch {
locationSource.stationPosition.collect { stationPos -> locationSource.stationPosition.collect { stationPos ->
delay(125)
locationSettings.value = locationSettings.value.copy( locationSettings.value = locationSettings.value.copy(
isUpdating = false, stationPos = stationPos isUpdating = false, stationPos = stationPos
) )
@@ -6,5 +6,5 @@
android:viewportHeight="24"> android:viewportHeight="24">
<path <path
android:fillColor="@android:color/white" android:fillColor="@android:color/white"
android:pathData="M16,6v14l-11,-7z" /> android:pathData="M19,6.41L17.59,5 12,10.59 6.41,5 5,6.41 10.59,12 5,17.59 6.41,19 12,13.41 17.59,19 19,17.59 13.41,12z" />
</vector> </vector>
+1 -1
View File
@@ -6,5 +6,5 @@
android:viewportHeight="24"> android:viewportHeight="24">
<path <path
android:fillColor="@android:color/white" android:fillColor="@android:color/white"
android:pathData="M8,6v14l11,-7z" /> android:pathData="M10,8.64L15.27,12 10,15.36V8.64M8,5v14l11,-7L8,5z" />
</vector> </vector>
+10
View File
@@ -0,0 +1,10 @@
<vector xmlns:android="http://schemas.android.com/apk/res/android"
android:width="24dp"
android:height="24dp"
android:autoMirrored="true"
android:viewportWidth="24"
android:viewportHeight="24">
<path
android:fillColor="@android:color/white"
android:pathData="M15.5,14h-0.79l-0.28,-0.27C15.41,12.59 16,11.11 16,9.5 16,5.91 13.09,3 9.5,3S3,5.91 3,9.5 5.91,16 9.5,16c1.61,0 3.09,-0.59 4.23,-1.57l0.27,0.28v0.79l5,4.99L20.49,19l-4.99,-5zM9.5,14C7.01,14 5,11.99 5,9.5S7.01,5 9.5,5 14,7.01 14,9.5 11.99,14 9.5,14z" />
</vector>
@@ -17,16 +17,16 @@
*/ */
package com.rtbishop.look4sat.domain package com.rtbishop.look4sat.domain
import com.rtbishop.look4sat.model.StationPos import com.rtbishop.look4sat.model.GeoPos
import kotlinx.coroutines.flow.StateFlow import kotlinx.coroutines.flow.StateFlow
interface ILocationSource { interface ILocationSource {
val stationPosition: StateFlow<StationPos> val stationPosition: StateFlow<GeoPos>
fun setGpsPosition(): Boolean fun setGpsPosition(): Boolean
fun setGeoPosition(latitude: Double, longitude: Double): Boolean fun setGeoPosition(latitude: Double, longitude: Double, altitude: Double = 0.0): Boolean
fun setQthPosition(locator: String): Boolean fun setQthPosition(locator: String): Boolean
} }
@@ -17,7 +17,7 @@
*/ */
package com.rtbishop.look4sat.domain package com.rtbishop.look4sat.domain
import com.rtbishop.look4sat.model.StationPos import com.rtbishop.look4sat.model.GeoPos
interface ISettingsSource { interface ISettingsSource {
@@ -51,9 +51,9 @@ interface ISettingsSource {
"Amsat" to "https://amsat.org/tle/current/nasabare.txt" "Amsat" to "https://amsat.org/tle/current/nasabare.txt"
) )
fun loadStationPosition(): StationPos fun loadStationPosition(): GeoPos
fun saveStationPosition(stationPos: StationPos) fun saveStationPosition(stationPos: GeoPos)
fun getHoursAhead(): Int fun getHoursAhead(): Int
@@ -60,7 +60,7 @@ abstract class Satellite(val data: OrbitalData) {
val apogee = sma * (1.0 + data.eccn) - EARTH_RADIUS val apogee = sma * (1.0 + data.eccn) - EARTH_RADIUS
var lin = data.incl var lin = data.incl
if (lin >= 90.0) lin = 180.0 - lin if (lin >= 90.0) lin = 180.0 - lin
acos(EARTH_RADIUS / (apogee + EARTH_RADIUS)) + lin * DEG2RAD > abs(pos.lat * DEG2RAD) acos(EARTH_RADIUS / (apogee + EARTH_RADIUS)) + lin * DEG2RAD > abs(pos.latitude * DEG2RAD)
} }
} }
@@ -146,8 +146,8 @@ abstract class Satellite(val data: OrbitalData) {
velocityVector.z - obsVel.z velocityVector.z - obsVel.z
) )
magnitude(range) magnitude(range)
val sinLat = sin(DEG2RAD * gsPos.lat) val sinLat = sin(DEG2RAD * gsPos.latitude)
val cosLat = cos(DEG2RAD * gsPos.lat) val cosLat = cos(DEG2RAD * gsPos.latitude)
val sinTheta = sin(gsPosTheta) val sinTheta = sin(gsPosTheta)
val cosTheta = cos(gsPosTheta) val cosTheta = cos(gsPosTheta)
val topS = sinLat * cosTheta * range.x + sinLat * sinTheta * range.y - cosLat * range.z val topS = sinLat * cosTheta * range.x + sinLat * sinTheta * range.y - cosLat * range.z
@@ -173,13 +173,13 @@ abstract class Satellite(val data: OrbitalData) {
obsVel: Vector4 obsVel: Vector4
) { ) {
val mFactor = 7.292115E-5 val mFactor = 7.292115E-5
gsPosTheta = mod2PI(thetaGJD(time) + DEG2RAD * gsPos.lon) gsPosTheta = mod2PI(thetaGJD(time) + DEG2RAD * gsPos.longitude)
val c = invert(sqrt(1.0 + FLAT_FACT * (FLAT_FACT - 2) * sqr(sin(DEG2RAD * gsPos.lat)))) val c = invert(sqrt(1.0 + FLAT_FACT * (FLAT_FACT - 2) * sqr(sin(DEG2RAD * gsPos.latitude))))
val sq = sqr(1.0 - FLAT_FACT) * c val sq = sqr(1.0 - FLAT_FACT) * c
val achcp = (EARTH_RADIUS * c + gsPos.alt / 1000.0) * cos(DEG2RAD * gsPos.lat) val achcp = (EARTH_RADIUS * c + gsPos.altitude / 1000.0) * cos(DEG2RAD * gsPos.latitude)
obsPos.setXYZ( obsPos.setXYZ(
achcp * cos(gsPosTheta), achcp * sin(gsPosTheta), achcp * cos(gsPosTheta), achcp * sin(gsPosTheta),
(EARTH_RADIUS * sq + gsPos.alt / 1000.0) * sin(DEG2RAD * gsPos.lat) (EARTH_RADIUS * sq + gsPos.altitude / 1000.0) * sin(DEG2RAD * gsPos.latitude)
) )
obsVel.setXYZ(-mFactor * obsPos.y, mFactor * obsPos.x, 0.0) obsVel.setXYZ(-mFactor * obsPos.y, mFactor * obsPos.x, 0.0)
magnitude(obsPos) magnitude(obsPos)
@@ -17,4 +17,10 @@
*/ */
package com.rtbishop.look4sat.model package com.rtbishop.look4sat.model
data class GeoPos(val lat: Double, val lon: Double, val alt: Double = 0.0, val name: String? = null) data class GeoPos(
val latitude: Double,
val longitude: Double,
val altitude: Double = 0.0,
val qthLocator: String = "null",
val timestamp: Long = 0L
)
@@ -1,7 +0,0 @@
package com.rtbishop.look4sat.model
data class StationPos(
val latitude: Double, val longitude: Double, val qthLocator: String, val timestamp: Long
) {
fun toGeoPos(): GeoPos = GeoPos(latitude, longitude)
}
@@ -25,11 +25,11 @@ class QthConverterTest {
@Test @Test
fun `Given valid QTH returns correct POS`() { fun `Given valid QTH returns correct POS`() {
var result = QthConverter.qthToPosition("io91VL39FX") var result = QthConverter.qthToPosition("io91VL39FX")
assert(result?.lat == 51.4792 && result.lon == -0.2083) assert(result?.latitude == 51.4792 && result.longitude == -0.2083)
result = QthConverter.qthToPosition("JN58TD") result = QthConverter.qthToPosition("JN58TD")
assert(result?.lat == 48.1458 && result.lon == 11.6250) assert(result?.latitude == 48.1458 && result.longitude == 11.6250)
result = QthConverter.qthToPosition("gf15vc") result = QthConverter.qthToPosition("gf15vc")
assert(result?.lat == -34.8958 && result.lon == -56.2083) assert(result?.latitude == -34.8958 && result.longitude == -56.2083)
} }
@Test @Test