mirror of
https://github.com/rt-bishop/Look4Sat.git
synced 2026-10-02 03:15:37 +00:00
Extracted implementation classes into data module
This commit is contained in:
1 parent
5a449d4133
commit
28e926eb86
54 files changed
+247
-153
No files matched your search
@@ -0,0 +1,22 @@
|
||||
package com.rtbishop.look4sat.data
|
||||
|
||||
import androidx.test.ext.junit.runners.AndroidJUnit4
|
||||
import androidx.test.platform.app.InstrumentationRegistry
|
||||
import org.junit.Assert.*
|
||||
import org.junit.Test
|
||||
import org.junit.runner.RunWith
|
||||
|
||||
/**
|
||||
* Instrumented test, which will execute on an Android device.
|
||||
*
|
||||
* See [testing documentation](http://d.android.com/tools/testing).
|
||||
*/
|
||||
@RunWith(AndroidJUnit4::class)
|
||||
class ExampleInstrumentedTest {
|
||||
@Test
|
||||
fun useAppContext() {
|
||||
// Context of the app under test.
|
||||
val appContext = InstrumentationRegistry.getInstrumentation().targetContext
|
||||
assertEquals("com.rtbishop.look4sat.data.test", appContext.packageName)
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,11 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<manifest xmlns:android="http://schemas.android.com/apk/res/android">
|
||||
|
||||
<uses-permission android:name="android.permission.ACCESS_COARSE_LOCATION" />
|
||||
<uses-permission android:name="android.permission.ACCESS_FINE_LOCATION" />
|
||||
<uses-permission
|
||||
android:name="android.permission.BLUETOOTH"
|
||||
android:maxSdkVersion="30" />
|
||||
<uses-permission android:name="android.permission.BLUETOOTH_CONNECT" />
|
||||
|
||||
</manifest>
|
||||
@@ -0,0 +1,41 @@
|
||||
/*
|
||||
* 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.data.database
|
||||
|
||||
import androidx.room.Database
|
||||
import androidx.room.RoomDatabase
|
||||
import androidx.room.migration.Migration
|
||||
import androidx.sqlite.db.SupportSQLiteDatabase
|
||||
import com.rtbishop.look4sat.data.database.dao.StorageDao
|
||||
import com.rtbishop.look4sat.data.database.entity.SatEntry
|
||||
import com.rtbishop.look4sat.data.database.entity.SatRadio
|
||||
|
||||
@Database(entities = [SatEntry::class, SatRadio::class], version = 2, exportSchema = false)
|
||||
abstract class MainDatabase : RoomDatabase() {
|
||||
|
||||
abstract fun storageDao(): StorageDao
|
||||
}
|
||||
|
||||
val MIGRATION_1_2 = object : Migration(1, 2) {
|
||||
override fun migrate(database: SupportSQLiteDatabase) {
|
||||
database.execSQL("CREATE TABLE entries_backup (name TEXT NOT NULL, epoch REAL NOT NULL, meanmo REAL NOT NULL, eccn REAL NOT NULL, incl REAL NOT NULL, raan REAL NOT NULL, argper REAL NOT NULL, meanan REAL NOT NULL, catnum INTEGER NOT NULL, bstar REAL NOT NULL, xincl REAL NOT NULL, xnodeo REAL NOT NULL, omegao REAL NOT NULL, xmo REAL NOT NULL, xno REAL NOT NULL, orbitalPeriod REAL NOT NULL, isDeepSpace INTEGER NOT NULL, comment TEXT, PRIMARY KEY(catnum))")
|
||||
database.execSQL("INSERT INTO entries_backup (name, epoch, meanmo, eccn, incl, raan, argper, meanan, catnum, bstar, xincl, xnodeo, omegao, xmo, xno, orbitalPeriod, isDeepSpace, comment) SELECT name, epoch, meanmo, eccn, incl, raan, argper, meanan, catnum, bstar, xincl, xnodeo, omegao, xmo, xno, 1440 / meanmo, 1440 / meanmo >= 225.0, comment FROM entries")
|
||||
database.execSQL("DROP TABLE entries")
|
||||
database.execSQL("ALTER TABLE entries_backup RENAME TO entries")
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,57 @@
|
||||
/*
|
||||
* 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.data.database.dao
|
||||
|
||||
import androidx.room.*
|
||||
import com.rtbishop.look4sat.data.database.entity.SatEntry
|
||||
import com.rtbishop.look4sat.data.database.entity.SatItem
|
||||
import com.rtbishop.look4sat.data.database.entity.SatRadio
|
||||
|
||||
@Dao
|
||||
interface StorageDao {
|
||||
|
||||
@Query("SELECT COUNT(*) FROM entries")
|
||||
suspend fun getEntriesTotal(): Int
|
||||
|
||||
//@Transaction
|
||||
@Query("SELECT catnum, name FROM entries ORDER BY name ASC")
|
||||
suspend fun getEntriesList(): List<SatItem>
|
||||
|
||||
@Transaction
|
||||
@Query("SELECT * FROM entries WHERE catnum IN (:selectedIds)")
|
||||
suspend fun getEntriesWithIds(selectedIds: List<Int>): List<SatEntry>
|
||||
|
||||
@Insert(onConflict = OnConflictStrategy.REPLACE)
|
||||
suspend fun insertEntries(entries: List<SatEntry>)
|
||||
|
||||
@Query("DELETE FROM entries")
|
||||
suspend fun deleteEntries()
|
||||
|
||||
@Query("SELECT COUNT(*) FROM radios")
|
||||
suspend fun getRadiosTotal(): Int
|
||||
|
||||
@Transaction
|
||||
@Query("SELECT * FROM radios WHERE catnum = :id AND isAlive = 1")
|
||||
suspend fun getRadiosWithId(id: Int): List<SatRadio>
|
||||
|
||||
@Insert(onConflict = OnConflictStrategy.REPLACE)
|
||||
suspend fun insertRadios(radios: List<SatRadio>)
|
||||
|
||||
@Query("DELETE FROM radios")
|
||||
suspend fun deleteRadios()
|
||||
}
|
||||
@@ -0,0 +1,25 @@
|
||||
/*
|
||||
* 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.data.database.entity
|
||||
|
||||
import androidx.room.Embedded
|
||||
import androidx.room.Entity
|
||||
import com.rtbishop.look4sat.domain.predict.OrbitalData
|
||||
|
||||
@Entity(tableName = "entries", primaryKeys = ["catnum"])
|
||||
data class SatEntry(@Embedded val data: OrbitalData, var comment: String? = null)
|
||||
@@ -0,0 +1,30 @@
|
||||
/*
|
||||
* 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.data.database.entity
|
||||
|
||||
data class SatItem(
|
||||
val catnum: Int,
|
||||
val name: String
|
||||
// @Relation(
|
||||
// parentColumn = "catnum",
|
||||
// entity = SatRadio::class,
|
||||
// entityColumn = "catnum",
|
||||
// projection = ["mode"]
|
||||
// )
|
||||
// val modes: List<String>
|
||||
)
|
||||
@@ -0,0 +1,34 @@
|
||||
/*
|
||||
* 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.data.database.entity
|
||||
|
||||
import androidx.room.Entity
|
||||
import androidx.room.PrimaryKey
|
||||
|
||||
@Entity(tableName = "radios")
|
||||
data class SatRadio(
|
||||
@PrimaryKey val uuid: String,
|
||||
val info: String,
|
||||
val isAlive: Boolean,
|
||||
var downlink: Long?,
|
||||
var uplink: Long?,
|
||||
val mode: String?,
|
||||
val isInverted: Boolean,
|
||||
val catnum: Int?,
|
||||
var comment: String? = null
|
||||
)
|
||||
@@ -0,0 +1,85 @@
|
||||
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
|
||||
|
||||
class BluetoothReporter(
|
||||
private val manager: BluetoothManager,
|
||||
private val reporterScope: CoroutineScope
|
||||
) {
|
||||
|
||||
private val tag = "BTReporter"
|
||||
private val sppid: UUID = UUID.fromString("00001101-0000-1000-8000-00805f9b34fb")
|
||||
private lateinit var outputStream: OutputStream
|
||||
private var rotationConnectionJob: Job? = null
|
||||
private var rotationReportingJob: Job? = null
|
||||
private var connected = false
|
||||
private var connecting = false
|
||||
|
||||
fun isConnected(): Boolean = connected
|
||||
|
||||
fun isConnecting(): Boolean = connecting
|
||||
|
||||
fun connectBTDevice(deviceId: String) {
|
||||
if (!connected) {
|
||||
rotationConnectionJob = reporterScope.launch {
|
||||
try {
|
||||
manager.adapter.getRemoteDevice(deviceId)?.let { device ->
|
||||
device.createInsecureRfcommSocketToServiceRecord(sppid)?.let { socket ->
|
||||
connecting = true
|
||||
socket.connect()
|
||||
outputStream = socket.outputStream
|
||||
connected = true
|
||||
connecting = false
|
||||
Log.i(tag, "$tag: Connected!")
|
||||
}
|
||||
}
|
||||
} catch (e: SecurityException) {
|
||||
Log.e(tag, "$tag: ${e.message}")
|
||||
} catch (e: Exception) {
|
||||
Log.e(tag, "$tag: ${e.message}")
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fun reportRotation(format: String, azimuth: Int, elevation: Int) {
|
||||
if (connected) {
|
||||
rotationReportingJob = reporterScope.launch {
|
||||
val newElevation = if (elevation > 0) elevation else 0
|
||||
try {
|
||||
val azimuthString = intToStringWithLeadingZeroes(azimuth)
|
||||
val elevationString = intToStringWithLeadingZeroes(newElevation)
|
||||
val crChar = '\r'
|
||||
val nlChar = '\n'
|
||||
val tbChar = '\t'
|
||||
var buffer = format.replace("\$AZ", azimuthString)
|
||||
buffer = buffer.replace("\$EL", elevationString)
|
||||
buffer = buffer.replace("\\r", crChar.toString())
|
||||
buffer = buffer.replace("\\n", nlChar.toString())
|
||||
buffer = buffer.replace("\\t", tbChar.toString())
|
||||
Log.i(tag, "$tag: Sending $buffer")
|
||||
if (connected) outputStream.write(buffer.toByteArray())
|
||||
} catch (e: Exception) {
|
||||
Log.e(tag, "$tag: ${e.message}")
|
||||
connected = false
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private fun intToStringWithLeadingZeroes(value: Int): String {
|
||||
return if (value > 0) {
|
||||
if (value < 10) "00$value" else if (value < 100) "0$value" else "$value"
|
||||
} else {
|
||||
val absValue = abs(value)
|
||||
if (value > -10) "-00$absValue" else if (value > -100) "-0$absValue" else "-$absValue"
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,70 @@
|
||||
/*
|
||||
* 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.data.framework
|
||||
|
||||
import java.net.InetSocketAddress
|
||||
import java.nio.ByteBuffer
|
||||
import java.nio.channels.SocketChannel
|
||||
import kotlinx.coroutines.CoroutineScope
|
||||
import kotlinx.coroutines.Job
|
||||
import kotlinx.coroutines.cancelAndJoin
|
||||
import kotlinx.coroutines.launch
|
||||
|
||||
class NetworkReporter(private val reporterScope: CoroutineScope) {
|
||||
|
||||
private var rotationSocketChannel: SocketChannel? = null
|
||||
private var rotationReporting: Job? = null
|
||||
|
||||
// private var frequencySocketChannel: SocketChannel? = null
|
||||
// private var frequencyReporting: Job? = null
|
||||
//
|
||||
// fun reportFrequency(server: String, port: Int, frequency: Long) {
|
||||
// frequencyReporting = reporterScope.launch {
|
||||
// runCatching {
|
||||
// if (frequencySocketChannel == null) {
|
||||
// frequencySocketChannel = SocketChannel.open(InetSocketAddress(server, port))
|
||||
// } else {
|
||||
// val buffer = ByteBuffer.wrap("\\set_freq $frequency\n".toByteArray())
|
||||
// frequencySocketChannel?.write(buffer)
|
||||
// }
|
||||
// }.onFailure { error ->
|
||||
// println(error.localizedMessage)
|
||||
// frequencySocketChannel = null
|
||||
// frequencyReporting?.cancelAndJoin()
|
||||
// }
|
||||
// }
|
||||
// }
|
||||
|
||||
fun reportRotation(server: String, port: Int, azimuth: Double, elevation: Double) {
|
||||
rotationReporting = reporterScope.launch {
|
||||
val newElevation = if (elevation > 0.0) elevation else 0.0
|
||||
runCatching {
|
||||
if (rotationSocketChannel == null) {
|
||||
rotationSocketChannel = SocketChannel.open(InetSocketAddress(server, port))
|
||||
} else {
|
||||
val buffer = ByteBuffer.wrap("\\P $azimuth $newElevation\n".toByteArray())
|
||||
rotationSocketChannel?.write(buffer)
|
||||
}
|
||||
}.onFailure { error ->
|
||||
println(error.localizedMessage)
|
||||
rotationSocketChannel = null
|
||||
rotationReporting?.cancelAndJoin()
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,310 @@
|
||||
/*
|
||||
* 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.data.framework
|
||||
|
||||
import android.content.SharedPreferences
|
||||
import android.location.Criteria
|
||||
import android.location.LocationManager
|
||||
import androidx.core.content.edit
|
||||
import androidx.core.location.LocationManagerCompat
|
||||
import androidx.core.os.CancellationSignal
|
||||
import com.rtbishop.look4sat.domain.model.DatabaseState
|
||||
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.round
|
||||
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
|
||||
) : ISettingsRepo {
|
||||
|
||||
private val keyModes = "satModes"
|
||||
private val keyUtcState = "utcState"
|
||||
private val keyUpdateState = "updateState"
|
||||
private val keySweepState = "sweepState"
|
||||
private val keySensorState = "sensorState"
|
||||
private val keyDataEntries = "dataEntries"
|
||||
private val keyDataRadios = "dataRadios"
|
||||
private val keyDataTimestamp = "dataTimestamp"
|
||||
private val keyHoursAhead = "hoursAhead"
|
||||
private val keyMinElevation = "minElevation"
|
||||
private val keyRotator = "isRotatorEnabled"
|
||||
private val keyRotatorAddress = "rotatorAddress"
|
||||
private val keyRotatorPort = "rotatorPort"
|
||||
private val keyBTEnabled = "isBTEnabled"
|
||||
private val keyBTDeviceName = "BTDeviceName"
|
||||
private val keyBTDeviceAddr = "BTDeviceAddr"
|
||||
private val keyBTFormat = "BTFormat"
|
||||
private val keyLatitude = "stationLat"
|
||||
private val keyLongitude = "stationLon"
|
||||
private val keyAltitude = "stationAlt"
|
||||
private val keyLocator = "stationQTH"
|
||||
private val keyLocTimestamp = "locTimestamp"
|
||||
private val keySelection = "selection"
|
||||
|
||||
//region # Station position settings
|
||||
|
||||
private val _stationPosition = MutableStateFlow(loadStationPosition())
|
||||
private val defaultProvider = LocationManager.PASSIVE_PROVIDER
|
||||
private val executor = Executors.newSingleThreadExecutor()
|
||||
private val timeoutSignal = CancellationSignal().apply {
|
||||
setOnCancelListener { _stationPosition.value = loadStationPosition() }
|
||||
}
|
||||
override val stationPosition: StateFlow<GeoPos> = _stationPosition
|
||||
|
||||
override fun setGpsPosition(): Boolean {
|
||||
if (!LocationManagerCompat.isLocationEnabled(manager)) return false
|
||||
try {
|
||||
val criteria = Criteria().apply { isCostAllowed = true }
|
||||
val provider = manager.getBestProvider(criteria, true) ?: defaultProvider
|
||||
println("Requesting location for $provider provider")
|
||||
LocationManagerCompat.getCurrentLocation(manager, provider, timeoutSignal, executor) {
|
||||
it?.let { setGeoPosition(it.latitude, it.longitude, it.altitude) }
|
||||
}
|
||||
} catch (exception: SecurityException) {
|
||||
println("No permissions were given")
|
||||
}
|
||||
return true
|
||||
}
|
||||
|
||||
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
|
||||
saveGeoPos(latitude, newLongitude, altitude, locator)
|
||||
return true
|
||||
}
|
||||
|
||||
override fun setQthPosition(locator: String): Boolean {
|
||||
val position = QthConverter.qthToPosition(locator) ?: return false
|
||||
saveGeoPos(position.latitude, position.longitude, 0.0, locator)
|
||||
return true
|
||||
}
|
||||
|
||||
private fun saveStationPosition(stationPos: GeoPos) = preferences.edit {
|
||||
putString(keyLatitude, stationPos.latitude.toString())
|
||||
putString(keyLongitude, stationPos.longitude.toString())
|
||||
putString(keyAltitude, stationPos.altitude.toString())
|
||||
putString(keyLocator, stationPos.qthLocator)
|
||||
putLong(keyLocTimestamp, stationPos.timestamp)
|
||||
_stationPosition.value = stationPos
|
||||
}
|
||||
|
||||
private fun saveGeoPos(latitude: Double, longitude: Double, altitude: Double, locator: String) {
|
||||
val newLat = latitude.round(4)
|
||||
val newLon = longitude.round(4)
|
||||
val newAlt = altitude.round(1)
|
||||
val timestamp = System.currentTimeMillis()
|
||||
println("Received new Position($newLat, $newLon, $newAlt) & Locator $locator")
|
||||
saveStationPosition(GeoPos(newLat, newLon, newAlt, locator, timestamp))
|
||||
}
|
||||
|
||||
private fun loadStationPosition(): GeoPos {
|
||||
val latitude = (preferences.getString(keyLatitude, null) ?: "0.0").toDouble()
|
||||
val longitude = (preferences.getString(keyLongitude, null) ?: "0.0").toDouble()
|
||||
val altitude = (preferences.getString(keyAltitude, null) ?: "0.0").toDouble()
|
||||
val qthLocator = preferences.getString(keyLocator, null) ?: "null"
|
||||
val timestamp = preferences.getLong(keyLocTimestamp, 0L)
|
||||
return GeoPos(latitude, longitude, altitude, qthLocator, timestamp)
|
||||
}
|
||||
|
||||
//endregion
|
||||
|
||||
//region # Database update settings
|
||||
|
||||
private val _databaseState = MutableStateFlow(loadDatabaseState())
|
||||
override val databaseState: StateFlow<DatabaseState> = _databaseState
|
||||
|
||||
override fun saveDatabaseState(state: DatabaseState) = preferences.edit {
|
||||
putInt(keyDataEntries, state.entriesTotal)
|
||||
putInt(keyDataRadios, state.radiosTotal)
|
||||
putLong(keyDataTimestamp, state.timestamp)
|
||||
_databaseState.value = state
|
||||
}
|
||||
|
||||
override fun saveSatType(type: String, catnums: List<Int>) {
|
||||
val stringList = catnums.map { catnum -> catnum.toString() }
|
||||
preferences.edit { putStringSet("type$type", stringList.toSet()) }
|
||||
}
|
||||
|
||||
override fun loadSatType(type: String): List<Int> {
|
||||
val catnums =
|
||||
preferences.getStringSet("type$type", emptySet())?.map { catnum -> catnum.toInt() }
|
||||
return catnums ?: emptyList()
|
||||
}
|
||||
|
||||
private fun loadDatabaseState(): DatabaseState {
|
||||
val entriesTotal = preferences.getInt(keyDataEntries, 0)
|
||||
val radiosTotal = preferences.getInt(keyDataRadios, 0)
|
||||
val timestamp = preferences.getLong(keyDataTimestamp, 0L)
|
||||
return DatabaseState(entriesTotal, radiosTotal, timestamp)
|
||||
}
|
||||
|
||||
//endregion
|
||||
|
||||
//region # Entries selection settings
|
||||
|
||||
private val _satelliteSelection = MutableStateFlow(loadEntriesSelection())
|
||||
override val satelliteSelection: StateFlow<List<Int>> = _satelliteSelection
|
||||
|
||||
override fun saveEntriesSelection(catnums: List<Int>) = preferences.edit {
|
||||
putStringSet(keySelection, catnums.map { catnum -> catnum.toString() }.toSet())
|
||||
_satelliteSelection.value = catnums
|
||||
}
|
||||
|
||||
private fun loadEntriesSelection(): List<Int> {
|
||||
val catNums = preferences.getStringSet(keySelection, emptySet())
|
||||
return catNums?.map { catnum -> catnum.toInt() }?.sorted() ?: emptyList()
|
||||
}
|
||||
|
||||
//endregion
|
||||
|
||||
//region # Passes filter settings
|
||||
|
||||
private val _passesSettings = MutableStateFlow(loadPassesSettings())
|
||||
override val passesSettings: StateFlow<PassesSettings> = _passesSettings
|
||||
|
||||
override fun savePassesSettings(settings: PassesSettings) = preferences.edit {
|
||||
putInt(keyHoursAhead, settings.hoursAhead)
|
||||
putDouble(keyMinElevation, settings.minElevation)
|
||||
_passesSettings.value = settings
|
||||
}
|
||||
|
||||
override fun saveModesSelection(modes: List<String>) {
|
||||
preferences.edit { putStringSet(keyModes, modes.toSet()) }
|
||||
}
|
||||
|
||||
override fun loadModesSelection(): List<String> {
|
||||
return preferences.getStringSet(keyModes, null)?.toList()?.sorted() ?: emptyList()
|
||||
}
|
||||
|
||||
private fun loadPassesSettings(): PassesSettings {
|
||||
val hoursAhead = preferences.getInt(keyHoursAhead, 24)
|
||||
val minElevation = preferences.getDouble(keyMinElevation, 16.0)
|
||||
return PassesSettings(hoursAhead, minElevation)
|
||||
}
|
||||
|
||||
//endregion
|
||||
|
||||
//region # Other settings
|
||||
|
||||
private val _otherSettings = MutableStateFlow(loadOtherSettings())
|
||||
override val otherSettings: StateFlow<OtherSettings> = _otherSettings
|
||||
|
||||
override fun toggleUtc(value: Boolean) {
|
||||
preferences.edit { putBoolean(keyUtcState, value) }
|
||||
_otherSettings.value = otherSettings.value.copy(utcState = value)
|
||||
}
|
||||
|
||||
override fun toggleUpdate(value: Boolean) {
|
||||
preferences.edit { putBoolean(keyUpdateState, value) }
|
||||
_otherSettings.value = otherSettings.value.copy(updateState = value)
|
||||
}
|
||||
|
||||
override fun toggleSweep(value: Boolean) {
|
||||
preferences.edit { putBoolean(keySweepState, value) }
|
||||
_otherSettings.value = otherSettings.value.copy(sweepState = value)
|
||||
}
|
||||
|
||||
override fun toggleSensor(value: Boolean) {
|
||||
preferences.edit { putBoolean(keySensorState, value) }
|
||||
_otherSettings.value = otherSettings.value.copy(sensorState = value)
|
||||
}
|
||||
|
||||
private fun loadOtherSettings(): OtherSettings {
|
||||
val utcState = preferences.getBoolean(keyUtcState, false)
|
||||
val updateState = preferences.getBoolean(keyUpdateState, true)
|
||||
val sweepState = preferences.getBoolean(keySweepState, true)
|
||||
val sensorState = preferences.getBoolean(keySensorState, true)
|
||||
return OtherSettings(utcState, updateState, sweepState, sensorState)
|
||||
}
|
||||
|
||||
//endregion
|
||||
|
||||
//region # Undefined settings
|
||||
|
||||
override fun getRotatorEnabled(): Boolean {
|
||||
return preferences.getBoolean(keyRotator, false)
|
||||
}
|
||||
|
||||
override fun setRotatorEnabled(value: Boolean) {
|
||||
preferences.edit { putBoolean(keyRotator, value) }
|
||||
}
|
||||
|
||||
override fun getRotatorServer(): String {
|
||||
return preferences.getString(keyRotatorAddress, null) ?: "127.0.0.1"
|
||||
}
|
||||
|
||||
override fun setRotatorServer(value: String) {
|
||||
preferences.edit { putString(keyRotatorAddress, value) }
|
||||
}
|
||||
|
||||
override fun getRotatorPort(): String {
|
||||
return preferences.getString(keyRotatorPort, null) ?: "4533"
|
||||
}
|
||||
|
||||
override fun setRotatorPort(value: String) {
|
||||
preferences.edit { putString(keyRotatorPort, value) }
|
||||
}
|
||||
|
||||
override fun getBTEnabled(): Boolean {
|
||||
return preferences.getBoolean(keyBTEnabled, false)
|
||||
}
|
||||
|
||||
override fun setBTEnabled(value: Boolean) {
|
||||
preferences.edit { putBoolean(keyBTEnabled, value) }
|
||||
}
|
||||
|
||||
override fun getBTDeviceAddr(): String {
|
||||
return preferences.getString(keyBTDeviceAddr, null) ?: "00:0C:BF:13:80:5D"
|
||||
}
|
||||
|
||||
override fun setBTDeviceAddr(value: String) {
|
||||
preferences.edit { putString(keyBTDeviceAddr, value) }
|
||||
}
|
||||
|
||||
override fun getBTDeviceName(): String {
|
||||
return preferences.getString(keyBTDeviceName, null) ?: "Default"
|
||||
}
|
||||
|
||||
override fun setBTDeviceName(value: String) {
|
||||
preferences.edit { putString(keyBTDeviceName, value) }
|
||||
}
|
||||
|
||||
override fun getBTFormat(): String {
|
||||
return preferences.getString(keyBTFormat, null) ?: "W\$AZ \$EL"
|
||||
}
|
||||
|
||||
override fun setBTFormat(value: String) {
|
||||
preferences.edit { putString(keyBTFormat, value) }
|
||||
}
|
||||
|
||||
//endregion
|
||||
|
||||
private fun SharedPreferences.getDouble(key: String, default: Double): Double {
|
||||
return Double.fromBits(getLong(key, default.toRawBits()))
|
||||
}
|
||||
|
||||
private fun SharedPreferences.Editor.putDouble(key: String, double: Double) {
|
||||
putLong(key, double.toRawBits())
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,157 @@
|
||||
/*
|
||||
* 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.data.repository
|
||||
|
||||
import com.rtbishop.look4sat.domain.model.SatRadio
|
||||
import com.rtbishop.look4sat.domain.predict.OrbitalData
|
||||
import kotlinx.coroutines.CoroutineDispatcher
|
||||
import kotlinx.coroutines.withContext
|
||||
import org.json.JSONArray
|
||||
import org.json.JSONObject
|
||||
import java.io.InputStream
|
||||
import kotlin.math.pow
|
||||
|
||||
class DataParser(private val dispatcher: CoroutineDispatcher) {
|
||||
|
||||
suspend fun parseCSVStream(stream: InputStream): List<OrbitalData> = withContext(dispatcher) {
|
||||
val parsedItems = mutableListOf<OrbitalData>()
|
||||
stream.bufferedReader().useLines { lines ->
|
||||
lines.forEachIndexed { index, line ->
|
||||
if (index != 0) {
|
||||
val values = line.split(",")
|
||||
parseCSV(values)?.let { tle -> parsedItems.add(tle) }
|
||||
}
|
||||
}
|
||||
}
|
||||
return@withContext parsedItems
|
||||
}
|
||||
|
||||
suspend fun parseTLEStream(stream: InputStream): List<OrbitalData> = withContext(dispatcher) {
|
||||
val tleStrings = mutableListOf(String(), String(), String())
|
||||
val parsedItems = mutableListOf<OrbitalData>()
|
||||
var lineIndex = 0
|
||||
stream.bufferedReader().forEachLine { line ->
|
||||
tleStrings[lineIndex] = line
|
||||
if (lineIndex < 2) {
|
||||
lineIndex++
|
||||
} else {
|
||||
val isLineOneValid = tleStrings[1].substring(0, 1) == "1"
|
||||
val isLineTwoValid = tleStrings[2].substring(0, 1) == "2"
|
||||
if (!isLineOneValid && !isLineTwoValid) return@forEachLine
|
||||
parseTLE(tleStrings)?.let { tle -> parsedItems.add(tle) }
|
||||
lineIndex = 0
|
||||
}
|
||||
}
|
||||
return@withContext parsedItems
|
||||
}
|
||||
|
||||
suspend fun parseJSONStream(stream: InputStream): List<SatRadio> = withContext(dispatcher) {
|
||||
val parsedItems = mutableListOf<SatRadio>()
|
||||
try {
|
||||
val jsonArray = JSONArray(stream.bufferedReader().readText())
|
||||
for (index in 0 until jsonArray.length()) {
|
||||
val jsonObject = jsonArray.getJSONObject(index)
|
||||
parseJSON(jsonObject)?.let { parsedItems.add(it) }
|
||||
}
|
||||
return@withContext parsedItems
|
||||
} catch (exception: Exception) {
|
||||
return@withContext parsedItems
|
||||
}
|
||||
}
|
||||
|
||||
private fun parseCSV(values: List<String>): OrbitalData? {
|
||||
try {
|
||||
val name = values[0]
|
||||
val year = values[2].substring(0, 4)
|
||||
val month = values[2].substring(5, 7)
|
||||
val dayOfMonth = values[2].substring(8, 10)
|
||||
val dayInt = getDayOfYear(year.toInt(), month.toInt(), dayOfMonth.toInt())
|
||||
val day = if (dayInt < 10) "00$dayInt" else if (dayInt < 100) "0$dayInt" else "$dayInt"
|
||||
val hour = values[2].substring(11, 13).toInt() * 3600000 // ms in one hour
|
||||
val min = values[2].substring(14, 16).toInt() * 60000 // ms in one minute
|
||||
val sec = values[2].substring(17, 19).toInt() * 1000 // ms in one second
|
||||
val ms = values[2].substring(20, 26).toInt() / 1000.0 // microseconds to ms
|
||||
val frac = ((hour + min + sec + ms) / 86400000.0).toString()
|
||||
val epoch = "${year.substring(2)}$day${frac.substring(1)}".toDouble()
|
||||
val meanmo = values[3].toDouble()
|
||||
val eccn = values[4].toDouble()
|
||||
val incl = values[5].toDouble()
|
||||
val raan = values[6].toDouble()
|
||||
val argper = values[7].toDouble()
|
||||
val meanan = values[8].toDouble()
|
||||
val catnum = values[11].toInt()
|
||||
val bstar = values[14].toDouble()
|
||||
return OrbitalData(name, epoch, meanmo, eccn, incl, raan, argper, meanan, catnum, bstar)
|
||||
} catch (exception: Exception) {
|
||||
return null
|
||||
}
|
||||
}
|
||||
|
||||
private fun parseTLE(tle: List<String>): OrbitalData? {
|
||||
if (tle[1].substring(0, 1) != "1" && tle[2].substring(0, 1) != "2") {
|
||||
return null
|
||||
}
|
||||
try {
|
||||
val name: String = tle[0].trim()
|
||||
val epoch: Double = tle[1].substring(18, 32).toDouble()
|
||||
val meanmo: Double = tle[2].substring(52, 63).toDouble()
|
||||
val eccn: Double = tle[2].substring(26, 33).toDouble() / 10000000.0
|
||||
val incl: Double = tle[2].substring(8, 16).toDouble()
|
||||
val raan: Double = tle[2].substring(17, 25).toDouble()
|
||||
val argper: Double = tle[2].substring(34, 42).toDouble()
|
||||
val meanan: Double = tle[2].substring(43, 51).toDouble()
|
||||
val catnum: Int = tle[1].substring(2, 7).trim().toInt()
|
||||
val bstar: Double = 1.0e-5 * tle[1].substring(53, 59).toDouble() / 10.0.pow(
|
||||
tle[1].substring(60, 61).toDouble()
|
||||
)
|
||||
return OrbitalData(name, epoch, meanmo, eccn, incl, raan, argper, meanan, catnum, bstar)
|
||||
} catch (exception: Exception) {
|
||||
return null
|
||||
}
|
||||
}
|
||||
|
||||
private fun parseJSON(json: JSONObject): SatRadio? {
|
||||
try {
|
||||
val uuid = json.getString("uuid")
|
||||
val info = json.getString("description")
|
||||
val isAlive = json.getBoolean("alive")
|
||||
val downlink = if (json.isNull("downlink_low")) null
|
||||
else json.getLong("downlink_low")
|
||||
val uplink = if (json.isNull("uplink_low")) null
|
||||
else json.getLong("uplink_low")
|
||||
val mode = if (json.isNull("mode")) null
|
||||
else json.getString("mode")
|
||||
val isInverted = json.getBoolean("invert")
|
||||
val catnum = if (json.isNull("norad_cat_id")) null
|
||||
else json.getInt("norad_cat_id")
|
||||
return SatRadio(uuid, info, isAlive, downlink, uplink, mode, isInverted, catnum)
|
||||
} catch (exception: Exception) {
|
||||
return null
|
||||
}
|
||||
}
|
||||
|
||||
private fun getDayOfYear(year: Int, month: Int, dayOfMonth: Int): Int {
|
||||
if (month == 1) return dayOfMonth
|
||||
val daysArray = arrayOf(31, 28, 31, 30, 31, 30, 31, 31, 30, 31, 30, 31)
|
||||
var dayOfYear = dayOfMonth
|
||||
// If leap year increment Feb days
|
||||
if (((year / 4 == 0) && (year / 100 != 0)) || (year / 400 == 0)) daysArray[1]++
|
||||
for (i in 0 until month - 1) dayOfYear += daysArray[i]
|
||||
return dayOfYear
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,108 @@
|
||||
/*
|
||||
* 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.data.repository
|
||||
|
||||
import com.rtbishop.look4sat.domain.model.DatabaseState
|
||||
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 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
|
||||
) : IDatabaseRepo {
|
||||
|
||||
override suspend fun updateFromFile(uri: String) = withContext(dispatcher) {
|
||||
val importedSatellites = dataSource.getFileStream(uri)?.let { importSatellites(it) }
|
||||
importedSatellites?.let { localStorage.insertEntries(it) }
|
||||
setUpdateSuccessful(System.currentTimeMillis())
|
||||
}
|
||||
|
||||
override suspend fun updateFromWeb() = withContext(dispatcher) {
|
||||
val sourcesMap = settingsRepository.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) }
|
||||
// parse
|
||||
jobsMap.mapValues { job -> job.value.await() }.forEach { entry ->
|
||||
entry.value?.let { stream ->
|
||||
when (val type = entry.key) {
|
||||
"AMSAT", "R4UAB" -> {
|
||||
// parse tle stream
|
||||
val satellites = importSatellites(stream)
|
||||
val catnums = satellites.map { it.data.catnum }
|
||||
settingsRepository.saveSatType(type, catnums)
|
||||
importedEntries.addAll(satellites)
|
||||
}
|
||||
|
||||
"McCants", "Classified" -> {
|
||||
// unzip and parse tle stream
|
||||
val unzipped = ZipInputStream(stream).apply { nextEntry }
|
||||
val satellites = importSatellites(unzipped)
|
||||
val catnums = satellites.map { it.data.catnum }
|
||||
settingsRepository.saveSatType(type, catnums)
|
||||
importedEntries.addAll(satellites)
|
||||
}
|
||||
|
||||
else -> {
|
||||
// parse csv stream
|
||||
val parsed = dataParser.parseCSVStream(stream)
|
||||
val satellites = parsed.map { data -> SatEntry(data) }
|
||||
val catnums = satellites.map { it.data.catnum }
|
||||
settingsRepository.saveSatType(type, catnums)
|
||||
importedEntries.addAll(satellites)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
jobRadios.await()?.let { importedRadios.addAll(dataParser.parseJSONStream(it)) }
|
||||
// insert
|
||||
localStorage.insertEntries(importedEntries)
|
||||
localStorage.insertRadios(importedRadios)
|
||||
setUpdateSuccessful(System.currentTimeMillis())
|
||||
}
|
||||
|
||||
override suspend fun clearAllData() = withContext(dispatcher) {
|
||||
localStorage.deleteEntries()
|
||||
localStorage.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))
|
||||
}
|
||||
|
||||
private suspend fun importSatellites(stream: InputStream): List<SatEntry> {
|
||||
return dataParser.parseTLEStream(stream).map { data -> SatEntry(data) }
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,236 @@
|
||||
/*
|
||||
* 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.data.repository
|
||||
|
||||
import com.rtbishop.look4sat.domain.model.SatRadio
|
||||
import com.rtbishop.look4sat.domain.predict.GeoPos
|
||||
import com.rtbishop.look4sat.domain.predict.SatPass
|
||||
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.utility.round
|
||||
import com.rtbishop.look4sat.domain.utility.toDegrees
|
||||
import kotlinx.coroutines.CoroutineDispatcher
|
||||
import kotlinx.coroutines.flow.MutableStateFlow
|
||||
import kotlinx.coroutines.flow.StateFlow
|
||||
import kotlinx.coroutines.flow.update
|
||||
import kotlinx.coroutines.withContext
|
||||
|
||||
class SatelliteRepo(
|
||||
private val dispatcher: CoroutineDispatcher,
|
||||
private val localStorage: ILocalStorage,
|
||||
private val settingsRepo: ISettingsRepo
|
||||
) : ISatelliteRepo {
|
||||
|
||||
private val _passes = MutableStateFlow<List<SatPass>>(emptyList())
|
||||
override val passes: StateFlow<List<SatPass>> = _passes
|
||||
|
||||
private val _satellites = MutableStateFlow<List<Satellite>>(emptyList())
|
||||
override val satellites: StateFlow<List<Satellite>> = _satellites
|
||||
|
||||
override suspend fun getRadiosWithId(id: Int) = localStorage.getRadiosWithId(id)
|
||||
|
||||
override suspend fun initRepository() = withContext(dispatcher) {
|
||||
settingsRepo.satelliteSelection.collect { selectedIds ->
|
||||
_satellites.update { localStorage.getEntriesWithIds(selectedIds) }
|
||||
val (hoursAhead, minElevation) = settingsRepo.passesSettings.value
|
||||
calculatePasses(System.currentTimeMillis(), hoursAhead, minElevation)
|
||||
}
|
||||
}
|
||||
|
||||
override suspend fun getPosition(sat: Satellite, pos: GeoPos, time: Long): SatPos {
|
||||
return withContext(dispatcher) { sat.getPosition(pos, time) }
|
||||
}
|
||||
|
||||
override suspend fun getTrack(
|
||||
sat: Satellite, pos: GeoPos, start: Long, end: Long
|
||||
): List<SatPos> {
|
||||
return withContext(dispatcher) {
|
||||
val positions = mutableListOf<SatPos>()
|
||||
var currentTime = start
|
||||
while (currentTime < end) {
|
||||
positions.add(sat.getPosition(pos, currentTime))
|
||||
currentTime += 15000
|
||||
}
|
||||
positions
|
||||
}
|
||||
}
|
||||
|
||||
override suspend fun processRadios(
|
||||
sat: Satellite, pos: GeoPos, radios: List<SatRadio>, time: Long
|
||||
): List<SatRadio> {
|
||||
return withContext(dispatcher) {
|
||||
val satPos = sat.getPosition(pos, time)
|
||||
val copiedList = radios.map { it.copy() }
|
||||
copiedList.forEach { transmitter ->
|
||||
transmitter.downlink?.let { transmitter.downlink = satPos.getDownlinkFreq(it) }
|
||||
transmitter.uplink?.let { transmitter.uplink = satPos.getUplinkFreq(it) }
|
||||
}
|
||||
copiedList.map { it.copy() }
|
||||
}
|
||||
}
|
||||
|
||||
override suspend fun processPasses(passList: List<SatPass>, time: Long): List<SatPass> {
|
||||
return withContext(dispatcher) {
|
||||
passList.forEach { pass ->
|
||||
if (!pass.isDeepSpace) {
|
||||
val timeStart = pass.aosTime
|
||||
if (time > timeStart) {
|
||||
val deltaNow = time.minus(timeStart).toFloat()
|
||||
val deltaTotal = pass.losTime.minus(timeStart).toFloat()
|
||||
pass.progress = (deltaNow / deltaTotal).round(2)
|
||||
}
|
||||
}
|
||||
}
|
||||
passList.filter { pass -> pass.progress < 1.0 }.map { it.copy() }
|
||||
}
|
||||
}
|
||||
|
||||
override suspend fun calculatePasses(time: Long, hoursAhead: Int, minElevation: Double) {
|
||||
if (_satellites.value.isNotEmpty()) {
|
||||
withContext(dispatcher) {
|
||||
val allPasses = mutableListOf<SatPass>()
|
||||
val stationPos = settingsRepo.stationPosition.value
|
||||
_satellites.value.forEach { satellite ->
|
||||
allPasses.addAll(satellite.getPasses(stationPos, time, hoursAhead))
|
||||
}
|
||||
_passes.update { allPasses.filter(time, hoursAhead, minElevation) }
|
||||
}
|
||||
} else {
|
||||
_passes.update { emptyList() }
|
||||
}
|
||||
}
|
||||
|
||||
private fun Satellite.getPasses(pos: GeoPos, time: Long, hours: Int): List<SatPass> {
|
||||
val passes = mutableListOf<SatPass>()
|
||||
val endDate = time + hours * 60L * 60L * 1000L
|
||||
val quarterOrbitMin = (this.data.orbitalPeriod / 4.0).toInt()
|
||||
var startDate = time
|
||||
var shouldRewind = true
|
||||
var lastAosDate: Long
|
||||
var count = 0
|
||||
if (this.willBeSeen(pos)) {
|
||||
if (this.data.isDeepSpace) {
|
||||
passes.add(getGeoPass(this, pos, time))
|
||||
} else {
|
||||
do {
|
||||
if (count > 0) shouldRewind = false
|
||||
val pass = getLeoPass(this, pos, startDate, shouldRewind)
|
||||
lastAosDate = pass.aosTime
|
||||
passes.add(pass)
|
||||
startDate = pass.losTime + (quarterOrbitMin * 3) * 60L * 1000L
|
||||
count++
|
||||
} while (lastAosDate < endDate)
|
||||
}
|
||||
}
|
||||
return passes
|
||||
}
|
||||
|
||||
private fun List<SatPass>.filter(time: Long, hoursAhead: Int, minElev: Double): List<SatPass> {
|
||||
val timeFuture = time + (hoursAhead * 60L * 60L * 1000L)
|
||||
return this.filter { it.losTime > time }.filter { it.aosTime < timeFuture }
|
||||
.filter { it.maxElevation > minElev }.sortedBy { it.aosTime }
|
||||
}
|
||||
|
||||
private fun getGeoPass(sat: Satellite, pos: GeoPos, time: Long): SatPass {
|
||||
val satPos = sat.getPosition(pos, time)
|
||||
val aos = time - 24 * 60L * 60L * 1000L
|
||||
val los = time + 24 * 60L * 60L * 1000L // val tca = (aos + los) / 2
|
||||
val az = satPos.azimuth.toDegrees().round(1)
|
||||
val elev = satPos.elevation.toDegrees().round(1)
|
||||
val alt = satPos.altitude
|
||||
return SatPass(aos, az, los, az, alt.toInt(), elev, sat)
|
||||
}
|
||||
|
||||
private fun getLeoPass(sat: Satellite, pos: GeoPos, time: Long, rewind: Boolean): SatPass {
|
||||
val quarterOrbitMin = (sat.data.orbitalPeriod / 4.0).toInt()
|
||||
var calendarTimeMillis = time
|
||||
var elevation: Double
|
||||
var maxElevation = 0.0
|
||||
var alt = 0.0 // var tcaAz = 0.0
|
||||
// rewind 1/4 of an orbit
|
||||
if (rewind) calendarTimeMillis += -quarterOrbitMin * 60L * 1000L
|
||||
|
||||
var satPos = sat.getPosition(pos, calendarTimeMillis)
|
||||
if (satPos.elevation > 0.0) {
|
||||
// move forward in 30 second intervals until the sat goes below the horizon
|
||||
do {
|
||||
calendarTimeMillis += 30 * 1000L
|
||||
satPos = sat.getPosition(pos, calendarTimeMillis)
|
||||
} while (satPos.elevation > 0.0)
|
||||
// move forward 3/4 of an orbit
|
||||
calendarTimeMillis += quarterOrbitMin * 3 * 60L * 1000L
|
||||
}
|
||||
|
||||
// find the next time sat comes above the horizon
|
||||
do {
|
||||
calendarTimeMillis += 60L * 1000L
|
||||
satPos = sat.getPosition(pos, calendarTimeMillis)
|
||||
elevation = satPos.elevation
|
||||
if (elevation > maxElevation) {
|
||||
maxElevation = elevation
|
||||
alt = satPos.altitude // tcaAz = satPos.azimuth.toDegrees()
|
||||
}
|
||||
} while (satPos.elevation < 0.0)
|
||||
|
||||
// refine to 3 seconds
|
||||
calendarTimeMillis += -60L * 1000L
|
||||
do {
|
||||
calendarTimeMillis += 3L * 1000L
|
||||
satPos = sat.getPosition(pos, calendarTimeMillis)
|
||||
elevation = satPos.elevation
|
||||
if (elevation > maxElevation) {
|
||||
maxElevation = elevation
|
||||
alt = satPos.altitude // tcaAz = satPos.azimuth.toDegrees()
|
||||
}
|
||||
} while (satPos.elevation < 0.0)
|
||||
|
||||
val aos = satPos.time
|
||||
val aosAz = satPos.azimuth.toDegrees().round(1)
|
||||
|
||||
// find when sat goes below
|
||||
do {
|
||||
calendarTimeMillis += 30L * 1000L
|
||||
satPos = sat.getPosition(pos, calendarTimeMillis)
|
||||
elevation = satPos.elevation
|
||||
if (elevation > maxElevation) {
|
||||
maxElevation = elevation
|
||||
alt = satPos.altitude // tcaAz = satPos.azimuth.toDegrees()
|
||||
}
|
||||
} while (satPos.elevation > 0.0)
|
||||
|
||||
// refine to 3 seconds
|
||||
calendarTimeMillis += -30L * 1000L
|
||||
do {
|
||||
calendarTimeMillis += 3L * 1000L
|
||||
satPos = sat.getPosition(pos, calendarTimeMillis)
|
||||
elevation = satPos.elevation
|
||||
if (elevation > maxElevation) {
|
||||
maxElevation = elevation
|
||||
alt = satPos.altitude // tcaAz = satPos.azimuth.toDegrees()
|
||||
}
|
||||
} while (satPos.elevation > 0.0)
|
||||
|
||||
val los = satPos.time // val tca = (aos + los) / 2
|
||||
val losAz = satPos.azimuth.toDegrees().round(1)
|
||||
val elev = maxElevation.toDegrees().round(1)
|
||||
return SatPass(aos, aosAz, los, losAz, alt.toInt(), elev, sat)
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,81 @@
|
||||
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 kotlinx.coroutines.CoroutineDispatcher
|
||||
import kotlinx.coroutines.ExperimentalCoroutinesApi
|
||||
import kotlinx.coroutines.flow.MutableStateFlow
|
||||
import kotlinx.coroutines.flow.first
|
||||
import kotlinx.coroutines.flow.flatMapLatest
|
||||
import kotlinx.coroutines.flow.map
|
||||
import kotlinx.coroutines.withContext
|
||||
|
||||
@OptIn(ExperimentalCoroutinesApi::class)
|
||||
class SelectionRepo(
|
||||
private val dispatcher: CoroutineDispatcher,
|
||||
private val localStorage: ILocalStorage,
|
||||
private val settingsRepo: ISettingsRepo
|
||||
) : ISelectionRepo {
|
||||
private val currentItems = MutableStateFlow<List<SatItem>>(emptyList())
|
||||
private val currentType = MutableStateFlow("All")
|
||||
private val currentQuery = MutableStateFlow("")
|
||||
private val itemsWithType = currentType.flatMapLatest { type ->
|
||||
currentItems.map { items -> items.filterByType(type) }
|
||||
}
|
||||
private val itemsWithQuery = currentQuery.flatMapLatest { query ->
|
||||
itemsWithType.map { items -> items.filterByQuery(query) }
|
||||
}
|
||||
|
||||
override fun getCurrentType() = currentType.value
|
||||
|
||||
override fun getTypesList() = settingsRepo.satelliteSourcesMap.keys.sorted()
|
||||
|
||||
override suspend fun getEntriesFlow() = withContext(dispatcher) {
|
||||
val selectedIds = settingsRepo.satelliteSelection.value
|
||||
currentItems.value = localStorage.getEntriesList().map { item ->
|
||||
item.copy(isSelected = item.catnum in selectedIds)
|
||||
}
|
||||
return@withContext itemsWithQuery
|
||||
}
|
||||
|
||||
override suspend fun setType(type: String) = withContext(dispatcher) {
|
||||
currentType.value = type
|
||||
}
|
||||
|
||||
override suspend fun setQuery(query: String) = withContext(dispatcher) {
|
||||
currentQuery.value = query
|
||||
}
|
||||
|
||||
override suspend fun setSelection(selectAll: Boolean) = withContext(dispatcher) {
|
||||
setSelection(itemsWithQuery.first().map { item -> item.catnum }, selectAll)
|
||||
}
|
||||
|
||||
override suspend fun setSelection(ids: List<Int>, isTicked: Boolean) = withContext(dispatcher) {
|
||||
currentItems.value = currentItems.value.map { item ->
|
||||
if (item.catnum in ids) item.copy(isSelected = isTicked) else item
|
||||
}
|
||||
}
|
||||
|
||||
override suspend fun saveSelection() = withContext(dispatcher) {
|
||||
val currentSelection = currentItems.value.filter { it.isSelected }.map { it.catnum }
|
||||
settingsRepo.saveEntriesSelection(currentSelection)
|
||||
}
|
||||
|
||||
private suspend fun List<SatItem>.filterByType(type: String) = withContext(dispatcher) {
|
||||
if (type == "All") return@withContext this@filterByType
|
||||
val catnums = settingsRepo.loadSatType(type)
|
||||
if (catnums.isEmpty()) return@withContext this@filterByType
|
||||
return@withContext this@filterByType.filter { item -> item.catnum in catnums }
|
||||
}
|
||||
|
||||
private suspend fun List<SatItem>.filterByQuery(query: String) = withContext(dispatcher) {
|
||||
if (query.isBlank()) return@withContext this@filterByQuery
|
||||
return@withContext try {
|
||||
this@filterByQuery.filter { it.catnum == query.toInt() }
|
||||
} catch (e: Exception) {
|
||||
this@filterByQuery.filter { item -> item.name.lowercase().contains(query.lowercase()) }
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,47 @@
|
||||
/*
|
||||
* 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.data.source
|
||||
|
||||
import android.content.ContentResolver
|
||||
import android.net.Uri
|
||||
import com.rtbishop.look4sat.domain.source.IDataSource
|
||||
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 {
|
||||
|
||||
override suspend fun getFileStream(uri: String): InputStream? {
|
||||
return withContext(dispatcher) { contentResolver.openInputStream(Uri.parse(uri)) }
|
||||
}
|
||||
|
||||
override suspend fun getNetworkStream(url: String): InputStream? = withContext(dispatcher) {
|
||||
try {
|
||||
val request = Request.Builder().url(url).build()
|
||||
httpClient.newCall(request).execute().body?.byteStream()
|
||||
} catch (exception: Exception) {
|
||||
null
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,95 @@
|
||||
/*
|
||||
* 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.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
|
||||
|
||||
class LocalStorage(private val storageDao: StorageDao) : ILocalStorage {
|
||||
|
||||
//region # Entries region
|
||||
|
||||
override suspend fun getEntriesTotal() = storageDao.getEntriesTotal()
|
||||
|
||||
override suspend fun getEntriesList(): List<DomainItem> {
|
||||
return storageDao.getEntriesList().toDomainItems()
|
||||
}
|
||||
|
||||
override suspend fun getEntriesWithIds(ids: List<Int>): List<Satellite> {
|
||||
val selectedSatellites = mutableListOf<Satellite>()
|
||||
ids.chunked(999).forEach { idsPart ->
|
||||
val entries = storageDao.getEntriesWithIds(idsPart)
|
||||
selectedSatellites.addAll(entries.map { entry -> entry.data.getSatellite() })
|
||||
}
|
||||
return selectedSatellites
|
||||
}
|
||||
|
||||
override suspend fun insertEntries(entries: List<DomainEntry>) {
|
||||
storageDao.insertEntries(entries.toFrameworkEntries())
|
||||
}
|
||||
|
||||
override suspend fun deleteEntries() = storageDao.deleteEntries()
|
||||
|
||||
private fun DomainEntry.toFramework() = FrameworkEntry(this.data, this.comment)
|
||||
|
||||
private fun FrameworkItem.toDomain() = DomainItem(this.catnum, this.name, false)
|
||||
|
||||
private fun List<DomainEntry>.toFrameworkEntries() = this.map { entry -> entry.toFramework() }
|
||||
|
||||
private fun List<FrameworkItem>.toDomainItems() = this.map { item -> item.toDomain() }
|
||||
|
||||
//endregion
|
||||
|
||||
//region # Radios region
|
||||
|
||||
override suspend fun getRadiosTotal() = storageDao.getRadiosTotal()
|
||||
|
||||
override suspend fun getRadiosWithId(id: Int): List<SatRadio> {
|
||||
return storageDao.getRadiosWithId(id).toDomainRadios()
|
||||
}
|
||||
|
||||
override suspend fun insertRadios(radios: List<SatRadio>) {
|
||||
storageDao.insertRadios(radios.toFrameworkRadios())
|
||||
}
|
||||
|
||||
override suspend fun deleteRadios() = storageDao.deleteRadios()
|
||||
|
||||
private fun DomainRadio.toFramework() = FrameworkRadio(
|
||||
this.uuid, this.info, this.isAlive, this.downlink, this.uplink,
|
||||
this.mode, this.isInverted, this.catnum, this.comment
|
||||
)
|
||||
|
||||
private fun FrameworkRadio.toDomain() = DomainRadio(
|
||||
this.uuid, this.info, this.isAlive, this.downlink, this.uplink,
|
||||
this.mode, this.isInverted, this.catnum, this.comment
|
||||
)
|
||||
|
||||
private fun List<DomainRadio>.toFrameworkRadios() = this.map { radio -> radio.toFramework() }
|
||||
|
||||
private fun List<FrameworkRadio>.toDomainRadios() = this.map { radio -> radio.toDomain() }
|
||||
|
||||
//endregion
|
||||
}
|
||||
@@ -0,0 +1,66 @@
|
||||
/*
|
||||
* 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.data.source
|
||||
|
||||
import android.hardware.Sensor
|
||||
import android.hardware.SensorEvent
|
||||
import android.hardware.SensorEventListener
|
||||
import android.hardware.SensorManager
|
||||
import com.rtbishop.look4sat.domain.predict.RAD2DEG
|
||||
import com.rtbishop.look4sat.domain.source.ISensorSource
|
||||
import kotlinx.coroutines.flow.MutableStateFlow
|
||||
import kotlinx.coroutines.flow.StateFlow
|
||||
import kotlin.math.round
|
||||
|
||||
class SensorSource(private val sensorManager: SensorManager, private val sensor: Sensor?) :
|
||||
SensorEventListener, ISensorSource {
|
||||
|
||||
private val _orientation = MutableStateFlow(Pair(0f, 0f))
|
||||
private val rotationMatrix = FloatArray(9)
|
||||
private val orientationValues = FloatArray(3)
|
||||
private var sensorAccuracy = SensorManager.SENSOR_STATUS_UNRELIABLE
|
||||
|
||||
override val orientation: StateFlow<Pair<Float, Float>> = _orientation
|
||||
|
||||
override fun enableSensor() {
|
||||
sensor?.let { sensorManager.registerListener(this, it, 8000) }
|
||||
}
|
||||
|
||||
override fun disableSensor() = sensorManager.unregisterListener(this)
|
||||
|
||||
override fun onAccuracyChanged(sensor: Sensor, accuracy: Int) {
|
||||
sensorAccuracy = accuracy
|
||||
}
|
||||
|
||||
override fun onSensorChanged(event: SensorEvent) = when {
|
||||
sensorAccuracy == SensorManager.SENSOR_STATUS_UNRELIABLE -> {}
|
||||
event.sensor == sensor -> updateOrientation(event.values)
|
||||
else -> {}
|
||||
}
|
||||
|
||||
private fun updateOrientation(rotationVector: FloatArray) {
|
||||
SensorManager.getRotationMatrixFromVector(rotationMatrix, rotationVector)
|
||||
SensorManager.getOrientation(rotationMatrix, orientationValues)
|
||||
val azimuth = (orientationValues[0] * RAD2DEG).toFloat()
|
||||
val pitch = (orientationValues[1] * RAD2DEG).toFloat() // roll [2]
|
||||
val magneticAzimuth = (azimuth + 360f) % 360f
|
||||
val roundedAzimuth = round(magneticAzimuth * 10) / 10
|
||||
val roundedPitch = round(pitch * 10) / 10
|
||||
_orientation.value = Pair(roundedAzimuth, roundedPitch)
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,115 @@
|
||||
/*
|
||||
* 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.data
|
||||
|
||||
import com.rtbishop.look4sat.data.repository.DataParser
|
||||
import kotlinx.coroutines.ExperimentalCoroutinesApi
|
||||
import kotlinx.coroutines.test.StandardTestDispatcher
|
||||
import kotlinx.coroutines.test.runTest
|
||||
import org.junit.Test
|
||||
|
||||
@ExperimentalCoroutinesApi
|
||||
class DataParserTest {
|
||||
|
||||
private val testDispatcher = StandardTestDispatcher()
|
||||
private val dataParser = DataParser(testDispatcher)
|
||||
private val validCSVStream = """
|
||||
OBJECT_NAME,OBJECT_ID,EPOCH,MEAN_MOTION,ECCENTRICITY,INCLINATION,RA_OF_ASC_NODE,ARG_OF_PERICENTER,MEAN_ANOMALY,EPHEMERIS_TYPE,CLASSIFICATION_TYPE,NORAD_CAT_ID,ELEMENT_SET_NO,REV_AT_EPOCH,BSTAR,MEAN_MOTION_DOT,MEAN_MOTION_DDOT
|
||||
ISS (ZARYA),1998-067A,2021-11-16T12:28:09.322176,15.48582035,.0004694,51.6447,309.4881,203.6966,299.8876,0,U,25544,999,31220,.31985E-4,.1288E-4,0
|
||||
ISS (ZARYA),1998-067A,2024-03-09T05:45:04.737024,15.49756209,.0005741,51.6418,90.7424,343.9724,92.8274,0,U,25544,999,44305,.25016E-3,.1373E-3,0
|
||||
FLTSATCOM 8 (USA 46),1989-077A,2022-01-07T11:37:38.074080,1.00273350,.0001114,12.9044,1.3272,91.5769,260.4200,0,U,20253,999,24434,0,-.85E-6,0
|
||||
""".trimIndent().byteInputStream()
|
||||
private val invalidCSVStream = """
|
||||
ISS (ZARYA),1998-067A,2021-11-16T12:28:09.322176,15.48582035,.0004694,51.6447,309.4881,203.6966,299.8876,0,U,25544,999,31220,.31985E-4,.1288E-4,0
|
||||
OBJECT_NAME,OBJECT_ID,EPOCH,MEAN_MOTION,ECCENTRICITY,INCLINATION,RA_OF_ASC_NODE,ARG_OF_PERICENTER,MEAN_ANOMALY,EPHEMERIS_TYPE,CLASSIFICATION_TYPE,NORAD_CAT_ID,ELEMENT_SET_NO,REV_AT_EPOCH,BSTAR,MEAN_MOTION_DOT,MEAN_MOTION_DDOT
|
||||
""".trimIndent().byteInputStream()
|
||||
private val validTLEStream = """
|
||||
ISS (ZARYA)
|
||||
1 25544U 98067A 21320.51955234 .00001288 00000+0 31985-4 0 9990
|
||||
2 25544 51.6447 309.4881 0004694 203.6966 299.8876 15.48582035312205
|
||||
ISS (ZARYA)
|
||||
1 25544U 98067A 24069.23963816 .00013730 00000+0 25016-3 0 9999
|
||||
2 25544 51.6418 90.7424 0005741 343.9724 92.8274 15.49756209443058
|
||||
FLTSATCOM 8 (USA 46)
|
||||
1 20253U 89077A 22007.48446845 -.00000085 00000+0 00000+0 0 9999
|
||||
2 20253 12.9044 1.3272 0001114 91.5769 260.4200 1.00273350244345
|
||||
""".trimIndent().byteInputStream()
|
||||
private val invalidTLEStream = """
|
||||
1 25544U 98067A 21320.51955234 .00001288 00000+0 31985-4 0 9990
|
||||
2 25544 51.6447 309.4881 0004694 203.6966 299.8876 15.48582035312205
|
||||
""".trimIndent().byteInputStream()
|
||||
private val validJSONStream = """
|
||||
[{"uuid":"UzPz4gcsNBPKPKAFPmer7g","description":"Upper side band (drifting)","alive":true,"type":"Transmitter","uplink_low":null,"uplink_high":null,"uplink_drift":null,"downlink_low":136658500,"downlink_high":null,"downlink_drift":null,"mode":"USB","mode_id":9,"uplink_mode":null,"invert":false,"baud":null,"sat_id":"SCHX-0895-2361-9925-0309","norad_cat_id":965,"status":"active","updated":"2019-04-18T05:39:53.343316Z","citation":"CITATION NEEDED - https://xkcd.com/285/","service":"Unknown","coordination":"","coordination_url":""}]
|
||||
""".trimIndent().byteInputStream()
|
||||
private val invalidJSONStream = """
|
||||
[{"description":"Upper side band (drifting)","alive":true,"type":"Transmitter","uplink_low":null,"uplink_high":null,"uplink_drift":null,"downlink_low":136658500,"downlink_high":null,"downlink_drift":null,"mode":"USB","mode_id":9,"uplink_mode":null,"invert":false,"baud":null,"sat_id":"SCHX-0895-2361-9925-0309","norad_cat_id":965,"status":"active","updated":"2019-04-18T05:39:53.343316Z","citation":"CITATION NEEDED - https://xkcd.com/285/","service":"Unknown","coordination":"","coordination_url":""}]
|
||||
""".trimIndent().byteInputStream()
|
||||
|
||||
@Test
|
||||
fun `Given valid CSV stream returns valid data`() = runTest(testDispatcher) {
|
||||
val parsedList = dataParser.parseCSVStream(validCSVStream)
|
||||
assert(parsedList[0].epoch == 21320.51955234)
|
||||
assert(parsedList[1].epoch == 24069.23963816)
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `Given invalid CSV stream returns empty list`() = runTest(testDispatcher) {
|
||||
val parsedList = dataParser.parseCSVStream(invalidCSVStream)
|
||||
assert(parsedList.isEmpty())
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `Given valid TLE stream returns valid data`() = runTest(testDispatcher) {
|
||||
val parsedList = dataParser.parseTLEStream(validTLEStream)
|
||||
assert(parsedList[0].epoch == 21320.51955234)
|
||||
assert(parsedList[1].epoch == 24069.23963816)
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `Given invalid TLE stream returns empty list`() = runTest(testDispatcher) {
|
||||
val parsedList = dataParser.parseTLEStream(invalidTLEStream)
|
||||
assert(parsedList.isEmpty())
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `Given valid data streams parsed results match`() = runTest(testDispatcher) {
|
||||
val csvResult = dataParser.parseCSVStream(validCSVStream)
|
||||
val tleResult = dataParser.parseTLEStream(validTLEStream)
|
||||
assert(csvResult == tleResult)
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `Given valid JSON stream returns valid data`() = runTest(testDispatcher) {
|
||||
val parsedList = dataParser.parseJSONStream(validJSONStream)
|
||||
assert(parsedList[0].downlink == 136658500L)
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `Given invalid JSON stream returns empty list`() = runTest(testDispatcher) {
|
||||
val parsedList = dataParser.parseJSONStream(invalidJSONStream)
|
||||
assert(parsedList.isEmpty())
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `Function isLeapYear returns correct data`() = runTest(testDispatcher) {
|
||||
val years = listOf(1900, 1984, 1994, 2016, 2022, 2024, 2042, 2048)
|
||||
val answers = listOf(false, true, false, true, false, true, false, true)
|
||||
val results = years.map { dataParser.isLeapYear(it) }
|
||||
assert(results == answers)
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,16 @@
|
||||
package com.rtbishop.look4sat.data
|
||||
|
||||
import org.junit.Assert.assertEquals
|
||||
import org.junit.Test
|
||||
|
||||
/**
|
||||
* Example local unit test, which will execute on the development machine (host).
|
||||
*
|
||||
* See [testing documentation](http://d.android.com/tools/testing).
|
||||
*/
|
||||
class ExampleUnitTest {
|
||||
@Test
|
||||
fun addition_isCorrect() {
|
||||
assertEquals(4, 2 + 2)
|
||||
}
|
||||
}
|
||||
Reference in new issue
Block a user