mirror of
https://github.com/atsunatsu/Look4Sat.git
synced 2026-10-02 03:15:37 +00:00
Added various minor fixes, cleaned up classes
This commit is contained in:
1 parent
5a8108384f
commit
fc0b995bac
35 files changed
+295
-310
No files matched your search
@@ -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() }
|
||||
|
||||
+27
-48
@@ -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
|
||||
+3
-11
@@ -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
|
||||
|
||||
+4
-13
@@ -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())
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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
|
||||
|
||||
+10
-9
@@ -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)) }
|
||||
}
|
||||
}
|
||||
+5
-5
@@ -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
|
||||
|
||||
+6
-14
@@ -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()
|
||||
}
|
||||
+1
-4
@@ -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?
|
||||
}
|
||||
+1
-1
@@ -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)
|
||||
}
|
||||
}
|
||||
Reference in new issue
Block a user