Added various minor fixes, cleaned up classes

This commit is contained in:
Arty Bishop committed 2025-12-27 15:01:45 +00:00
1 parent 5a8108384f
commit fc0b995bac
35 files changed
+295 -310

No files matched your search

+4 -7
View File
@@ -1,11 +1,13 @@
@file: Suppress("UnstableApiUsage")
plugins {
id("com.android.application")
id("com.google.devtools.ksp")
id("org.jetbrains.kotlin.android")
}
kotlin {
jvmToolchain(17)
}
android {
namespace = "com.rtbishop.look4sat"
compileSdk = 33
@@ -29,12 +31,7 @@ android {
proguardFiles(getDefaultProguardFile("proguard-android-optimize.txt"), "proguard-rules.pro")
}
}
compileOptions {
sourceCompatibility = JavaVersion.VERSION_17
targetCompatibility = JavaVersion.VERSION_17
}
composeOptions { kotlinCompilerExtensionVersion = "1.5.1" }
packaging { resources { excludes += listOf("META-INF/{AL2.0,LGPL2.1}") } }
}
dependencies {
@@ -40,7 +40,7 @@ class MainApplication : Application() {
if (timeDelta > 172800000) { // 48 hours in ms
val sdf = SimpleDateFormat("d MMM yyyy - HH:mm:ss", Locale.getDefault())
println("Started periodic data update on ${sdf.format(Date())}")
container.databaseRepo.updateFromWeb()
container.databaseRepo.updateFromRemote()
}
}
}
@@ -10,18 +10,21 @@ import com.rtbishop.look4sat.data.database.MIGRATION_1_2
import com.rtbishop.look4sat.data.database.MainDatabase
import com.rtbishop.look4sat.data.framework.BluetoothReporter
import com.rtbishop.look4sat.data.framework.NetworkReporter
import com.rtbishop.look4sat.data.framework.SettingsRepo
import com.rtbishop.look4sat.data.repository.DatabaseRepo
import com.rtbishop.look4sat.data.repository.SatelliteRepo
import com.rtbishop.look4sat.data.repository.SelectionRepo
import com.rtbishop.look4sat.data.source.DataSource
import com.rtbishop.look4sat.data.source.LocalStorage
import com.rtbishop.look4sat.data.repository.SettingsRepo
import com.rtbishop.look4sat.data.source.FileSource
import com.rtbishop.look4sat.data.source.LocalSource
import com.rtbishop.look4sat.data.source.RemoteSource
import com.rtbishop.look4sat.data.source.SensorSource
import com.rtbishop.look4sat.domain.repository.IDatabaseRepo
import com.rtbishop.look4sat.domain.repository.ISatelliteRepo
import com.rtbishop.look4sat.domain.repository.ISelectionRepo
import com.rtbishop.look4sat.domain.repository.ISettingsRepo
import com.rtbishop.look4sat.domain.source.IDataSource
import com.rtbishop.look4sat.domain.source.IFileSource
import com.rtbishop.look4sat.domain.source.ILocalSource
import com.rtbishop.look4sat.domain.source.IRemoteSource
import com.rtbishop.look4sat.domain.source.ISensorSource
import com.rtbishop.look4sat.domain.utility.DataParser
import kotlinx.coroutines.CoroutineExceptionHandler
@@ -34,21 +37,16 @@ import org.osmdroid.config.Configuration
class MainContainer(private val context: Context) {
private val database =
Room.databaseBuilder(context, MainDatabase::class.java, "Look4SatDb").addMigrations(MIGRATION_1_2)
.fallbackToDestructiveMigration().build()
private val localStorage = LocalStorage(database.storageDao())
private val mainHandler = CoroutineExceptionHandler { _, error -> println("Look4Sat: $error") }
private val remoteSource = provideRemoteSource()
val mainScope = CoroutineScope(SupervisorJob() + Dispatchers.Default + mainHandler)
private val localSource = provideLocalSource()
val mainScope = provideMainScope()
val settingsRepo = provideSettingsRepo()
val databaseRepo = provideDatabaseRepo()
val satelliteRepo = provideSatelliteRepo()
val selectionRepo = provideSelectionRepo()
val satelliteRepo = provideSatelliteRepo()
val databaseRepo = provideDatabaseRepo()
fun provideBluetoothReporter(): BluetoothReporter {
val btManager = context.getSystemService(Context.BLUETOOTH_SERVICE) as BluetoothManager
return BluetoothReporter(btManager, CoroutineScope(Dispatchers.IO))
val manager = context.getSystemService(Context.BLUETOOTH_SERVICE) as BluetoothManager
return BluetoothReporter(manager, CoroutineScope(Dispatchers.IO))
}
fun provideNetworkReporter(): NetworkReporter {
@@ -56,34 +54,53 @@ class MainContainer(private val context: Context) {
}
fun provideSensorSource(): ISensorSource {
val sensorManager = context.getSystemService(Context.SENSOR_SERVICE) as SensorManager
val sensor = sensorManager.getDefaultSensor(Sensor.TYPE_ROTATION_VECTOR)
return SensorSource(sensorManager, sensor)
}
private fun provideRemoteSource(): IDataSource {
val cache = Cache(context.cacheDir, 1000 * 1000 * 10L)
val httpClient = OkHttpClient.Builder().cache(cache).build()
return DataSource(context.contentResolver, httpClient, Dispatchers.IO)
val manager = context.getSystemService(Context.SENSOR_SERVICE) as SensorManager
val sensor = manager.getDefaultSensor(Sensor.TYPE_ROTATION_VECTOR)
return SensorSource(manager, sensor)
}
private fun provideDatabaseRepo(): IDatabaseRepo {
val dataParser = DataParser(Dispatchers.Default)
return DatabaseRepo(Dispatchers.Default, dataParser, remoteSource, localStorage, settingsRepo)
val fileSource = provideFileSource()
val remoteSource = provideRemoteSource()
return DatabaseRepo(Dispatchers.Default, dataParser, fileSource, localSource, remoteSource, settingsRepo)
}
private fun provideFileSource(): IFileSource {
return FileSource(context.contentResolver, Dispatchers.IO)
}
private fun provideLocalSource(): ILocalSource {
val database = Room.databaseBuilder(context, MainDatabase::class.java, "Look4SatDb").apply {
addMigrations(MIGRATION_1_2)
fallbackToDestructiveMigration()
}.build()
return LocalSource(database.storageDao())
}
private fun provideMainScope(): CoroutineScope {
val mainHandler = CoroutineExceptionHandler { _, error -> println("Look4Sat: $error") }
return CoroutineScope(SupervisorJob() + Dispatchers.Default + mainHandler)
}
private fun provideRemoteSource(): IRemoteSource {
val cache = Cache(context.cacheDir, 1000 * 1000 * 10L)
val httpClient = OkHttpClient.Builder().cache(cache).build()
return RemoteSource(httpClient, Dispatchers.IO)
}
private fun provideSatelliteRepo(): ISatelliteRepo {
return SatelliteRepo(Dispatchers.Default, localStorage, settingsRepo)
return SatelliteRepo(Dispatchers.Default, localSource, settingsRepo)
}
private fun provideSelectionRepo(): ISelectionRepo {
return SelectionRepo(Dispatchers.Default, localStorage, settingsRepo)
return SelectionRepo(Dispatchers.Default, localSource, settingsRepo)
}
private fun provideSettingsRepo(): ISettingsRepo {
val locationManager = context.getSystemService(Context.LOCATION_SERVICE) as LocationManager
val manager = context.getSystemService(Context.LOCATION_SERVICE) as LocationManager
val preferences = context.getSharedPreferences("default", Context.MODE_PRIVATE)
Configuration.getInstance().load(context, preferences)
return SettingsRepo(locationManager, preferences)
return SettingsRepo(manager, preferences)
}
}
@@ -16,8 +16,6 @@ import androidx.compose.material3.MaterialTheme
import androidx.compose.material3.Surface
import androidx.compose.material3.Text
import androidx.compose.runtime.Composable
import androidx.compose.runtime.mutableStateOf
import androidx.compose.runtime.remember
import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier
import androidx.compose.ui.text.font.FontWeight
@@ -34,18 +32,11 @@ private val allModes = listOf(
"LRPT", "LSB", "MFSK", "MSK", "MSK AX.100 Mode 5", "MSK AX.100 Mode 6", "OFDM", "OQPSK",
"PSK", "PSK31", "PSK63", "QPSK", "QPSK31", "QPSK63", "SSTV", "USB", "WSJT"
)
private val selectedModes = mutableListOf("AFSK", "AFSK S-Net")
@Preview(showBackground = true)
@Composable
private fun ModesDialogPreview() {
val modesList = remember { mutableStateOf(selectedModes) }
MainTheme {
ModesDialog(allModes, modesList.value, {}) {
if (modesList.value.contains(it)) modesList.value.remove(it)
else modesList.value.add(it)
}
}
MainTheme { ModesDialog(allModes, listOf("AFSK", "AFSK S-Net"), {}) {} }
}
@Composable
@@ -97,9 +97,7 @@ private fun setStationPosition(stationPos: GeoPos, mapView: MapView) {
}
}
private fun setPositions(
posMap: Map<Satellite, GeoPos>, mapView: MapView, action: (Satellite) -> Unit
) {
private fun setPositions(posMap: Map<Satellite, GeoPos>, mapView: MapView, action: (Satellite) -> Unit) {
val markers = FolderOverlay()
try {
posMap.entries.forEach {
@@ -247,9 +245,7 @@ private val tonerLiteTileSource = XYTileSource(
private fun getColorFilter(): ColorMatrixColorFilter {
val negativeMatrix = ColorMatrix(
floatArrayOf(
-1f, 0f, 0f, 0f, 260f, 0f, -1f, 0f, 0f, 260f, 0f, 0f, -1f, 0f, 260f, 0f, 0f, 0f, 1f, 0f
)
floatArrayOf(-1f, 0f, 0f, 0f, 260f, 0f, -1f, 0f, 0f, 260f, 0f, 0f, -1f, 0f, 260f, 0f, 0f, 0f, 1f, 0f)
)
return ColorMatrixColorFilter(negativeMatrix)
}
@@ -28,9 +28,9 @@ import com.rtbishop.look4sat.domain.predict.SatPos
import com.rtbishop.look4sat.domain.predict.Satellite
import com.rtbishop.look4sat.domain.repository.ISatelliteRepo
import com.rtbishop.look4sat.domain.repository.ISettingsRepo
import com.rtbishop.look4sat.domain.utility.QthConverter
import com.rtbishop.look4sat.domain.utility.clipLat
import com.rtbishop.look4sat.domain.utility.clipLon
import com.rtbishop.look4sat.domain.utility.positionToQth
import com.rtbishop.look4sat.domain.utility.toDegrees
import com.rtbishop.look4sat.domain.utility.toTimerString
import java.util.Date
@@ -66,23 +66,23 @@ class MapViewModel(private val satelliteRepo: ISatelliteRepo, settingsRepo: ISet
val footprint: SharedFlow<SatPos> = _footprint
private val _mapData = MutableSharedFlow<MapData>()
val mapData: SharedFlow<MapData> = _mapData
// val mapData: SharedFlow<MapData> = _mapData
private val _positions = MutableSharedFlow<Map<Satellite, GeoPos>>()
val positions: SharedFlow<Map<Satellite, GeoPos>> = _positions
fun scrollSelection(decrement: Boolean) {
if (allSatellites.isNotEmpty()) {
val index = allSatellites.indexOf(selectedSatellite)
if (decrement) {
if (index > 0) selectSatellite(allSatellites[index - 1])
else selectSatellite(allSatellites[allSatellites.size - 1])
} else {
if (index < allSatellites.size - 1) selectSatellite(allSatellites[index + 1])
else selectSatellite(allSatellites[0])
}
}
}
// fun scrollSelection(decrement: Boolean) {
// if (allSatellites.isNotEmpty()) {
// val index = allSatellites.indexOf(selectedSatellite)
// if (decrement) {
// if (index > 0) selectSatellite(allSatellites[index - 1])
// else selectSatellite(allSatellites[allSatellites.size - 1])
// } else {
// if (index < allSatellites.size - 1) selectSatellite(allSatellites[index + 1])
// else selectSatellite(allSatellites[0])
// }
// }
// }
fun selectDefaultSatellite(catnum: Int) {
if (allSatellites.isNotEmpty()) {
@@ -178,7 +178,7 @@ class MapViewModel(private val satelliteRepo: ISatelliteRepo, settingsRepo: ISet
val osmLat = clipLat(satPos.latitude.toDegrees())
val osmLon = clipLon(satPos.longitude.toDegrees())
val osmPos = GeoPos(osmLat, osmLon)
val qthLoc = QthConverter.positionToQth(osmPos.latitude, osmPos.longitude) ?: "-- --"
val qthLoc = positionToQth(osmPos.latitude, osmPos.longitude) ?: "-- --"
val velocity = satPos.getOrbitalVelocity()
val phase = satPos.phase.toDegrees()
val visibility = satPos.eclipsed
@@ -88,9 +88,7 @@ fun PassesScreen(navToRadar: (Int, Long) -> Unit) {
contentDescription = null
)
}
Column(
verticalArrangement = Arrangement.SpaceEvenly, modifier = Modifier.weight(1f)
) {
Column(verticalArrangement = Arrangement.SpaceEvenly, modifier = Modifier.weight(1f)) {
Text(text = timerText.value?.first ?: "Null")
Text(text = timerText.value?.second ?: "Null")
}
@@ -50,7 +50,7 @@ class PassesViewModel(
fun getFilterSettings() = settingsRepo.passesSettings.value
fun shouldUseUTC() = settingsRepo.otherSettings.value.utcState
// fun shouldUseUTC() = settingsRepo.otherSettings.value.utcState
init {
viewModelScope.launch { satelliteRepo.initRepository() }
@@ -73,27 +73,25 @@ fun PullRefreshIndicator(
val showElevation by remember(refreshing, state) {
derivedStateOf { refreshing || state.position > 0.5f }
}
Surface(
modifier = modifier
.size(IndicatorSize)
.size(indicatorSize)
.pullRefreshIndicatorTransform(state, scale),
shape = SpinnerShape,
shape = spinnerShape,
color = backgroundColor,
shadowElevation = if (showElevation) Elevation else 0.dp,
shadowElevation = if (showElevation) elevation else 0.dp,
) {
Crossfade(
targetState = refreshing, animationSpec = tween(durationMillis = CrossfadeDurationMs)
targetState = refreshing,
animationSpec = tween(durationMillis = CROSSFADE_DURATION_MS),
label = "pullToRefresh"
) { refreshing ->
Box(
modifier = Modifier.fillMaxSize(), contentAlignment = Alignment.Center
) {
val spinnerSize = (ArcRadius + StrokeWidth).times(2)
Box(modifier = Modifier.fillMaxSize(), contentAlignment = Alignment.Center) {
val spinnerSize = (arcRadius + strokeWidth).times(2)
if (refreshing) {
CircularProgressIndicator(
color = contentColor,
strokeWidth = StrokeWidth,
strokeWidth = strokeWidth,
modifier = Modifier.size(spinnerSize),
)
} else {
@@ -108,18 +106,12 @@ fun PullRefreshIndicator(
* Modifier.size MUST be specified.
*/
@Composable
private fun CircularArrowIndicator(
state: PullRefreshState,
color: Color,
modifier: Modifier,
) {
private fun CircularArrowIndicator(state: PullRefreshState, color: Color, modifier: Modifier) {
val path = remember { Path().apply { fillType = PathFillType.EvenOdd } }
Canvas(modifier.semantics { contentDescription = "Refreshing" }) {
val values = ArrowValues(state.progress)
rotate(degrees = values.rotation) {
val arcRadius = ArcRadius.toPx() + StrokeWidth.toPx() / 2f
val arcRadius = arcRadius.toPx() + strokeWidth.toPx() / 2f
val arcBounds = Rect(
size.center.x - arcRadius,
size.center.y - arcRadius,
@@ -134,9 +126,7 @@ private fun CircularArrowIndicator(
useCenter = false,
topLeft = arcBounds.topLeft,
size = arcBounds.size,
style = Stroke(
width = StrokeWidth.toPx(), cap = StrokeCap.Square
)
style = Stroke(width = strokeWidth.toPx(), cap = StrokeCap.Square)
)
drawArrow(path, arcBounds, color, values)
}
@@ -145,11 +135,7 @@ private fun CircularArrowIndicator(
@Immutable
private class ArrowValues(
val alpha: Float,
val rotation: Float,
val startAngle: Float,
val endAngle: Float,
val scale: Float
val alpha: Float, val rotation: Float, val startAngle: Float, val endAngle: Float, val scale: Float
)
private fun ArrowValues(progress: Float): ArrowValues {
@@ -164,7 +150,7 @@ private fun ArrowValues(progress: Float): ArrowValues {
// Calculations based on SwipeRefreshLayout specification.
val alpha = progress.coerceIn(0f, 1f)
val endTrim = adjustedPercent * MaxProgressArc
val endTrim = adjustedPercent * MAX_PROGRESS_ARC
val rotation = (-0.25f + 0.4f * adjustedPercent + tensionPercent) * 0.5f
val startAngle = rotation * 360
val endAngle = (rotation + endTrim) * 360
@@ -176,33 +162,26 @@ private fun ArrowValues(progress: Float): ArrowValues {
private fun DrawScope.drawArrow(arrow: Path, bounds: Rect, color: Color, values: ArrowValues) {
arrow.reset()
arrow.moveTo(0f, 0f) // Move to left corner
arrow.lineTo(x = ArrowWidth.toPx() * values.scale, y = 0f) // Line to right corner
arrow.lineTo(x = arrowWidth.toPx() * values.scale, y = 0f) // Line to right corner
// Line to tip of arrow
arrow.lineTo(
x = ArrowWidth.toPx() * values.scale / 2, y = ArrowHeight.toPx() * values.scale
)
arrow.lineTo(x = arrowWidth.toPx() * values.scale / 2, y = arrowHeight.toPx() * values.scale)
val radius = min(bounds.width, bounds.height) / 2f
val inset = ArrowWidth.toPx() * values.scale / 2f
arrow.translate(
Offset(
x = radius + bounds.center.x - inset, y = bounds.center.y + StrokeWidth.toPx() / 2f
)
)
val inset = arrowWidth.toPx() * values.scale / 2f
arrow.translate(Offset(x = radius + bounds.center.x - inset, y = bounds.center.y + strokeWidth.toPx() / 2f))
arrow.close()
rotate(degrees = values.endAngle) {
drawPath(path = arrow, color = color, alpha = values.alpha)
}
}
private const val CrossfadeDurationMs = 100
private const val MaxProgressArc = 0.8f
private val IndicatorSize = 40.dp
private val SpinnerShape = CircleShape
private val ArcRadius = 7.5.dp
private val StrokeWidth = 2.5.dp
private val ArrowWidth = 10.dp
private val ArrowHeight = 5.dp
private val Elevation = 6.dp
private const val CROSSFADE_DURATION_MS = 100
private const val MAX_PROGRESS_ARC = 0.8f
private val indicatorSize = 40.dp
private val spinnerShape = CircleShape
private val arcRadius = 7.5.dp
private val strokeWidth = 2.5.dp
private val arrowWidth = 10.dp
private val arrowHeight = 5.dp
private val elevation = 6.dp
@@ -49,12 +49,10 @@ fun Modifier.pullRefreshIndicatorTransform(
properties["scale"] = scale
}) {
var height by remember { mutableStateOf(0) }
Modifier
.onSizeChanged { height = it.height }
.graphicsLayer {
translationY = state.position - height
if (scale && !state.refreshing) {
val scaleFraction = LinearOutSlowInEasing
.transform(state.position / state.threshold)
@@ -95,9 +93,7 @@ fun Modifier.pullRefresh(
* [onPull] nor [onRelease] will be invoked.
*/
fun Modifier.pullRefresh(
onPull: (pullDelta: Float) -> Float,
onRelease: suspend (flingVelocity: Float) -> Unit,
enabled: Boolean = true
onPull: (pullDelta: Float) -> Float, onRelease: suspend (flingVelocity: Float) -> Unit, enabled: Boolean = true
) = inspectable(inspectorInfo = debugInspectorInfo {
name = "pullRefresh"
properties["onPull"] = onPull
@@ -113,17 +109,13 @@ private class PullRefreshNestedScrollConnection(
private val enabled: Boolean
) : NestedScrollConnection {
override fun onPreScroll(
available: Offset, source: NestedScrollSource
): Offset = when {
override fun onPreScroll(available: Offset, source: NestedScrollSource): Offset = when {
!enabled -> Offset.Zero
source == Drag && available.y < 0 -> Offset(0f, onPull(available.y)) // Swiping up
else -> Offset.Zero
}
override fun onPostScroll(
consumed: Offset, available: Offset, source: NestedScrollSource
): Offset = when {
override fun onPostScroll(consumed: Offset, available: Offset, source: NestedScrollSource): Offset = when {
!enabled -> Offset.Zero
source == Drag && available.y > 0 -> Offset(0f, onPull(available.y)) // Pulling down
else -> Offset.Zero
@@ -53,27 +53,18 @@ fun rememberPullRefreshState(
refreshingOffset: Dp = PullRefreshDefaults.RefreshingOffset,
): PullRefreshState {
require(refreshThreshold > 0.dp) { "The refresh trigger must be greater than zero!" }
val scope = rememberCoroutineScope()
val onRefreshState = rememberUpdatedState(onRefresh)
val thresholdPx: Float
val refreshingOffsetPx: Float
with(LocalDensity.current) {
thresholdPx = refreshThreshold.toPx()
refreshingOffsetPx = refreshingOffset.toPx()
}
// refreshThreshold and refreshingOffset should not be changed after instantiation, so any
// changes to these values are ignored.
val state = remember(scope) {
PullRefreshState(scope, onRefreshState, refreshingOffsetPx, thresholdPx)
}
SideEffect {
state.setRefreshing(refreshing)
}
val state = remember(scope) { PullRefreshState(scope, onRefreshState, refreshingOffsetPx, thresholdPx) }
SideEffect { state.setRefreshing(refreshing) }
return state
}
@@ -109,7 +100,7 @@ class PullRefreshState internal constructor(
internal val refreshing get() = _refreshing
internal val position get() = _position
private val adjustedDistancePulled by derivedStateOf { distancePulled * DragMultiplier }
private val adjustedDistancePulled by derivedStateOf { distancePulled * DRAG_MULTIPLIER }
private var _refreshing by mutableStateOf(false)
private var _position by mutableStateOf(0f)
@@ -189,4 +180,4 @@ object PullRefreshDefaults {
* the refresh threshold, it is the indicator position, otherwise the indicator position is
* derived from the progress).
*/
private const val DragMultiplier = 0.5f
private const val DRAG_MULTIPLIER = 0.5f
@@ -44,14 +44,9 @@ fun RadarScreen() {
modifier = Modifier.fillMaxSize()
) {
IconButton(onClick = {}) {
Icon(
painter = painterResource(id = R.drawable.ic_filter),
contentDescription = null
)
Icon(painter = painterResource(id = R.drawable.ic_filter), contentDescription = null)
}
Column(
verticalArrangement = Arrangement.SpaceEvenly, modifier = Modifier.weight(1f)
) {
Column(verticalArrangement = Arrangement.SpaceEvenly, modifier = Modifier.weight(1f)) {
Text(text = "Null")
Text(text = "Null")
}
@@ -109,10 +104,7 @@ private fun TransmitterItem(radio: SatRadio) {
.background(MaterialTheme.colorScheme.surface)
.padding(6.dp)
) {
Row(
verticalAlignment = Alignment.CenterVertically,
modifier = Modifier.fillMaxWidth()
) {
Row(verticalAlignment = Alignment.CenterVertically, modifier = Modifier.fillMaxWidth()) {
Icon(
painter = painterResource(id = R.drawable.ic_arrow),
contentDescription = null, modifier = Modifier.rotate(180f)
@@ -127,10 +119,7 @@ private fun TransmitterItem(radio: SatRadio) {
contentDescription = null
)
}
Row(
verticalAlignment = Alignment.CenterVertically,
modifier = Modifier.fillMaxWidth()
) {
Row(verticalAlignment = Alignment.CenterVertically, modifier = Modifier.fillMaxWidth()) {
Text(
text = stringResource(id = R.string.radio_downlink, radio.downlink ?: 0L),
textAlign = TextAlign.Center,
@@ -43,9 +43,9 @@ fun RadarViewCompose(item: SatPos, items: List<SatPos>, azimElev: Pair<Float, Fl
val primaryColor = Color(0xFFFFE082)
val secondaryColor = Color(0xFFDC0000)
val strokeWidth = 4f
val animTransition = rememberInfiniteTransition()
val animTransition = rememberInfiniteTransition(label = "animScale")
val animSpec = infiniteRepeatable<Float>(tween(1000))
val animScale = animTransition.animateFloat(16f, 64f, animSpec)
val animScale = animTransition.animateFloat(16f, 64f, animSpec, label = "animScale")
val measurer = rememberTextMeasurer()
val sweepDegrees = remember { mutableStateOf(0f) }
val trackCreated = remember { mutableStateOf(false) }
@@ -99,9 +99,9 @@ class RadarViewModel(
}
}
fun getUseCompass(): Boolean = settingsRepo.otherSettings.value.sensorState
fun getShowSweep(): Boolean = settingsRepo.otherSettings.value.sweepState
// fun getUseCompass(): Boolean = settingsRepo.otherSettings.value.sensorState
//
// fun getShowSweep(): Boolean = settingsRepo.otherSettings.value.sweepState
private fun getMagDeclination(geoPos: GeoPos, time: Long = System.currentTimeMillis()): Float {
val latitude = geoPos.latitude.toFloat()
@@ -1,7 +1,6 @@
package com.rtbishop.look4sat.presentation.settings
import android.Manifest
import android.os.Build
import androidx.activity.compose.rememberLauncherForActivityResult
import androidx.activity.result.contract.ActivityResultContracts
import androidx.compose.foundation.layout.Arrangement
@@ -61,16 +60,16 @@ fun SettingsScreen() {
val context = LocalContext.current
// Permissions setup
val bluetoothContract = ActivityResultContracts.RequestPermission()
val bluetoothError = stringResource(R.string.BTremote_perm_error)
val bluetoothPerm = when {
Build.VERSION.SDK_INT < Build.VERSION_CODES.S -> Manifest.permission.BLUETOOTH
else -> Manifest.permission.BLUETOOTH_CONNECT
}
val bluetoothRequest = rememberLauncherForActivityResult(bluetoothContract) { isGranted ->
// val bluetoothContract = ActivityResultContracts.RequestPermission()
// val bluetoothError = stringResource(R.string.BTremote_perm_error)
// val bluetoothPerm = when {
// Build.VERSION.SDK_INT < Build.VERSION_CODES.S -> Manifest.permission.BLUETOOTH
// else -> Manifest.permission.BLUETOOTH_CONNECT
// }
// val bluetoothRequest = rememberLauncherForActivityResult(bluetoothContract) { isGranted ->
// viewModel.setBTEnabled(isGranted)
if (!isGranted) showToast(context, bluetoothError)
}
// if (!isGranted) showToast(context, bluetoothError)
// }
val locationContract = ActivityResultContracts.RequestMultiplePermissions()
val locationError = stringResource(R.string.location_gps_error)
val locationPermCoarse = Manifest.permission.ACCESS_COARSE_LOCATION
@@ -119,9 +118,7 @@ fun SettingsScreen() {
val toggleSensor = { value: Boolean -> viewModel.toggleSensor(value) }
// Screen setup
LazyColumn(
modifier = Modifier.padding(6.dp), verticalArrangement = Arrangement.spacedBy(6.dp)
) {
LazyColumn(modifier = Modifier.padding(6.dp), verticalArrangement = Arrangement.spacedBy(6.dp)) {
item { CardAbout(BuildConfig.VERSION_NAME) }
item { LocationCard(positionSettings, setGpsPos, showPosDialog, showLocDialog, dismissPos) }
item { DataCard(dataSettings, updateFromWeb, updateFromFile, clearAllData) }
@@ -376,8 +373,7 @@ private fun DataCard(
@Preview(showBackground = true)
@Composable
private fun OtherCardPreview() = MainTheme {
val values =
OtherSettings(utcState = false, updateState = true, sweepState = true, sensorState = true)
val values = OtherSettings(utcState = false, updateState = true, sweepState = true, sensorState = true)
OtherCard(settings = values, {}, {}, {}, {})
}
@@ -4,6 +4,4 @@ import com.rtbishop.look4sat.domain.predict.GeoPos
data class PositionSettings(val isUpdating: Boolean, val stationPos: GeoPos, val messageResId: Int)
data class DataSettings(
val isUpdating: Boolean, val entriesTotal: Int, val radiosTotal: Int, val timestamp: Long
)
data class DataSettings(val isUpdating: Boolean, val entriesTotal: Int, val radiosTotal: Int, val timestamp: Long)
@@ -106,7 +106,7 @@ class SettingsViewModel(
fun updateFromWeb() = viewModelScope.launch {
try {
_dataSettings.value = _dataSettings.value.copy(isUpdating = true)
databaseRepo.updateFromWeb()
databaseRepo.updateFromRemote()
} catch (exception: Exception) {
_dataSettings.value = _dataSettings.value.copy(isUpdating = false)
println(exception.printStackTrace())
-2
View File
@@ -147,8 +147,6 @@
<string name="data_success">යාවත්කාලීනය සම්පූර්ණ කිරීම සාර්ථකයි</string>
<string name="data_clear_success">දත්ත ඉවත් කිරීම සාර්ථකයි</string>
<string name="last_updated_pattern" translatable="false">d MMM yyyy - HH:mm:ss</string>
<string name="remote_title">භ්‍රමක මනාප</string>
<string name="remote_switch">දුරස්ථ පාලනය සබල ක°</string>
<string name="remote_ip_hint">IP ලිපිනය</string>
+4 -6
View File
@@ -1,11 +1,13 @@
@file:Suppress("UnstableApiUsage")
plugins {
id("com.android.library")
id("com.google.devtools.ksp")
id("org.jetbrains.kotlin.android")
}
kotlin {
jvmToolchain(17)
}
android {
namespace = "com.rtbishop.look4sat.data"
compileSdk = 33
@@ -13,10 +15,6 @@ android {
minSdk = 21
testInstrumentationRunner = "androidx.test.runner.AndroidJUnitRunner"
}
compileOptions {
sourceCompatibility = JavaVersion.VERSION_17
targetCompatibility = JavaVersion.VERSION_17
}
}
dependencies {
@@ -2,15 +2,15 @@ package com.rtbishop.look4sat.data.framework
import android.bluetooth.BluetoothManager
import android.util.Log
import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.Job
import kotlinx.coroutines.launch
import java.io.OutputStream
import java.util.*
import kotlin.math.abs
import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.Job
import kotlinx.coroutines.launch
class BluetoothReporter(
private val manager: BluetoothManager,
private val bluetoothManager: BluetoothManager,
private val reporterScope: CoroutineScope
) {
@@ -30,7 +30,7 @@ class BluetoothReporter(
if (!connected) {
rotationConnectionJob = reporterScope.launch {
try {
manager.adapter.getRemoteDevice(deviceId)?.let { device ->
bluetoothManager.adapter.getRemoteDevice(deviceId)?.let { device ->
device.createInsecureRfcommSocketToServiceRecord(sppid)?.let { socket ->
connecting = true
socket.connect()
@@ -22,36 +22,38 @@ import com.rtbishop.look4sat.domain.model.SatEntry
import com.rtbishop.look4sat.domain.model.SatRadio
import com.rtbishop.look4sat.domain.repository.IDatabaseRepo
import com.rtbishop.look4sat.domain.repository.ISettingsRepo
import com.rtbishop.look4sat.domain.source.IDataSource
import com.rtbishop.look4sat.domain.source.ILocalStorage
import com.rtbishop.look4sat.domain.source.IFileSource
import com.rtbishop.look4sat.domain.source.ILocalSource
import com.rtbishop.look4sat.domain.source.IRemoteSource
import com.rtbishop.look4sat.domain.utility.DataParser
import java.io.InputStream
import java.util.zip.ZipInputStream
import kotlinx.coroutines.CoroutineDispatcher
import kotlinx.coroutines.async
import kotlinx.coroutines.withContext
import java.io.InputStream
import java.util.zip.ZipInputStream
class DatabaseRepo(
private val dispatcher: CoroutineDispatcher,
private val dataParser: DataParser,
private val dataSource: IDataSource,
private val localStorage: ILocalStorage,
private val settingsRepository: ISettingsRepo
private val fileSource: IFileSource,
private val localSource: ILocalSource,
private val remoteSource: IRemoteSource,
private val settingsRepo: ISettingsRepo
) : IDatabaseRepo {
override suspend fun updateFromFile(uri: String) = withContext(dispatcher) {
val importedSatellites = dataSource.getFileStream(uri)?.let { importSatellites(it) }
importedSatellites?.let { localStorage.insertEntries(it) }
val importedSatellites = fileSource.getFileStream(uri)?.let { importSatellites(it) }
importedSatellites?.let { localSource.insertEntries(it) }
setUpdateSuccessful(System.currentTimeMillis())
}
override suspend fun updateFromWeb() = withContext(dispatcher) {
val sourcesMap = settingsRepository.satelliteSourcesMap
override suspend fun updateFromRemote() = withContext(dispatcher) {
val sourcesMap = settingsRepo.satelliteSourcesMap
val importedEntries = mutableListOf<SatEntry>()
val importedRadios = mutableListOf<SatRadio>()
// fetch
val jobsMap = sourcesMap.mapValues { async { dataSource.getNetworkStream(it.value) } }
val jobRadios = async { dataSource.getNetworkStream(settingsRepository.radioSourceUrl) }
val jobsMap = sourcesMap.mapValues { async { remoteSource.getRemoteStream(it.value) } }
val jobRadios = async { remoteSource.getRemoteStream(settingsRepo.radioSourceUrl) }
// parse
jobsMap.mapValues { job -> job.value.await() }.forEach { entry ->
entry.value?.let { stream ->
@@ -60,7 +62,7 @@ class DatabaseRepo(
// parse tle stream
val satellites = importSatellites(stream)
val catnums = satellites.map { it.data.catnum }
settingsRepository.saveSatType(type, catnums)
settingsRepo.saveSatType(type, catnums)
importedEntries.addAll(satellites)
}
@@ -69,7 +71,7 @@ class DatabaseRepo(
val unzipped = ZipInputStream(stream).apply { nextEntry }
val satellites = importSatellites(unzipped)
val catnums = satellites.map { it.data.catnum }
settingsRepository.saveSatType(type, catnums)
settingsRepo.saveSatType(type, catnums)
importedEntries.addAll(satellites)
}
@@ -78,7 +80,7 @@ class DatabaseRepo(
val parsed = dataParser.parseCSVStream(stream)
val satellites = parsed.map { data -> SatEntry(data) }
val catnums = satellites.map { it.data.catnum }
settingsRepository.saveSatType(type, catnums)
settingsRepo.saveSatType(type, catnums)
importedEntries.addAll(satellites)
}
}
@@ -86,21 +88,21 @@ class DatabaseRepo(
}
jobRadios.await()?.let { importedRadios.addAll(dataParser.parseJSONStream(it)) }
// insert
localStorage.insertEntries(importedEntries)
localStorage.insertRadios(importedRadios)
localSource.insertEntries(importedEntries)
localSource.insertRadios(importedRadios)
setUpdateSuccessful(System.currentTimeMillis())
}
override suspend fun clearAllData() = withContext(dispatcher) {
localStorage.deleteEntries()
localStorage.deleteRadios()
localSource.deleteEntries()
localSource.deleteRadios()
setUpdateSuccessful(0L)
}
private suspend fun setUpdateSuccessful(timestamp: Long) = withContext(dispatcher) {
val satellitesTotal = localStorage.getEntriesTotal()
val radiosTotal = localStorage.getRadiosTotal()
settingsRepository.saveDatabaseState(DatabaseState(satellitesTotal, radiosTotal, timestamp))
val satellitesTotal = localSource.getEntriesTotal()
val radiosTotal = localSource.getRadiosTotal()
settingsRepo.saveDatabaseState(DatabaseState(satellitesTotal, radiosTotal, timestamp))
}
private suspend fun importSatellites(stream: InputStream): List<SatEntry> {
@@ -24,7 +24,7 @@ import com.rtbishop.look4sat.domain.predict.SatPos
import com.rtbishop.look4sat.domain.predict.Satellite
import com.rtbishop.look4sat.domain.repository.ISatelliteRepo
import com.rtbishop.look4sat.domain.repository.ISettingsRepo
import com.rtbishop.look4sat.domain.source.ILocalStorage
import com.rtbishop.look4sat.domain.source.ILocalSource
import com.rtbishop.look4sat.domain.utility.round
import com.rtbishop.look4sat.domain.utility.toDegrees
import kotlinx.coroutines.CoroutineDispatcher
@@ -35,7 +35,7 @@ import kotlinx.coroutines.withContext
class SatelliteRepo(
private val dispatcher: CoroutineDispatcher,
private val localStorage: ILocalStorage,
private val localStorage: ILocalSource,
private val settingsRepo: ISettingsRepo
) : ISatelliteRepo {
@@ -3,7 +3,7 @@ package com.rtbishop.look4sat.data.repository
import com.rtbishop.look4sat.domain.model.SatItem
import com.rtbishop.look4sat.domain.repository.ISelectionRepo
import com.rtbishop.look4sat.domain.repository.ISettingsRepo
import com.rtbishop.look4sat.domain.source.ILocalStorage
import com.rtbishop.look4sat.domain.source.ILocalSource
import kotlinx.coroutines.CoroutineDispatcher
import kotlinx.coroutines.ExperimentalCoroutinesApi
import kotlinx.coroutines.flow.MutableStateFlow
@@ -15,7 +15,7 @@ import kotlinx.coroutines.withContext
@OptIn(ExperimentalCoroutinesApi::class)
class SelectionRepo(
private val dispatcher: CoroutineDispatcher,
private val localStorage: ILocalStorage,
private val localSource: ILocalSource,
private val settingsRepo: ISettingsRepo
) : ISelectionRepo {
private val currentItems = MutableStateFlow<List<SatItem>>(emptyList())
@@ -34,7 +34,7 @@ class SelectionRepo(
override suspend fun getEntriesFlow() = withContext(dispatcher) {
val selectedIds = settingsRepo.satelliteSelection.value
currentItems.value = localStorage.getEntriesList().map { item ->
currentItems.value = localSource.getEntriesList().map { item ->
item.copy(isSelected = item.catnum in selectedIds)
}
return@withContext itemsWithQuery
@@ -15,7 +15,7 @@
* You should have received a copy of the GNU General Public License
* along with this program. If not, see <https://www.gnu.org/licenses/>.
*/
package com.rtbishop.look4sat.data.framework
package com.rtbishop.look4sat.data.repository
import android.content.SharedPreferences
import android.location.Criteria
@@ -28,14 +28,15 @@ import com.rtbishop.look4sat.domain.model.OtherSettings
import com.rtbishop.look4sat.domain.model.PassesSettings
import com.rtbishop.look4sat.domain.predict.GeoPos
import com.rtbishop.look4sat.domain.repository.ISettingsRepo
import com.rtbishop.look4sat.domain.utility.QthConverter
import com.rtbishop.look4sat.domain.utility.positionToQth
import com.rtbishop.look4sat.domain.utility.qthToPosition
import com.rtbishop.look4sat.domain.utility.round
import java.util.concurrent.Executors
import kotlinx.coroutines.flow.MutableStateFlow
import kotlinx.coroutines.flow.StateFlow
import java.util.concurrent.Executors
class SettingsRepo(
private val manager: LocationManager, private val preferences: SharedPreferences
private val locationManager: LocationManager, private val preferences: SharedPreferences
) : ISettingsRepo {
private val keyModes = "satModes"
@@ -73,12 +74,12 @@ class SettingsRepo(
override val stationPosition: StateFlow<GeoPos> = _stationPosition
override fun setGpsPosition(): Boolean {
if (!LocationManagerCompat.isLocationEnabled(manager)) return false
if (!LocationManagerCompat.isLocationEnabled(locationManager)) return false
try {
val criteria = Criteria().apply { isCostAllowed = true }
val provider = manager.getBestProvider(criteria, true) ?: defaultProvider
val provider = locationManager.getBestProvider(criteria, true) ?: defaultProvider
println("Requesting location for $provider provider")
LocationManagerCompat.getCurrentLocation(manager, provider, timeoutSignal, executor) {
LocationManagerCompat.getCurrentLocation(locationManager, provider, timeoutSignal, executor) {
it?.let { setGeoPosition(it.latitude, it.longitude, it.altitude) }
}
} catch (exception: SecurityException) {
@@ -89,13 +90,13 @@ class SettingsRepo(
override fun setGeoPosition(latitude: Double, longitude: Double, altitude: Double): Boolean {
val newLongitude = if (longitude > 180.0) longitude - 180 else longitude
val locator = QthConverter.positionToQth(latitude, newLongitude) ?: return false
val locator = positionToQth(latitude, newLongitude) ?: return false
saveGeoPos(latitude, newLongitude, altitude, locator)
return true
}
override fun setQthPosition(locator: String): Boolean {
val position = QthConverter.qthToPosition(locator) ?: return false
val position = qthToPosition(locator) ?: return false
saveGeoPos(position.latitude, position.longitude, 0.0, locator)
return true
}
@@ -0,0 +1,16 @@
package com.rtbishop.look4sat.data.source
import android.content.ContentResolver
import android.net.Uri
import com.rtbishop.look4sat.domain.source.IFileSource
import java.io.InputStream
import kotlinx.coroutines.CoroutineDispatcher
import kotlinx.coroutines.withContext
class FileSource(
private val contentResolver: ContentResolver, private val dispatcher: CoroutineDispatcher
) : IFileSource {
override suspend fun getFileStream(uri: String): InputStream? {
return withContext(dispatcher) { contentResolver.openInputStream(Uri.parse(uri)) }
}
}
@@ -17,18 +17,18 @@
*/
package com.rtbishop.look4sat.data.source
import com.rtbishop.look4sat.data.database.dao.StorageDao
import com.rtbishop.look4sat.domain.model.SatRadio
import com.rtbishop.look4sat.domain.predict.Satellite
import com.rtbishop.look4sat.domain.source.ILocalStorage
import com.rtbishop.look4sat.data.database.entity.SatEntry as FrameworkEntry
import com.rtbishop.look4sat.data.database.entity.SatItem as FrameworkItem
import com.rtbishop.look4sat.data.database.entity.SatRadio as FrameworkRadio
import com.rtbishop.look4sat.domain.model.SatEntry as DomainEntry
import com.rtbishop.look4sat.domain.model.SatItem as DomainItem
import com.rtbishop.look4sat.domain.model.SatRadio as DomainRadio
import com.rtbishop.look4sat.data.database.dao.StorageDao
import com.rtbishop.look4sat.domain.model.SatRadio
import com.rtbishop.look4sat.domain.predict.Satellite
import com.rtbishop.look4sat.domain.source.ILocalSource
class LocalStorage(private val storageDao: StorageDao) : ILocalStorage {
class LocalSource(private val storageDao: StorageDao) : ILocalSource {
//region # Entries region
@@ -17,26 +17,18 @@
*/
package com.rtbishop.look4sat.data.source
import android.content.ContentResolver
import android.net.Uri
import com.rtbishop.look4sat.domain.source.IDataSource
import com.rtbishop.look4sat.domain.source.IRemoteSource
import java.io.InputStream
import kotlinx.coroutines.CoroutineDispatcher
import kotlinx.coroutines.withContext
import okhttp3.OkHttpClient
import okhttp3.Request
import java.io.InputStream
class DataSource(
private val contentResolver: ContentResolver,
private val httpClient: OkHttpClient,
private val dispatcher: CoroutineDispatcher
) : IDataSource {
class RemoteSource(
private val httpClient: OkHttpClient, private val dispatcher: CoroutineDispatcher
) : IRemoteSource {
override suspend fun getFileStream(uri: String): InputStream? {
return withContext(dispatcher) { contentResolver.openInputStream(Uri.parse(uri)) }
}
override suspend fun getNetworkStream(url: String): InputStream? = withContext(dispatcher) {
override suspend fun getRemoteStream(url: String): InputStream? = withContext(dispatcher) {
try {
val request = Request.Builder().url(url).build()
httpClient.newCall(request).execute().body?.byteStream()
@@ -19,8 +19,6 @@ package com.rtbishop.look4sat.domain.model
sealed class DataState<out T> {
data class Success<out T>(val data: T) : DataState<T>()
data class Error(val resId: Int?) : DataState<Nothing>()
data class Exception(val message: String?) : DataState<Nothing>()
data object Handled : DataState<Nothing>()
data object Loading : DataState<Nothing>()
}
@@ -21,7 +21,7 @@ interface IDatabaseRepo {
suspend fun updateFromFile(uri: String)
suspend fun updateFromWeb()
suspend fun updateFromRemote()
suspend fun clearAllData()
}
@@ -19,9 +19,6 @@ package com.rtbishop.look4sat.domain.source
import java.io.InputStream
interface IDataSource {
interface IFileSource {
suspend fun getFileStream(uri: String): InputStream?
suspend fun getNetworkStream(url: String): InputStream?
}
@@ -22,7 +22,7 @@ import com.rtbishop.look4sat.domain.model.SatItem
import com.rtbishop.look4sat.domain.model.SatRadio
import com.rtbishop.look4sat.domain.predict.Satellite
interface ILocalStorage {
interface ILocalSource {
suspend fun getEntriesTotal(): Int
@@ -0,0 +1,24 @@
/*
* Look4Sat. Amateur radio satellite tracker and pass predictor.
* Copyright (C) 2019-2022 Arty Bishop (bishop.arty@gmail.com)
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with this program. If not, see <https://www.gnu.org/licenses/>.
*/
package com.rtbishop.look4sat.domain.source
import java.io.InputStream
interface IRemoteSource {
suspend fun getRemoteStream(url: String): InputStream?
}
@@ -1,3 +1,20 @@
/*
* Look4Sat. Amateur radio satellite tracker and pass predictor.
* Copyright (C) 2019-2022 Arty Bishop (bishop.arty@gmail.com)
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with this program. If not, see <https://www.gnu.org/licenses/>.
*/
package com.rtbishop.look4sat.domain.utility
import com.rtbishop.look4sat.domain.predict.DEG2RAD
@@ -19,43 +19,40 @@ package com.rtbishop.look4sat.domain.utility
import com.rtbishop.look4sat.domain.predict.GeoPos
object QthConverter {
fun qthToPosition(locator: String): GeoPos? {
val trimmedQth = locator.take(6)
if (!isValidLocator(trimmedQth)) return null
val lonFirst = (trimmedQth[0].uppercaseChar().code - 65) * 20
val latFirst = (trimmedQth[1].uppercaseChar().code - 65) * 10
val lonSecond = trimmedQth[2].toString().toInt() * 2
val latSecond = trimmedQth[3].toString().toInt()
val lonThird = (((trimmedQth[4].lowercaseChar().code - 97) / 12.0) + (1.0 / 24.0)) - 180
val latThird = (((trimmedQth[5].lowercaseChar().code - 97) / 24.0) + (1.0 / 48.0)) - 90
val longitude = (lonFirst + lonSecond + lonThird).round(4)
val latitude = (latFirst + latSecond + latThird).round(4)
return GeoPos(latitude, longitude)
}
fun positionToQth(latitude: Double, longitude: Double): String? {
if (!isValidPosition(latitude, longitude)) return null
val tempLon = if (longitude > 180.0) longitude - 180 else longitude
val upper = "ABCDEFGHIJKLMNOPQRSTUVWX"
val lower = "abcdefghijklmnopqrstuvwx"
val newLongitude = tempLon + 180
val newLatitude = latitude + 90
val lonFirst = upper[(newLongitude / 20).toInt()]
val latFirst = upper[(newLatitude / 10).toInt()]
val lonSecond = ((newLongitude / 2) % 10).toInt().toString()
val latSecond = (newLatitude % 10).toInt().toString()
val lonThird = lower[((newLongitude % 2) * 12).toInt()]
val latThird = lower[((newLatitude % 1) * 24).toInt()]
return "$lonFirst$latFirst$lonSecond$latSecond$lonThird$latThird"
}
private fun isValidPosition(lat: Double, lon: Double): Boolean {
return (lat >= -90.0 && lat <= 90.0) && (lon >= -180.0 && lon <= 360.0)
}
private fun isValidLocator(locator: String): Boolean {
return locator.matches("[a-xA-X][a-xA-X]\\d\\d[a-xA-X][a-xA-X]".toRegex())
}
fun qthToPosition(locator: String): GeoPos? {
val trimmedQth = locator.take(6)
if (!isValidLocator(trimmedQth)) return null
val lonFirst = (trimmedQth[0].uppercaseChar().code - 65) * 20
val latFirst = (trimmedQth[1].uppercaseChar().code - 65) * 10
val lonSecond = trimmedQth[2].toString().toInt() * 2
val latSecond = trimmedQth[3].toString().toInt()
val lonThird = (((trimmedQth[4].lowercaseChar().code - 97) / 12.0) + (1.0 / 24.0)) - 180
val latThird = (((trimmedQth[5].lowercaseChar().code - 97) / 24.0) + (1.0 / 48.0)) - 90
val longitude = (lonFirst + lonSecond + lonThird).round(4)
val latitude = (latFirst + latSecond + latThird).round(4)
return GeoPos(latitude, longitude)
}
fun positionToQth(latitude: Double, longitude: Double): String? {
if (!isValidPosition(latitude, longitude)) return null
val tempLon = if (longitude > 180.0) longitude - 180 else longitude
val upper = "ABCDEFGHIJKLMNOPQRSTUVWX"
val lower = "abcdefghijklmnopqrstuvwx"
val newLongitude = tempLon + 180
val newLatitude = latitude + 90
val lonFirst = upper[(newLongitude / 20).toInt()]
val latFirst = upper[(newLatitude / 10).toInt()]
val lonSecond = ((newLongitude / 2) % 10).toInt().toString()
val latSecond = (newLatitude % 10).toInt().toString()
val lonThird = lower[((newLongitude % 2) * 12).toInt()]
val latThird = lower[((newLatitude % 1) * 24).toInt()]
return "$lonFirst$latFirst$lonSecond$latSecond$lonThird$latThird"
}
private fun isValidPosition(lat: Double, lon: Double): Boolean {
return (lat >= -90.0 && lat <= 90.0) && (lon >= -180.0 && lon <= 360.0)
}
private fun isValidLocator(locator: String): Boolean {
return locator.matches("[a-xA-X][a-xA-X]\\d\\d[a-xA-X][a-xA-X]".toRegex())
}
@@ -17,37 +17,38 @@
*/
package com.rtbishop.look4sat.domain
import com.rtbishop.look4sat.domain.utility.QthConverter
import com.rtbishop.look4sat.domain.utility.positionToQth
import com.rtbishop.look4sat.domain.utility.qthToPosition
import org.junit.Test
class QthConverterTest {
@Test
fun `Given valid QTH returns correct POS`() {
var result = QthConverter.qthToPosition("io91VL39FX")
var result = qthToPosition("io91VL39FX")
assert(result?.latitude == 51.4792 && result.longitude == -0.2083)
result = QthConverter.qthToPosition("JN58TD")
result = qthToPosition("JN58TD")
assert(result?.latitude == 48.1458 && result.longitude == 11.6250)
result = QthConverter.qthToPosition("gf15vc")
result = qthToPosition("gf15vc")
assert(result?.latitude == -34.8958 && result.longitude == -56.2083)
}
@Test
fun `Given invalid QTH returns null`() {
assert(QthConverter.qthToPosition("ZZ00zz") == null)
assert(QthConverter.qthToPosition("JN58") == null)
assert(qthToPosition("ZZ00zz") == null)
assert(qthToPosition("JN58") == null)
}
@Test
fun `Given valid POS returns correct QTH`() {
assert(QthConverter.positionToQth(51.4878, -0.2146) == "IO91vl")
assert(QthConverter.positionToQth(48.1466, 11.6083) == "JN58td")
assert(QthConverter.positionToQth(-34.91, -56.2116) == "GF15vc")
assert(positionToQth(51.4878, -0.2146) == "IO91vl")
assert(positionToQth(48.1466, 11.6083) == "JN58td")
assert(positionToQth(-34.91, -56.2116) == "GF15vc")
}
@Test
fun `Given invalid POS returns null`() {
assert(QthConverter.positionToQth(91.0542, -170.1142) == null)
assert(QthConverter.positionToQth(89.0542, -240.1142) == null)
assert(positionToQth(91.0542, -170.1142) == null)
assert(positionToQth(89.0542, -240.1142) == null)
}
}