mirror of
https://github.com/rt-bishop/Look4Sat.git
synced 2026-10-02 03:15:37 +00:00
Fixes and refactoring for LocationSource
This commit is contained in:
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 = {}) {}
|
||||||
|
|||||||
+2
-2
@@ -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>
|
||||||
@@ -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>
|
||||||
@@ -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
|
||||||
|
|||||||
Reference in new issue
Block a user