Added basic convention plugins setup for modularization
This commit is contained in:
1 parent
de99e09f5e
commit
4d88214c1d
5675 files changed
+10339
-9969
No files matched your search
@@ -0,0 +1 @@
|
||||
/build
|
||||
@@ -0,0 +1,7 @@
|
||||
plugins {
|
||||
alias(libs.plugins.convention.androidLibraryPlugin)
|
||||
}
|
||||
|
||||
android {
|
||||
namespace = "com.rtbishop.look4sat.core.data"
|
||||
}
|
||||
@@ -0,0 +1,15 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<manifest xmlns:android="http://schemas.android.com/apk/res/android">
|
||||
|
||||
<uses-permission android:name="android.permission.ACCESS_NETWORK_STATE" />
|
||||
<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_ADMIN" />
|
||||
<uses-permission android:name="android.permission.BLUETOOTH_CONNECT" />
|
||||
<uses-permission android:name="android.permission.BLUETOOTH_SCAN" />
|
||||
<uses-permission android:name="android.permission.INTERNET" />
|
||||
|
||||
</manifest>
|
||||
@@ -0,0 +1,63 @@
|
||||
/*
|
||||
* Look4Sat. Amateur radio satellite tracker and pass predictor.
|
||||
* Copyright (C) 2019-2026 Arty Bishop and contributors.
|
||||
*
|
||||
* 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.core.data.database
|
||||
|
||||
import androidx.room.Dao
|
||||
import androidx.room.Insert
|
||||
import androidx.room.OnConflictStrategy
|
||||
import androidx.room.Query
|
||||
import androidx.room.Transaction
|
||||
import com.rtbishop.look4sat.core.data.database.entity.SatEntry
|
||||
import com.rtbishop.look4sat.core.data.database.entity.SatRadio
|
||||
import com.rtbishop.look4sat.core.domain.model.SatItem
|
||||
|
||||
@Dao
|
||||
interface Look4SatDao {
|
||||
|
||||
@Query("SELECT COUNT(*) FROM entries")
|
||||
suspend fun getEntriesTotal(): Int
|
||||
|
||||
@Query("SELECT catnum, name, 0 as isSelected 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 catnum FROM radios WHERE downlinkMode IN (:modes)")
|
||||
suspend fun getIdsWithModes(modes: List<String>): List<Int>
|
||||
|
||||
@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,37 @@
|
||||
/*
|
||||
* Look4Sat. Amateur radio satellite tracker and pass predictor.
|
||||
* Copyright (C) 2019-2026 Arty Bishop and contributors.
|
||||
*
|
||||
* 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.core.data.database
|
||||
|
||||
import androidx.room.Database
|
||||
import androidx.room.RoomDatabase
|
||||
import com.rtbishop.look4sat.core.data.database.entity.SatEntry
|
||||
import com.rtbishop.look4sat.core.data.database.entity.SatRadio
|
||||
|
||||
@Database(entities = [SatEntry::class, SatRadio::class], version = 1, exportSchema = false)
|
||||
abstract class Look4SatDb : RoomDatabase() {
|
||||
abstract fun look4SatDao(): Look4SatDao
|
||||
}
|
||||
|
||||
//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,34 @@
|
||||
/*
|
||||
* Look4Sat. Amateur radio satellite tracker and pass predictor.
|
||||
* Copyright (C) 2019-2026 Arty Bishop and contributors.
|
||||
*
|
||||
* 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.core.data.database.entity
|
||||
|
||||
import androidx.room.Entity
|
||||
|
||||
@Entity(tableName = "entries", primaryKeys = ["catnum"])
|
||||
data class SatEntry(
|
||||
val name: String,
|
||||
val epoch: Double,
|
||||
val meanmo: Double,
|
||||
val eccn: Double,
|
||||
val incl: Double,
|
||||
val raan: Double,
|
||||
val argper: Double,
|
||||
val meanan: Double,
|
||||
val catnum: Int,
|
||||
val bstar: Double
|
||||
)
|
||||
@@ -0,0 +1,36 @@
|
||||
/*
|
||||
* Look4Sat. Amateur radio satellite tracker and pass predictor.
|
||||
* Copyright (C) 2019-2026 Arty Bishop and contributors.
|
||||
*
|
||||
* 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.core.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 downlinkLow: Long?,
|
||||
var downlinkHigh: Long?,
|
||||
val downlinkMode: String?,
|
||||
var uplinkLow: Long?,
|
||||
var uplinkHigh: Long?,
|
||||
val uplinkMode: String?,
|
||||
val isInverted: Boolean,
|
||||
val catnum: Int?
|
||||
)
|
||||
+140
@@ -0,0 +1,140 @@
|
||||
/*
|
||||
* Look4Sat. Amateur radio satellite tracker and pass predictor.
|
||||
* Copyright (C) 2019-2026 Arty Bishop and contributors.
|
||||
*
|
||||
* 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.core.data.framework
|
||||
|
||||
import android.bluetooth.BluetoothManager
|
||||
import android.bluetooth.BluetoothSocket
|
||||
import android.util.Log
|
||||
import com.rtbishop.look4sat.core.domain.repository.BtService
|
||||
import com.rtbishop.look4sat.core.domain.repository.IReporterRepo
|
||||
import com.rtbishop.look4sat.core.domain.repository.WithoutExtParams
|
||||
import kotlinx.coroutines.CoroutineScope
|
||||
import kotlinx.coroutines.Job
|
||||
import kotlinx.coroutines.launch
|
||||
import kotlinx.coroutines.sync.Mutex
|
||||
import kotlinx.coroutines.sync.withLock
|
||||
import java.io.OutputStream
|
||||
import java.util.UUID
|
||||
import kotlin.math.abs
|
||||
|
||||
data class DeviceConnection(
|
||||
var socket: BluetoothSocket? = null,
|
||||
var outputStream: OutputStream? = null,
|
||||
var connected: Boolean = false,
|
||||
var connecting: Boolean = false,
|
||||
var connectionJob: Job? = null
|
||||
)
|
||||
|
||||
class BluetoothReporter(
|
||||
private val bluetoothManager: BluetoothManager,
|
||||
private val reporterScope: CoroutineScope
|
||||
) : IReporterRepo<WithoutExtParams> {
|
||||
|
||||
private val tag = "BTReporter"
|
||||
private val sppid: UUID = UUID.fromString("00001101-0000-1000-8000-00805f9b34fb")
|
||||
|
||||
private val serviceToDevice = mutableMapOf<BtService, String>()
|
||||
private val deviceConnections = mutableMapOf<String, DeviceConnection>()
|
||||
private val writeMutex = Mutex()
|
||||
|
||||
override fun isConnected(service: BtService): Boolean {
|
||||
val deviceId = serviceToDevice[service] ?: return false
|
||||
return deviceConnections[deviceId]?.connected == true
|
||||
}
|
||||
|
||||
override fun isConnecting(service: BtService): Boolean {
|
||||
val deviceId = serviceToDevice[service] ?: return false
|
||||
return deviceConnections[deviceId]?.connecting == true
|
||||
}
|
||||
|
||||
override fun connect(service: BtService, deviceId: String) {
|
||||
serviceToDevice[service] = deviceId
|
||||
val connection = deviceConnections.getOrPut(deviceId) {
|
||||
DeviceConnection()
|
||||
}
|
||||
if (connection.connected || connection.connecting) return
|
||||
connection.connectionJob = reporterScope.launch {
|
||||
try {
|
||||
connection.connecting = true
|
||||
val device = bluetoothManager.adapter.getRemoteDevice(deviceId)
|
||||
val socket = device.createInsecureRfcommSocketToServiceRecord(sppid)
|
||||
socket.connect()
|
||||
connection.socket = socket
|
||||
connection.outputStream = socket.outputStream
|
||||
connection.connected = true
|
||||
Log.i(tag, "$tag: Connected to $deviceId")
|
||||
} catch (e: Exception) {
|
||||
Log.e(tag, "$tag: ${e.message}")
|
||||
connection.connected = false
|
||||
} finally {
|
||||
connection.connecting = false
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private suspend fun write(service: BtService, buffer: String) {
|
||||
val deviceId = serviceToDevice[service] ?: return
|
||||
val connection = deviceConnections[deviceId] ?: return
|
||||
if (!connection.connected) return
|
||||
try {
|
||||
writeMutex.withLock {
|
||||
connection.outputStream?.write(buffer.toByteArray())
|
||||
}
|
||||
} catch (e: Exception) {
|
||||
Log.e(tag, "$tag: Write failed ${e.message}")
|
||||
connection.connected = false
|
||||
}
|
||||
}
|
||||
|
||||
override fun reportRotation(format: String, azimuth: Double, elevation: Double, params: WithoutExtParams) {
|
||||
reporterScope.launch {
|
||||
if (!isConnected(BtService.ROTATOR)) return@launch
|
||||
val newElevation = if (elevation > 0.0) elevation else 0.0
|
||||
val azimuthString = intToStringWithLeadingZeroes(azimuth.toInt())
|
||||
val elevationString = intToStringWithLeadingZeroes(newElevation.toInt())
|
||||
val buffer = format
|
||||
.replace($$"$AZ", azimuthString)
|
||||
.replace($$"$EL", elevationString)
|
||||
.replace("\\r", "\r")
|
||||
.replace("\\n", "\n")
|
||||
.replace("\\t", "\t")
|
||||
write(BtService.ROTATOR, buffer)
|
||||
}
|
||||
}
|
||||
|
||||
override fun reportFrequency(format: String, frequency: Long, params: WithoutExtParams) {
|
||||
reporterScope.launch {
|
||||
if (!isConnected(BtService.FREQUENCY)) return@launch
|
||||
val buffer = format
|
||||
.replace($$"$FREQ", frequency.toString())
|
||||
.replace("\\r", "\r")
|
||||
.replace("\\n", "\n")
|
||||
.replace("\\t", "\t")
|
||||
write(BtService.FREQUENCY, buffer)
|
||||
}
|
||||
}
|
||||
|
||||
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,117 @@
|
||||
/*
|
||||
* Look4Sat. Amateur radio satellite tracker and pass predictor.
|
||||
* Copyright (C) 2019-2026 Arty Bishop and contributors.
|
||||
*
|
||||
* 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.core.data.framework
|
||||
|
||||
import com.rtbishop.look4sat.core.domain.repository.BtService
|
||||
import com.rtbishop.look4sat.core.domain.repository.ExtendedParams
|
||||
import com.rtbishop.look4sat.core.domain.repository.IReporterRepo
|
||||
import kotlinx.coroutines.CoroutineScope
|
||||
import kotlinx.coroutines.Job
|
||||
import kotlinx.coroutines.launch
|
||||
import kotlinx.coroutines.sync.Mutex
|
||||
import kotlinx.coroutines.sync.withLock
|
||||
import java.net.InetSocketAddress
|
||||
import java.nio.ByteBuffer
|
||||
import java.nio.channels.SocketChannel
|
||||
|
||||
enum class NetworkService { ROTATOR, FREQUENCY }
|
||||
|
||||
data class SocketConnection(
|
||||
var socket: SocketChannel? = null,
|
||||
var connected: Boolean = false,
|
||||
var connecting: Boolean = false,
|
||||
var connectionJob: Job? = null
|
||||
)
|
||||
|
||||
class NetworkReporter(private val reporterScope: CoroutineScope) : IReporterRepo<ExtendedParams> {
|
||||
private val serviceToAddress = mutableMapOf<NetworkService, String>()
|
||||
private val connections = mutableMapOf<String, SocketConnection>()
|
||||
private val writeMutex = Mutex()
|
||||
|
||||
fun isConnected(service: NetworkService): Boolean {
|
||||
val addr = serviceToAddress[service] ?: return false
|
||||
return connections[addr]?.connected == true
|
||||
}
|
||||
|
||||
private fun getOrCreateConnection(addr: String): SocketConnection {
|
||||
return connections.getOrPut(addr) { SocketConnection() }
|
||||
}
|
||||
|
||||
fun connect(service: NetworkService, server: String, port: Int) {
|
||||
val addr = "$server:$port"
|
||||
serviceToAddress[service] = addr
|
||||
val connection = getOrCreateConnection(addr)
|
||||
if (connection.connected || connection.connecting) return
|
||||
connection.connectionJob = reporterScope.launch {
|
||||
try {
|
||||
connection.connecting = true
|
||||
val socket = SocketChannel.open(InetSocketAddress(server, port))
|
||||
connection.socket = socket
|
||||
connection.connected = true
|
||||
println("NetworkReporter: $service connected to $addr")
|
||||
} catch (e: Exception) {
|
||||
println("NetworkReporter connect error: ${e.message}")
|
||||
connection.connected = false
|
||||
} finally {
|
||||
connection.connecting = false
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private suspend fun write(service: NetworkService, command: String) {
|
||||
val addr = serviceToAddress[service] ?: return
|
||||
val connection = connections[addr] ?: return
|
||||
if (!connection.connected) return
|
||||
try {
|
||||
writeMutex.withLock {
|
||||
val buffer = ByteBuffer.wrap("\\$command\n".toByteArray())
|
||||
connection.socket?.write(buffer)
|
||||
}
|
||||
} catch (e: Exception) {
|
||||
println("NetworkReporter write error: ${e.message}")
|
||||
connection.connected = false
|
||||
}
|
||||
}
|
||||
|
||||
override fun isConnected(service: BtService): Boolean = false
|
||||
|
||||
override fun isConnecting(service: BtService): Boolean = false
|
||||
|
||||
override fun connect(service: BtService, deviceId: String) {}
|
||||
|
||||
override fun reportRotation(format: String, azimuth: Double, elevation: Double, params: ExtendedParams) {
|
||||
reporterScope.launch {
|
||||
connect(NetworkService.ROTATOR, params.server, params.port)
|
||||
if (!isConnected(NetworkService.ROTATOR)) return@launch
|
||||
val el = if (elevation > 0.0) elevation else 0.0
|
||||
val command = format
|
||||
.replace("\$AZ", azimuth.toString())
|
||||
.replace("\$EL", el.toString())
|
||||
write(NetworkService.ROTATOR, command)
|
||||
}
|
||||
}
|
||||
|
||||
override fun reportFrequency(format: String, frequency: Long, params: ExtendedParams) {
|
||||
reporterScope.launch {
|
||||
connect(NetworkService.FREQUENCY, params.server, params.port)
|
||||
if (!isConnected(NetworkService.FREQUENCY)) return@launch
|
||||
val command = format.replace("\$FREQ", frequency.toString())
|
||||
write(NetworkService.FREQUENCY, command)
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,102 @@
|
||||
package com.rtbishop.look4sat.core.data.injection
|
||||
|
||||
import android.bluetooth.BluetoothManager
|
||||
import android.content.Context
|
||||
import android.hardware.Sensor
|
||||
import android.hardware.SensorManager
|
||||
import android.location.LocationManager
|
||||
import androidx.room.Room
|
||||
import com.rtbishop.look4sat.core.data.database.Look4SatDb
|
||||
import com.rtbishop.look4sat.core.data.framework.BluetoothReporter
|
||||
import com.rtbishop.look4sat.core.data.framework.NetworkReporter
|
||||
import com.rtbishop.look4sat.core.data.repository.DatabaseRepo
|
||||
import com.rtbishop.look4sat.core.data.repository.SatelliteRepo
|
||||
import com.rtbishop.look4sat.core.data.repository.SelectionRepo
|
||||
import com.rtbishop.look4sat.core.data.repository.SensorsRepo
|
||||
import com.rtbishop.look4sat.core.data.repository.SettingsRepo
|
||||
import com.rtbishop.look4sat.core.data.source.LocalSource
|
||||
import com.rtbishop.look4sat.core.data.source.RemoteSource
|
||||
import com.rtbishop.look4sat.core.data.usecase.AddToCalendar
|
||||
import com.rtbishop.look4sat.core.data.usecase.ShowToast
|
||||
import com.rtbishop.look4sat.core.domain.repository.ExtendedParams
|
||||
import com.rtbishop.look4sat.core.domain.repository.IDatabaseRepo
|
||||
import com.rtbishop.look4sat.core.domain.repository.IMainContainer
|
||||
import com.rtbishop.look4sat.core.domain.repository.IReporterRepo
|
||||
import com.rtbishop.look4sat.core.domain.repository.ISatelliteRepo
|
||||
import com.rtbishop.look4sat.core.domain.repository.ISelectionRepo
|
||||
import com.rtbishop.look4sat.core.domain.repository.ISensorsRepo
|
||||
import com.rtbishop.look4sat.core.domain.repository.ISettingsRepo
|
||||
import com.rtbishop.look4sat.core.domain.repository.WithoutExtParams
|
||||
import com.rtbishop.look4sat.core.domain.source.ILocalSource
|
||||
import com.rtbishop.look4sat.core.domain.source.IRemoteSource
|
||||
import com.rtbishop.look4sat.core.domain.usecase.IAddToCalendar
|
||||
import com.rtbishop.look4sat.core.domain.usecase.IShowToast
|
||||
import com.rtbishop.look4sat.core.domain.utility.DataParser
|
||||
import kotlinx.coroutines.CoroutineExceptionHandler
|
||||
import kotlinx.coroutines.CoroutineScope
|
||||
import kotlinx.coroutines.Dispatchers
|
||||
import kotlinx.coroutines.SupervisorJob
|
||||
import okhttp3.OkHttpClient
|
||||
|
||||
class MainContainer(private val context: Context) : IMainContainer {
|
||||
|
||||
private val localSource = provideLocalSource()
|
||||
private val mainHandler = CoroutineExceptionHandler { _, error -> println("MainHandler: $error") }
|
||||
override val appScope = CoroutineScope(SupervisorJob() + Dispatchers.Default + mainHandler)
|
||||
override val settingsRepo = provideSettingsRepo()
|
||||
override val selectionRepo = provideSelectionRepo()
|
||||
override val satelliteRepo = provideSatelliteRepo()
|
||||
override val databaseRepo = provideDatabaseRepo()
|
||||
|
||||
override fun provideAddToCalendar(): IAddToCalendar = AddToCalendar(context)
|
||||
|
||||
override fun provideShowToast(): IShowToast = ShowToast(context)
|
||||
|
||||
override fun provideBluetoothReporter(): IReporterRepo<WithoutExtParams> {
|
||||
val manager = context.getSystemService(Context.BLUETOOTH_SERVICE) as BluetoothManager
|
||||
return BluetoothReporter(manager, CoroutineScope(Dispatchers.IO))
|
||||
}
|
||||
|
||||
override fun provideNetworkReporter(): IReporterRepo<ExtendedParams> {
|
||||
return NetworkReporter(CoroutineScope(Dispatchers.IO))
|
||||
}
|
||||
|
||||
override fun provideSensorsRepo(): ISensorsRepo {
|
||||
val manager = context.getSystemService(Context.SENSOR_SERVICE) as SensorManager
|
||||
val sensor = manager.getDefaultSensor(Sensor.TYPE_ROTATION_VECTOR)
|
||||
return SensorsRepo(manager, sensor)
|
||||
}
|
||||
|
||||
private fun provideDatabaseRepo(): IDatabaseRepo {
|
||||
val dbDispatcher = Dispatchers.Default
|
||||
val dataParser = DataParser(dbDispatcher)
|
||||
val remoteSource = provideRemoteSource()
|
||||
return DatabaseRepo(dbDispatcher, dataParser, localSource, remoteSource, settingsRepo)
|
||||
}
|
||||
|
||||
private fun provideLocalSource(): ILocalSource {
|
||||
val builder = Room.databaseBuilder(context, Look4SatDb::class.java, "Look4SatDBv400")
|
||||
val database = builder.fallbackToDestructiveMigration(false).build()
|
||||
return LocalSource(database.look4SatDao())
|
||||
}
|
||||
|
||||
private fun provideRemoteSource(): IRemoteSource {
|
||||
return RemoteSource(Dispatchers.IO, context.contentResolver, OkHttpClient.Builder().build())
|
||||
}
|
||||
|
||||
private fun provideSatelliteRepo(): ISatelliteRepo {
|
||||
return SatelliteRepo(Dispatchers.Default, localSource, settingsRepo)
|
||||
}
|
||||
|
||||
private fun provideSelectionRepo(): ISelectionRepo {
|
||||
return SelectionRepo(Dispatchers.Default, localSource, settingsRepo)
|
||||
}
|
||||
|
||||
private fun provideSettingsRepo(): ISettingsRepo {
|
||||
val manager = context.getSystemService(Context.LOCATION_SERVICE) as LocationManager
|
||||
val appPrefsFileName = "${context.packageName}_preferences"
|
||||
val appPreferences = context.getSharedPreferences(appPrefsFileName, Context.MODE_PRIVATE)
|
||||
val appVersionName = context.packageManager.getPackageInfo(context.packageName, 0).versionName ?: "4.0.4"
|
||||
return SettingsRepo(manager, appPreferences, appVersionName)
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,130 @@
|
||||
/*
|
||||
* Look4Sat. Amateur radio satellite tracker and pass predictor.
|
||||
* Copyright (C) 2019-2026 Arty Bishop and contributors.
|
||||
*
|
||||
* 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.core.data.repository
|
||||
|
||||
import com.rtbishop.look4sat.core.domain.model.DatabaseState
|
||||
import com.rtbishop.look4sat.core.domain.model.SatRadio
|
||||
import com.rtbishop.look4sat.core.domain.predict.OrbitalData
|
||||
import com.rtbishop.look4sat.core.domain.repository.IDatabaseRepo
|
||||
import com.rtbishop.look4sat.core.domain.repository.ISettingsRepo
|
||||
import com.rtbishop.look4sat.core.domain.source.ILocalSource
|
||||
import com.rtbishop.look4sat.core.domain.source.IRemoteSource
|
||||
import com.rtbishop.look4sat.core.domain.source.Sources
|
||||
import com.rtbishop.look4sat.core.domain.utility.DataParser
|
||||
import kotlinx.coroutines.CoroutineDispatcher
|
||||
import kotlinx.coroutines.async
|
||||
import kotlinx.coroutines.withContext
|
||||
import java.util.zip.ZipInputStream
|
||||
|
||||
class DatabaseRepo(
|
||||
private val dispatcher: CoroutineDispatcher,
|
||||
private val dataParser: DataParser,
|
||||
private val localSource: ILocalSource,
|
||||
private val remoteSource: IRemoteSource,
|
||||
private val settingsRepo: ISettingsRepo
|
||||
) : IDatabaseRepo {
|
||||
|
||||
override suspend fun updateTLEFromFile(uri: String) = withContext(dispatcher) {
|
||||
val importedSatellites = remoteSource.getFileStream(uri)?.let { dataParser.parseTLEStream(it) }
|
||||
importedSatellites?.let { localSource.insertEntries(it) }
|
||||
setUpdateSuccessful(System.currentTimeMillis())
|
||||
}
|
||||
|
||||
override suspend fun updateTransceiversFromFile(uri: String) = withContext(dispatcher) {
|
||||
val radios = remoteSource.getFileStream(uri)?.let { dataParser.parseJSONStream(it) }
|
||||
radios?.let {
|
||||
if(it.isNotEmpty()) {
|
||||
localSource.deleteRadios()
|
||||
localSource.insertRadios(it)
|
||||
}
|
||||
}
|
||||
setUpdateSuccessful(System.currentTimeMillis())
|
||||
}
|
||||
|
||||
override suspend fun updateFromRemote() = withContext(dispatcher) {
|
||||
val importedEntries = mutableListOf<OrbitalData>()
|
||||
val importedRadios = mutableListOf<SatRadio>()
|
||||
val tleUrls = Sources.satelliteDataUrls.toMutableMap().apply {
|
||||
if (settingsRepo.dataSourcesSettings.value.useCustomTLE) {
|
||||
this["Other"] = settingsRepo.dataSourcesSettings.value.tleUrl
|
||||
}
|
||||
}
|
||||
val jobsMap = tleUrls
|
||||
.filter { (_, url) -> url.isNotEmpty() }
|
||||
.mapValues { (_, url) -> async { remoteSource.getNetworkStream(url) } }
|
||||
val radioUrls = buildList {
|
||||
add(Sources.RADIO_DATA_URL)
|
||||
if (settingsRepo.dataSourcesSettings.value.useCustomTransceivers) {
|
||||
add(settingsRepo.dataSourcesSettings.value.transceiversUrl)
|
||||
}
|
||||
}
|
||||
val jobRadios = radioUrls.associateWith { url -> async { remoteSource.getNetworkStream(url) } }
|
||||
// parse
|
||||
jobsMap.mapValues { job -> job.value.await() }.forEach { entry ->
|
||||
entry.value?.let { stream ->
|
||||
when (val type = entry.key) {
|
||||
"Amsat", "R4UAB", "Other" -> {
|
||||
// parse tle stream
|
||||
val satellites = dataParser.parseTLEStream(stream)
|
||||
val catnums = satellites.map { it.catnum }
|
||||
settingsRepo.setSatelliteTypeIds(type, catnums)
|
||||
importedEntries.addAll(satellites)
|
||||
}
|
||||
|
||||
"McCants", "Classified" -> {
|
||||
// unzip and parse tle stream
|
||||
val unzipped = ZipInputStream(stream).apply { nextEntry }
|
||||
val satellites = dataParser.parseTLEStream(unzipped)
|
||||
val catnums = satellites.map { it.catnum }
|
||||
settingsRepo.setSatelliteTypeIds(type, catnums)
|
||||
importedEntries.addAll(satellites)
|
||||
}
|
||||
|
||||
else -> {
|
||||
// parse csv stream
|
||||
val satellites = dataParser.parseCSVStream(stream)
|
||||
val catnums = satellites.map { it.catnum }
|
||||
settingsRepo.setSatelliteTypeIds(type, catnums)
|
||||
importedEntries.addAll(satellites)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
jobRadios.values.forEach { job ->
|
||||
job.await()?.let {
|
||||
importedRadios.addAll(dataParser.parseJSONStream(it))
|
||||
}
|
||||
}
|
||||
// insert
|
||||
localSource.insertEntries(importedEntries)
|
||||
localSource.insertRadios(importedRadios)
|
||||
setUpdateSuccessful(System.currentTimeMillis())
|
||||
}
|
||||
|
||||
override suspend fun clearAllData() = withContext(dispatcher) {
|
||||
localSource.deleteEntries()
|
||||
localSource.deleteRadios()
|
||||
setUpdateSuccessful(0L)
|
||||
}
|
||||
|
||||
private suspend fun setUpdateSuccessful(timestamp: Long) = withContext(dispatcher) {
|
||||
val numberOfRadios = localSource.getRadiosTotal()
|
||||
val numberOfSatellites = localSource.getEntriesTotal()
|
||||
settingsRepo.updateDatabaseState(DatabaseState(numberOfRadios, numberOfSatellites, timestamp))
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,242 @@
|
||||
/*
|
||||
* Look4Sat. Amateur radio satellite tracker and pass predictor.
|
||||
* Copyright (C) 2019-2026 Arty Bishop and contributors.
|
||||
*
|
||||
* 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.core.data.repository
|
||||
|
||||
import com.rtbishop.look4sat.core.domain.model.SatRadio
|
||||
import com.rtbishop.look4sat.core.domain.predict.GeoPos
|
||||
import com.rtbishop.look4sat.core.domain.predict.OrbitalObject
|
||||
import com.rtbishop.look4sat.core.domain.predict.OrbitalPass
|
||||
import com.rtbishop.look4sat.core.domain.predict.OrbitalPos
|
||||
import com.rtbishop.look4sat.core.domain.repository.ISatelliteRepo
|
||||
import com.rtbishop.look4sat.core.domain.repository.ISettingsRepo
|
||||
import com.rtbishop.look4sat.core.domain.source.ILocalSource
|
||||
import com.rtbishop.look4sat.core.domain.utility.round
|
||||
import com.rtbishop.look4sat.core.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: ILocalSource,
|
||||
private val settingsRepo: ISettingsRepo
|
||||
) : ISatelliteRepo {
|
||||
|
||||
private val _passes = MutableStateFlow<List<OrbitalPass>>(emptyList())
|
||||
override val passes: StateFlow<List<OrbitalPass>> = _passes
|
||||
|
||||
private val _satellites = MutableStateFlow<List<OrbitalObject>>(emptyList())
|
||||
override val satellites: StateFlow<List<OrbitalObject>> = _satellites
|
||||
|
||||
override suspend fun getRadiosWithId(id: Int) = localStorage.getRadiosWithId(id)
|
||||
|
||||
override suspend fun initRepository() = withContext(dispatcher) {
|
||||
settingsRepo.selectedIds.collect { selectedIds ->
|
||||
_satellites.update { localStorage.getEntriesWithIds(selectedIds) }
|
||||
val (hoursAhead, minElevation, modes) = settingsRepo.passesSettings.value
|
||||
val timeNow = 1000 * ((System.currentTimeMillis() + 500) / 1000)
|
||||
calculatePasses(timeNow, hoursAhead, minElevation, modes)
|
||||
}
|
||||
}
|
||||
|
||||
override suspend fun getPosition(sat: OrbitalObject, pos: GeoPos, time: Long): OrbitalPos {
|
||||
return withContext(dispatcher) { sat.getPosition(pos, time) }
|
||||
}
|
||||
|
||||
override suspend fun getTrack(sat: OrbitalObject, pos: GeoPos, start: Long, end: Long): List<OrbitalPos> {
|
||||
return withContext(dispatcher) {
|
||||
val positions = mutableListOf<OrbitalPos>()
|
||||
var currentTime = start
|
||||
while (currentTime < end) {
|
||||
positions.add(sat.getPosition(pos, currentTime))
|
||||
currentTime += 15000
|
||||
}
|
||||
positions
|
||||
}
|
||||
}
|
||||
|
||||
override suspend fun getRadios(
|
||||
sat: OrbitalObject,
|
||||
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.downlinkLow?.let { transmitter.downlinkLow = satPos.getDownlinkFreq(it) }
|
||||
transmitter.downlinkHigh?.let { transmitter.downlinkHigh = satPos.getDownlinkFreq(it) }
|
||||
transmitter.uplinkLow?.let { transmitter.uplinkLow = satPos.getUplinkFreq(it) }
|
||||
transmitter.uplinkHigh?.let { transmitter.uplinkHigh = satPos.getUplinkFreq(it) }
|
||||
}
|
||||
copiedList.map { it.copy() }
|
||||
}
|
||||
}
|
||||
|
||||
override suspend fun processPasses(passList: List<OrbitalPass>, time: Long): List<OrbitalPass> {
|
||||
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, modes: List<String>) {
|
||||
if (_satellites.value.isNotEmpty()) {
|
||||
withContext(dispatcher) {
|
||||
val newPasses = mutableListOf<OrbitalPass>()
|
||||
val idsWithModes = localStorage.getIdsWithModes(modes)
|
||||
_satellites.value.forEach { satellite ->
|
||||
if (idsWithModes.isEmpty() || satellite.data.catnum in idsWithModes) {
|
||||
newPasses.addAll(satellite.getPasses(settingsRepo.stationPosition.value, time, hoursAhead))
|
||||
}
|
||||
}
|
||||
_passes.update { newPasses.filter(time, hoursAhead, minElevation) }
|
||||
}
|
||||
} else {
|
||||
_passes.update { emptyList() }
|
||||
}
|
||||
}
|
||||
|
||||
private fun OrbitalObject.getPasses(pos: GeoPos, time: Long, hours: Int): List<OrbitalPass> {
|
||||
val passes = mutableListOf<OrbitalPass>()
|
||||
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<OrbitalPass>.filter(time: Long, hoursAhead: Int, minElev: Double): List<OrbitalPass> {
|
||||
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: OrbitalObject, pos: GeoPos, time: Long): OrbitalPass {
|
||||
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 OrbitalPass(aos, az, los, az, alt.toInt(), elev, sat)
|
||||
}
|
||||
|
||||
private fun getLeoPass(sat: OrbitalObject, pos: GeoPos, time: Long, rewind: Boolean): OrbitalPass {
|
||||
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 1 second
|
||||
calendarTimeMillis += -60L * 1000L
|
||||
do {
|
||||
calendarTimeMillis += 1L * 500L
|
||||
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 = 1000 * ((satPos.time + 500) / 1000)
|
||||
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 1 second
|
||||
calendarTimeMillis += -30L * 1000L
|
||||
do {
|
||||
calendarTimeMillis += 1L * 500L
|
||||
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 = 1000 * ((satPos.time + 500) / 1000) // val tca = (aos + los) / 2
|
||||
val losAz = satPos.azimuth.toDegrees().round(1)
|
||||
val elev = maxElevation.toDegrees().round(1)
|
||||
return OrbitalPass(aos, aosAz, los, losAz, alt.toInt(), elev, sat)
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,103 @@
|
||||
/*
|
||||
* Look4Sat. Amateur radio satellite tracker and pass predictor.
|
||||
* Copyright (C) 2019-2026 Arty Bishop and contributors.
|
||||
*
|
||||
* 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.core.data.repository
|
||||
|
||||
import com.rtbishop.look4sat.core.domain.model.SatItem
|
||||
import com.rtbishop.look4sat.core.domain.repository.ISelectionRepo
|
||||
import com.rtbishop.look4sat.core.domain.repository.ISettingsRepo
|
||||
import com.rtbishop.look4sat.core.domain.source.ILocalSource
|
||||
import com.rtbishop.look4sat.core.domain.source.Sources
|
||||
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 localSource: ILocalSource,
|
||||
private val settingsRepo: ISettingsRepo
|
||||
) : ISelectionRepo {
|
||||
|
||||
private val currentItems = MutableStateFlow<List<SatItem>>(emptyList())
|
||||
private val currentTypes = MutableStateFlow(settingsRepo.selectedTypes.value)
|
||||
private val currentQuery = MutableStateFlow("")
|
||||
private val itemsWithTypes = currentTypes.flatMapLatest { types: List<String> ->
|
||||
currentItems.map { items -> items.filterByTypes(types) }
|
||||
}
|
||||
private val itemsWithQuery = currentQuery.flatMapLatest { query ->
|
||||
itemsWithTypes.map { items -> items.filterByQuery(query) }
|
||||
}
|
||||
|
||||
override fun getCurrentTypes() = currentTypes.value
|
||||
|
||||
override fun getTypesList() = Sources.satelliteDataUrls.keys.sorted().toMutableList().apply {
|
||||
removeAt(0)
|
||||
}
|
||||
|
||||
override suspend fun getEntriesFlow() = withContext(dispatcher) {
|
||||
val selectedIds = settingsRepo.selectedIds.value
|
||||
currentItems.value = localSource.getEntriesList().map { item ->
|
||||
item.copy(isSelected = item.catnum in selectedIds)
|
||||
}
|
||||
return@withContext itemsWithQuery
|
||||
}
|
||||
|
||||
override suspend fun setTypes(types: List<String>) {
|
||||
currentTypes.value = types
|
||||
settingsRepo.setSelectedTypes(types)
|
||||
}
|
||||
|
||||
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.setSelectedIds(currentSelection)
|
||||
}
|
||||
|
||||
private suspend fun List<SatItem>.filterByTypes(types: List<String>) = withContext(dispatcher) {
|
||||
if (types.isEmpty()) return@withContext this@filterByTypes
|
||||
val catnums = settingsRepo.getSatelliteTypesIds(types)
|
||||
if (catnums.isEmpty()) return@withContext this@filterByTypes
|
||||
return@withContext this@filterByTypes.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 (_: Exception) {
|
||||
this@filterByQuery.filter { item -> item.name.lowercase().contains(query.lowercase()) }
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,74 @@
|
||||
/*
|
||||
* Look4Sat. Amateur radio satellite tracker and pass predictor.
|
||||
* Copyright (C) 2019-2026 Arty Bishop and contributors.
|
||||
*
|
||||
* 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.core.data.repository
|
||||
|
||||
import android.hardware.GeomagneticField
|
||||
import android.hardware.Sensor
|
||||
import android.hardware.SensorEvent
|
||||
import android.hardware.SensorEventListener
|
||||
import android.hardware.SensorManager
|
||||
import com.rtbishop.look4sat.core.domain.predict.GeoPos
|
||||
import com.rtbishop.look4sat.core.domain.predict.RAD2DEG
|
||||
import com.rtbishop.look4sat.core.domain.repository.ISensorsRepo
|
||||
import kotlinx.coroutines.flow.MutableStateFlow
|
||||
import kotlinx.coroutines.flow.StateFlow
|
||||
import kotlin.math.round
|
||||
|
||||
class SensorsRepo(private val sensorManager: SensorManager, private val sensor: Sensor?) :
|
||||
SensorEventListener, ISensorsRepo {
|
||||
|
||||
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 getMagDeclination(geoPos: GeoPos, time: Long): Float {
|
||||
val latitude = geoPos.latitude.toFloat()
|
||||
val longitude = geoPos.longitude.toFloat()
|
||||
return GeomagneticField(latitude, longitude, geoPos.altitude.toFloat(), time).declination
|
||||
}
|
||||
|
||||
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,427 @@
|
||||
/*
|
||||
* Look4Sat. Amateur radio satellite tracker and pass predictor.
|
||||
* Copyright (C) 2019-2026 Arty Bishop and contributors.
|
||||
*
|
||||
* 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.core.data.repository
|
||||
|
||||
import android.content.SharedPreferences
|
||||
import android.location.Location
|
||||
import android.location.LocationManager
|
||||
import androidx.core.content.edit
|
||||
import androidx.core.location.LocationListenerCompat
|
||||
import androidx.core.location.LocationManagerCompat
|
||||
import com.rtbishop.look4sat.core.domain.model.DataSourcesSettings
|
||||
import com.rtbishop.look4sat.core.domain.model.DatabaseState
|
||||
import com.rtbishop.look4sat.core.domain.model.OtherSettings
|
||||
import com.rtbishop.look4sat.core.domain.model.PassesSettings
|
||||
import com.rtbishop.look4sat.core.domain.model.RCSettings
|
||||
import com.rtbishop.look4sat.core.domain.predict.GeoPos
|
||||
import com.rtbishop.look4sat.core.domain.repository.ISettingsRepo
|
||||
import com.rtbishop.look4sat.core.domain.utility.positionToQth
|
||||
import com.rtbishop.look4sat.core.domain.utility.qthToPosition
|
||||
import com.rtbishop.look4sat.core.domain.utility.round
|
||||
import kotlinx.coroutines.flow.MutableStateFlow
|
||||
import kotlinx.coroutines.flow.StateFlow
|
||||
import kotlinx.coroutines.flow.update
|
||||
|
||||
class SettingsRepo(
|
||||
private val locationManager: LocationManager,
|
||||
private val preferences: SharedPreferences,
|
||||
override val appVersionName: String
|
||||
) : ISettingsRepo, LocationListenerCompat {
|
||||
|
||||
private val keyBluetoothRotatorAddress = "bluetoothAddress"
|
||||
private val keyBluetoothRotatorName = "bluetoothName"
|
||||
private val keyBluetoothRotatorFormat = "bluetoothFormat"
|
||||
private val keyBluetoothRotatorState = "bluetoothState"
|
||||
private val keyBluetoothFrequencyState = "bluetoothFrequencyState"
|
||||
private val keyBluetoothFrequencyAddress = "bluetoothFrequencyAddress"
|
||||
private val keyBluetoothFrequencyFormat = "bluetoothFrequencyFormat"
|
||||
private val keyFilterHoursAhead = "filterHoursAhead"
|
||||
private val keyFilterMinElevation = "filterMinElevation"
|
||||
private val keyNumberOfRadios = "numberOfRadios"
|
||||
private val keyNumberOfSatellites = "numberOfSatellites"
|
||||
private val keyRotatorAddress = "rotatorAddress"
|
||||
private val keyRotatorPort = "rotatorPort"
|
||||
private val keyRotatorState = "rotatorState"
|
||||
private val keyRotatorFormat = "rotatorFormat"
|
||||
private val keyFrequencyState = "frequencyState"
|
||||
private val keyFrequencyAddress = "frequencyAddress"
|
||||
private val keyFrequencyPort = "frequencyPort"
|
||||
private val keyFrequencyFormat = "frequencyFormat"
|
||||
private val keySelectedIds = "selectedIds"
|
||||
private val keySelectedTypes = "selectedTypes"
|
||||
private val keySelectedModes = "selectedModes"
|
||||
private val keyStateOfAutoUpdate = "stateOfAutoUpdate"
|
||||
private val keyStateOfSensors = "stateOfSensors"
|
||||
private val keyStateOfSweep = "stateOfSweep"
|
||||
private val keyStateOfUtc = "stateOfUtc"
|
||||
private val keyStateOfLightTheme = "stateOfLightTheme"
|
||||
private val keyStationAltitude = "stationAltitude"
|
||||
private val keyStationLatitude = "stationLatitude"
|
||||
private val keyStationLongitude = "stationLongitude"
|
||||
private val keyStationQth = "stationQth"
|
||||
private val keyStationTimestamp = "stationTimestamp"
|
||||
private val keyUpdateTimestamp = "updateTimestamp"
|
||||
private val keyShouldSeeWarning = "shouldSeeWarning"
|
||||
private val keyShouldSeeWhatsNew = "shouldSeeWhatsNew_v$appVersionName"
|
||||
private val keyUseCustomTle = "useCustomTle"
|
||||
private val keyUseCustomTransceivers = "useCustomTransceivers"
|
||||
private val keyTleUrl = "tleUrl"
|
||||
private val keyTransceiversUrl = "transceiversUrl"
|
||||
private val separatorComma = ","
|
||||
|
||||
//region # Satellites selection settings
|
||||
private val _satelliteSelection = MutableStateFlow(getSelectedIds())
|
||||
private val _typesSelection = MutableStateFlow(getSelectedTypes())
|
||||
override val selectedIds: StateFlow<List<Int>> = _satelliteSelection
|
||||
override val selectedTypes: StateFlow<List<String>> = _typesSelection
|
||||
|
||||
override fun setSelectedIds(ids: List<Int>) {
|
||||
val selectionString = ids.joinToString(separatorComma)
|
||||
preferences.edit { putString(keySelectedIds, selectionString) }
|
||||
_satelliteSelection.value = ids
|
||||
}
|
||||
|
||||
override fun setSelectedTypes(types: List<String>) {
|
||||
val typesString = types.joinToString(separatorComma)
|
||||
preferences.edit { putString(keySelectedTypes, typesString) }
|
||||
_typesSelection.value = types
|
||||
}
|
||||
|
||||
private fun getSelectedIds(): List<Int> {
|
||||
val selectionString = preferences.getString(keySelectedIds, null)
|
||||
if (selectionString.isNullOrEmpty()) return emptyList()
|
||||
return selectionString.split(separatorComma).map { it.toInt() }
|
||||
}
|
||||
|
||||
private fun getSelectedTypes(): List<String> {
|
||||
val typesString = preferences.getString(keySelectedTypes, null)
|
||||
if (typesString.isNullOrEmpty()) return listOf("Amateur")
|
||||
return typesString.split(separatorComma)
|
||||
}
|
||||
//endregion
|
||||
|
||||
//region # Passes filter settings
|
||||
private val _passesSettings = MutableStateFlow(getPassesSettings())
|
||||
override val passesSettings: StateFlow<PassesSettings> = _passesSettings
|
||||
|
||||
override fun setPassesSettings(settings: PassesSettings) = preferences.edit {
|
||||
putInt(keyFilterHoursAhead, settings.hoursAhead)
|
||||
putLong(keyFilterMinElevation, settings.minElevation.toRawBits())
|
||||
putString(keySelectedModes, settings.selectedModes.joinToString(separatorComma))
|
||||
_passesSettings.value = settings
|
||||
}
|
||||
|
||||
private fun getPassesSettings(): PassesSettings {
|
||||
val hoursAhead = preferences.getInt(keyFilterHoursAhead, 24)
|
||||
val minElevation = Double.fromBits(preferences.getLong(keyFilterMinElevation, 16.0.toRawBits()))
|
||||
val selectedModesString = preferences.getString(keySelectedModes, null)
|
||||
val selectedModes = selectedModesString?.split(separatorComma)?.sorted() ?: emptyList()
|
||||
return PassesSettings(hoursAhead, minElevation, selectedModes)
|
||||
}
|
||||
//endregion
|
||||
|
||||
//region # Station position settings
|
||||
private val _stationPosition = MutableStateFlow(getStationPosition())
|
||||
private val providerDef = LocationManager.PASSIVE_PROVIDER
|
||||
private val providerGps = LocationManager.GPS_PROVIDER
|
||||
private val providerNet = LocationManager.NETWORK_PROVIDER
|
||||
override val stationPosition: StateFlow<GeoPos> = _stationPosition
|
||||
|
||||
override fun onLocationChanged(location: Location) {
|
||||
setStationPosition(location.latitude, location.longitude, location.altitude)
|
||||
}
|
||||
|
||||
override fun setStationPosition(latitude: Double, longitude: Double, altitude: Double): Boolean {
|
||||
val newLongitude = if (longitude > 180.0) longitude - 180 else longitude
|
||||
val locator = positionToQth(latitude, newLongitude) ?: return false
|
||||
setStationPosition(latitude, newLongitude, altitude, locator)
|
||||
return true
|
||||
}
|
||||
|
||||
override fun setStationPosition(): Boolean {
|
||||
if (!LocationManagerCompat.isLocationEnabled(locationManager)) return false
|
||||
try {
|
||||
val hasGps = LocationManagerCompat.hasProvider(locationManager, providerGps)
|
||||
val hasNet = LocationManagerCompat.hasProvider(locationManager, providerNet)
|
||||
val provider = if (hasGps) providerGps else if (hasNet) providerNet else providerDef
|
||||
val location = locationManager.getLastKnownLocation(providerDef)
|
||||
if (location == null || System.currentTimeMillis() - location.time > 600_000L) {
|
||||
println("Requesting location for $provider provider")
|
||||
locationManager.requestLocationUpdates(provider, 0L, 0f, this)
|
||||
} else {
|
||||
setStationPosition(location.latitude, location.longitude, location.altitude)
|
||||
}
|
||||
} catch (exception: SecurityException) {
|
||||
println("No permissions were given - $exception")
|
||||
}
|
||||
return true
|
||||
}
|
||||
|
||||
override fun setStationPosition(locator: String): Boolean {
|
||||
val position = qthToPosition(locator) ?: return false
|
||||
setStationPosition(position.latitude, position.longitude, 0.0, locator)
|
||||
return true
|
||||
}
|
||||
|
||||
private fun getStationPosition(): GeoPos {
|
||||
val latitude = (preferences.getString(keyStationLatitude, null) ?: "0.0").toDouble()
|
||||
val longitude = (preferences.getString(keyStationLongitude, null) ?: "0.0").toDouble()
|
||||
val altitude = (preferences.getString(keyStationAltitude, null) ?: "0.0").toDouble()
|
||||
val qthLocator = preferences.getString(keyStationQth, null) ?: "JJ00aa"
|
||||
val timestamp = preferences.getLong(keyStationTimestamp, 0L)
|
||||
return GeoPos(latitude, longitude, altitude, qthLocator, timestamp)
|
||||
}
|
||||
|
||||
private fun setStationPosition(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")
|
||||
setStationPosition(GeoPos(newLat, newLon, newAlt, locator, timestamp))
|
||||
}
|
||||
|
||||
private fun setStationPosition(stationPos: GeoPos) = preferences.edit {
|
||||
putString(keyStationLatitude, stationPos.latitude.toString())
|
||||
putString(keyStationLongitude, stationPos.longitude.toString())
|
||||
putString(keyStationAltitude, stationPos.altitude.toString())
|
||||
putString(keyStationQth, stationPos.qthLocator)
|
||||
putLong(keyStationTimestamp, stationPos.timestamp)
|
||||
_stationPosition.value = stationPos
|
||||
}
|
||||
//endregion
|
||||
|
||||
//region # Database update settings
|
||||
private val _databaseState = MutableStateFlow(getDatabaseState())
|
||||
override val databaseState: StateFlow<DatabaseState> = _databaseState
|
||||
|
||||
override fun getSatelliteTypesIds(types: List<String>): List<Int> {
|
||||
val idsSet = mutableSetOf<Int>()
|
||||
types.forEach { type ->
|
||||
val typeString = preferences.getString("type$type", null)
|
||||
val typeIds = if (typeString.isNullOrBlank()) {
|
||||
emptyList()
|
||||
} else {
|
||||
typeString.split(separatorComma).map { it.toInt() }
|
||||
}
|
||||
idsSet.addAll(typeIds)
|
||||
}
|
||||
return idsSet.toList()
|
||||
}
|
||||
|
||||
override fun setSatelliteTypeIds(type: String, ids: List<Int>) {
|
||||
if (type == "All") return
|
||||
val typesString = ids.joinToString(separatorComma)
|
||||
preferences.edit { putString("type$type", typesString) }
|
||||
}
|
||||
|
||||
override fun updateDatabaseState(state: DatabaseState) = preferences.edit {
|
||||
putInt(keyNumberOfSatellites, state.numberOfSatellites)
|
||||
putInt(keyNumberOfRadios, state.numberOfRadios)
|
||||
putLong(keyUpdateTimestamp, state.updateTimestamp)
|
||||
_databaseState.value = state
|
||||
}
|
||||
|
||||
private fun getDatabaseState(): DatabaseState {
|
||||
val numberOfRadios = preferences.getInt(keyNumberOfRadios, 0)
|
||||
val numberOfSatellites = preferences.getInt(keyNumberOfSatellites, 0)
|
||||
val updateTimestamp = preferences.getLong(keyUpdateTimestamp, 0L)
|
||||
return DatabaseState(numberOfRadios, numberOfSatellites, updateTimestamp)
|
||||
}
|
||||
//endregion
|
||||
|
||||
//region # RC settings
|
||||
private val _rcSettings = MutableStateFlow(getRCSettings())
|
||||
override val rcSettings: StateFlow<RCSettings> = _rcSettings
|
||||
|
||||
override fun setBluetoothRotatorAddress(value: String) {
|
||||
preferences.edit { putString(keyBluetoothRotatorAddress, value) }
|
||||
_rcSettings.update { it.copy(bluetoothRotatorAddress = value) }
|
||||
}
|
||||
|
||||
override fun setBluetoothRotatorFormat(value: String) {
|
||||
preferences.edit { putString(keyBluetoothRotatorFormat, value) }
|
||||
_rcSettings.update { it.copy(bluetoothRotatorFormat = value) }
|
||||
}
|
||||
|
||||
override fun setBluetoothRotatorName(value: String) {
|
||||
preferences.edit { putString(keyBluetoothRotatorName, value) }
|
||||
_rcSettings.update { it.copy(bluetoothRotatorName = value) }
|
||||
}
|
||||
|
||||
override fun setBluetoothRotatorState(value: Boolean) {
|
||||
preferences.edit { putBoolean(keyBluetoothRotatorState, value) }
|
||||
_rcSettings.update { it.copy(bluetoothRotatorState = value) }
|
||||
}
|
||||
|
||||
override fun setBluetoothFrequencyState(value: Boolean) {
|
||||
preferences.edit { putBoolean(keyBluetoothFrequencyState, value) }
|
||||
_rcSettings.update { it.copy(bluetoothFrequencyState = value) }
|
||||
}
|
||||
|
||||
override fun setBluetoothFrequencyAddress(value: String) {
|
||||
preferences.edit { putString(keyBluetoothFrequencyAddress, value) }
|
||||
_rcSettings.update { it.copy(bluetoothFrequencyAddress = value) }
|
||||
}
|
||||
|
||||
override fun setBluetoothFrequencyFormat(value: String) {
|
||||
preferences.edit { putString(keyBluetoothFrequencyFormat, value) }
|
||||
_rcSettings.update { it.copy(bluetoothFrequencyFormat = value) }
|
||||
}
|
||||
|
||||
override fun setRotatorAddress(value: String) {
|
||||
preferences.edit { putString(keyRotatorAddress, value) }
|
||||
_rcSettings.update { it.copy(rotatorAddress = value) }
|
||||
}
|
||||
|
||||
override fun setRotatorPort(value: String) {
|
||||
preferences.edit { putString(keyRotatorPort, value) }
|
||||
_rcSettings.update { it.copy(rotatorPort = value) }
|
||||
}
|
||||
|
||||
override fun setRotatorState(value: Boolean) {
|
||||
preferences.edit { putBoolean(keyRotatorState, value) }
|
||||
_rcSettings.update { it.copy(rotatorState = value) }
|
||||
}
|
||||
|
||||
override fun setRotatorFormat(value: String) {
|
||||
preferences.edit { putString(keyRotatorFormat, value) }
|
||||
_rcSettings.update { it.copy(rotatorFormat = value) }
|
||||
}
|
||||
|
||||
override fun setFrequencyState(value: Boolean) {
|
||||
preferences.edit { putBoolean(keyFrequencyState, value) }
|
||||
_rcSettings.update { it.copy(frequencyState = value) }
|
||||
}
|
||||
|
||||
override fun setFrequencyAddress(value: String) {
|
||||
preferences.edit { putString(keyFrequencyAddress, value) }
|
||||
_rcSettings.update { it.copy(frequencyAddress = value) }
|
||||
}
|
||||
|
||||
override fun setFrequencyPort(value: String) {
|
||||
preferences.edit { putString(keyFrequencyPort, value) }
|
||||
_rcSettings.update { it.copy(frequencyPort = value) }
|
||||
}
|
||||
|
||||
override fun setFrequencyFormat(value: String) {
|
||||
preferences.edit { putString(keyFrequencyFormat, value) }
|
||||
_rcSettings.update { it.copy(frequencyFormat = value) }
|
||||
}
|
||||
|
||||
private fun getRCSettings(): RCSettings = RCSettings(
|
||||
rotatorState = preferences.getBoolean(keyRotatorState, false),
|
||||
rotatorAddress = preferences.getString(keyRotatorAddress, null) ?: "127.0.0.1",
|
||||
rotatorPort = preferences.getString(keyRotatorPort, null) ?: "4533",
|
||||
rotatorFormat = preferences.getString(keyRotatorFormat, null) ?: $$"P $AZ $EL",
|
||||
frequencyState = preferences.getBoolean(keyFrequencyState, false),
|
||||
frequencyAddress = preferences.getString(keyFrequencyAddress, null) ?: "127.0.0.1",
|
||||
frequencyPort = preferences.getString(keyFrequencyPort, null) ?: "4532",
|
||||
frequencyFormat = preferences.getString(keyFrequencyFormat, null) ?: $$"set_freq $FREQ",
|
||||
bluetoothRotatorState = preferences.getBoolean(keyBluetoothRotatorState, false),
|
||||
bluetoothRotatorFormat = preferences.getString(keyBluetoothRotatorFormat, null) ?: $$"W$AZ $EL",
|
||||
bluetoothRotatorName = preferences.getString(keyBluetoothRotatorName, null) ?: "Default",
|
||||
bluetoothRotatorAddress = preferences.getString(keyBluetoothRotatorAddress, null) ?: "00:0C:BF:13:80:5D",
|
||||
bluetoothFrequencyState = preferences.getBoolean(keyBluetoothFrequencyState, false),
|
||||
bluetoothFrequencyAddress = preferences.getString(keyBluetoothFrequencyAddress, null) ?: "00:0C:BF:13:80:5D",
|
||||
bluetoothFrequencyFormat = preferences.getString(keyBluetoothFrequencyFormat, null) ?: $$"FA$FREQ"
|
||||
)
|
||||
//endregion
|
||||
|
||||
//region # Other settings
|
||||
private val _otherSettings = MutableStateFlow(getOtherSettings())
|
||||
override val otherSettings: StateFlow<OtherSettings> = _otherSettings
|
||||
|
||||
override fun setStateOfAutoUpdate(value: Boolean) {
|
||||
preferences.edit { putBoolean(keyStateOfAutoUpdate, value) }
|
||||
_otherSettings.update { it.copy(stateOfAutoUpdate = value) }
|
||||
}
|
||||
|
||||
override fun setStateOfSensors(value: Boolean) {
|
||||
preferences.edit { putBoolean(keyStateOfSensors, value) }
|
||||
_otherSettings.update { it.copy(stateOfSensors = value) }
|
||||
}
|
||||
|
||||
override fun setStateOfSweep(value: Boolean) {
|
||||
preferences.edit { putBoolean(keyStateOfSweep, value) }
|
||||
_otherSettings.update { it.copy(stateOfSweep = value) }
|
||||
}
|
||||
|
||||
override fun setStateOfUtc(value: Boolean) {
|
||||
preferences.edit { putBoolean(keyStateOfUtc, value) }
|
||||
_otherSettings.update { it.copy(stateOfUtc = value) }
|
||||
}
|
||||
|
||||
override fun setStateOfLightTheme(value: Boolean){
|
||||
preferences.edit { putBoolean(keyStateOfLightTheme, value) }
|
||||
_otherSettings.update { it.copy(stateOfLightTheme = value) }
|
||||
}
|
||||
|
||||
override fun setWarningDismissed() {
|
||||
preferences.edit { putBoolean(keyShouldSeeWarning, false) }
|
||||
_otherSettings.update { it.copy(shouldSeeWarning = false) }
|
||||
}
|
||||
|
||||
override fun setWhatsNewDismissed() {
|
||||
preferences.edit { putBoolean(keyShouldSeeWhatsNew, false) }
|
||||
_otherSettings.update { it.copy(shouldSeeWhatsNew = false) }
|
||||
}
|
||||
|
||||
private fun getOtherSettings(): OtherSettings = OtherSettings(
|
||||
stateOfAutoUpdate = preferences.getBoolean(keyStateOfAutoUpdate, true),
|
||||
stateOfSensors = preferences.getBoolean(keyStateOfSensors, true),
|
||||
stateOfSweep = preferences.getBoolean(keyStateOfSweep, true),
|
||||
stateOfUtc = preferences.getBoolean(keyStateOfUtc, false),
|
||||
stateOfLightTheme = preferences.getBoolean(keyStateOfLightTheme, false),
|
||||
shouldSeeWarning = preferences.getBoolean(keyShouldSeeWarning, true),
|
||||
shouldSeeWhatsNew = preferences.getBoolean(keyShouldSeeWhatsNew, true)
|
||||
)
|
||||
//endregion
|
||||
|
||||
//region # Data sources settings
|
||||
private val _dataSourcesSettings = MutableStateFlow(getDataSourcesSettings())
|
||||
override val dataSourcesSettings: StateFlow<DataSourcesSettings> = _dataSourcesSettings
|
||||
|
||||
override fun setUseCustomTle(value: Boolean) {
|
||||
preferences.edit { putBoolean(keyUseCustomTle, value) }
|
||||
_dataSourcesSettings.update { it.copy(useCustomTLE = value) }
|
||||
}
|
||||
|
||||
override fun setUseCustomTransceivers(value: Boolean) {
|
||||
preferences.edit { putBoolean(keyUseCustomTransceivers, value) }
|
||||
_dataSourcesSettings.update { it.copy(useCustomTransceivers = value) }
|
||||
}
|
||||
|
||||
override fun setTleUrl(value: String) {
|
||||
preferences.edit { putString(keyTleUrl, value) }
|
||||
_dataSourcesSettings.update { it.copy(tleUrl = value) }
|
||||
}
|
||||
|
||||
override fun setTransceiversUrl(value: String) {
|
||||
preferences.edit { putString(keyTransceiversUrl, value) }
|
||||
_dataSourcesSettings.update { it.copy(transceiversUrl = value) }
|
||||
}
|
||||
|
||||
private fun getDataSourcesSettings(): DataSourcesSettings = DataSourcesSettings(
|
||||
useCustomTLE = preferences.getBoolean(keyUseCustomTle, false),
|
||||
useCustomTransceivers = preferences.getBoolean(keyUseCustomTransceivers, false),
|
||||
tleUrl = preferences.getString(keyTleUrl, "https://example.com/tle.txt") ?: "",
|
||||
transceiversUrl = preferences.getString(keyTransceiversUrl, "https://example.com/radio.json") ?: ""
|
||||
)
|
||||
//endregion
|
||||
}
|
||||
@@ -0,0 +1,98 @@
|
||||
/*
|
||||
* Look4Sat. Amateur radio satellite tracker and pass predictor.
|
||||
* Copyright (C) 2019-2026 Arty Bishop and contributors.
|
||||
*
|
||||
* 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.core.data.source
|
||||
|
||||
import com.rtbishop.look4sat.core.data.database.Look4SatDao
|
||||
import com.rtbishop.look4sat.core.domain.model.SatRadio
|
||||
import com.rtbishop.look4sat.core.domain.predict.OrbitalData
|
||||
import com.rtbishop.look4sat.core.domain.predict.OrbitalObject
|
||||
import com.rtbishop.look4sat.core.domain.source.ILocalSource
|
||||
import com.rtbishop.look4sat.core.data.database.entity.SatEntry as FrameworkEntry
|
||||
import com.rtbishop.look4sat.core.data.database.entity.SatRadio as FrameworkRadio
|
||||
import com.rtbishop.look4sat.core.domain.model.SatRadio as DomainRadio
|
||||
|
||||
class LocalSource(private val look4SatDao: Look4SatDao) : ILocalSource {
|
||||
|
||||
//region # Entries region
|
||||
|
||||
override suspend fun getEntriesTotal() = look4SatDao.getEntriesTotal()
|
||||
|
||||
override suspend fun getEntriesList() = look4SatDao.getEntriesList()
|
||||
|
||||
override suspend fun getEntriesWithIds(ids: List<Int>): List<OrbitalObject> {
|
||||
val selectedOrbitalObjects = mutableListOf<OrbitalObject>()
|
||||
ids.chunked(999).forEach { idsPart ->
|
||||
val entries = look4SatDao.getEntriesWithIds(idsPart)
|
||||
selectedOrbitalObjects.addAll(entries.toDomain().map { entry -> entry.getObject() })
|
||||
}
|
||||
return selectedOrbitalObjects
|
||||
}
|
||||
|
||||
override suspend fun insertEntries(entries: List<OrbitalData>) = look4SatDao.insertEntries(entries.toEntity())
|
||||
|
||||
override suspend fun deleteEntries() = look4SatDao.deleteEntries()
|
||||
|
||||
override suspend fun getIdsWithModes(modes: List<String>) = look4SatDao.getIdsWithModes(modes)
|
||||
|
||||
private fun FrameworkEntry.toDomain() = OrbitalData(
|
||||
this.name, this.epoch, this.meanmo, this.eccn, this.incl,
|
||||
this.raan, this.argper, this.meanan, this.catnum, this.bstar
|
||||
)
|
||||
|
||||
private fun OrbitalData.toEntity() = FrameworkEntry(
|
||||
this.name, this.epoch, this.meanmo, this.eccn, this.incl,
|
||||
this.raan, this.argper, this.meanan, this.catnum, this.bstar
|
||||
)
|
||||
|
||||
private fun List<OrbitalData>.toEntity() = this.map { item -> item.toEntity() }
|
||||
|
||||
private fun List<FrameworkEntry>.toDomain() = this.map { item -> item.toDomain() }
|
||||
|
||||
//endregion
|
||||
|
||||
//region # Radios region
|
||||
|
||||
override suspend fun getRadiosTotal() = look4SatDao.getRadiosTotal()
|
||||
|
||||
override suspend fun getRadiosWithId(id: Int): List<SatRadio> {
|
||||
return look4SatDao.getRadiosWithId(id).toDomainRadios()
|
||||
}
|
||||
|
||||
override suspend fun insertRadios(radios: List<SatRadio>) {
|
||||
look4SatDao.deleteRadios()
|
||||
look4SatDao.insertRadios(radios.toFrameworkRadios())
|
||||
}
|
||||
|
||||
override suspend fun deleteRadios() = look4SatDao.deleteRadios()
|
||||
|
||||
private fun DomainRadio.toFramework() = FrameworkRadio(
|
||||
this.uuid, this.info, this.isAlive, this.downlinkLow, this.downlinkHigh, this.downlinkMode,
|
||||
this.uplinkLow, this.uplinkHigh, this.uplinkMode, this.isInverted, this.catnum
|
||||
)
|
||||
|
||||
private fun FrameworkRadio.toDomain() = DomainRadio(
|
||||
this.uuid, this.info, this.isAlive, this.downlinkLow, this.downlinkHigh, this.downlinkMode,
|
||||
this.uplinkLow, this.uplinkHigh, this.uplinkMode, this.isInverted, this.catnum
|
||||
)
|
||||
|
||||
private fun List<DomainRadio>.toFrameworkRadios() = this.map { radio -> radio.toFramework() }
|
||||
|
||||
private fun List<FrameworkRadio>.toDomainRadios() = this.map { radio -> radio.toDomain() }
|
||||
|
||||
//endregion
|
||||
}
|
||||
@@ -0,0 +1,54 @@
|
||||
/*
|
||||
* Look4Sat. Amateur radio satellite tracker and pass predictor.
|
||||
* Copyright (C) 2019-2026 Arty Bishop and contributors.
|
||||
*
|
||||
* 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.core.data.source
|
||||
|
||||
import android.content.ContentResolver
|
||||
import androidx.core.net.toUri
|
||||
import com.rtbishop.look4sat.core.domain.source.IRemoteSource
|
||||
import kotlinx.coroutines.CoroutineDispatcher
|
||||
import kotlinx.coroutines.withContext
|
||||
import okhttp3.OkHttpClient
|
||||
import okhttp3.Request
|
||||
import java.io.InputStream
|
||||
|
||||
class RemoteSource(
|
||||
private val dispatcher: CoroutineDispatcher,
|
||||
private val contentResolver: ContentResolver,
|
||||
private val httpClient: OkHttpClient
|
||||
) : IRemoteSource {
|
||||
|
||||
override suspend fun getFileStream(uri: String): InputStream? = withContext(dispatcher) {
|
||||
try {
|
||||
val fileUri = uri.toUri()
|
||||
contentResolver.openInputStream(fileUri)?.buffered()
|
||||
} catch (exception: Exception) {
|
||||
println("RemoteSource file stream exception: $exception")
|
||||
null
|
||||
}
|
||||
}
|
||||
|
||||
override suspend fun getNetworkStream(url: String): InputStream? = withContext(dispatcher) {
|
||||
try {
|
||||
val networkRequest = Request.Builder().url(url).build()
|
||||
httpClient.newCall(networkRequest).execute().body.byteStream()
|
||||
} catch (exception: Exception) {
|
||||
println("RemoteSource network stream exception: $exception")
|
||||
null
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,41 @@
|
||||
/*
|
||||
* Look4Sat. Amateur radio satellite tracker and pass predictor.
|
||||
* Copyright (C) 2019-2026 Arty Bishop and contributors.
|
||||
*
|
||||
* 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.core.data.usecase
|
||||
|
||||
import android.content.ActivityNotFoundException
|
||||
import android.content.Context
|
||||
import android.content.Intent
|
||||
import android.provider.CalendarContract
|
||||
import com.rtbishop.look4sat.core.domain.usecase.IAddToCalendar
|
||||
|
||||
class AddToCalendar(private val context: Context) : IAddToCalendar {
|
||||
override fun invoke(name: String, aosTime: Long, losTime: Long) {
|
||||
val intent = Intent(Intent.ACTION_INSERT).apply {
|
||||
addFlags(Intent.FLAG_ACTIVITY_NEW_TASK)
|
||||
setData(CalendarContract.Events.CONTENT_URI)
|
||||
putExtra(CalendarContract.Events.TITLE, name)
|
||||
putExtra(CalendarContract.Events.DESCRIPTION, "Look4Sat Pass")
|
||||
putExtra(CalendarContract.EXTRA_EVENT_BEGIN_TIME, aosTime)
|
||||
putExtra(CalendarContract.EXTRA_EVENT_END_TIME, losTime)
|
||||
}
|
||||
try {
|
||||
context.startActivity(intent)
|
||||
} catch (_: ActivityNotFoundException) {
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,28 @@
|
||||
/*
|
||||
* Look4Sat. Amateur radio satellite tracker and pass predictor.
|
||||
* Copyright (C) 2019-2026 Arty Bishop and contributors.
|
||||
*
|
||||
* 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.core.data.usecase
|
||||
|
||||
import android.content.Context
|
||||
import android.widget.Toast
|
||||
import com.rtbishop.look4sat.core.domain.usecase.IShowToast
|
||||
|
||||
class ShowToast(private val context: Context) : IShowToast {
|
||||
override fun invoke(message: String) {
|
||||
Toast.makeText(context, message, Toast.LENGTH_SHORT).show()
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1 @@
|
||||
/build
|
||||
@@ -0,0 +1,3 @@
|
||||
plugins {
|
||||
alias(libs.plugins.convention.kotlinLibraryPlugin)
|
||||
}
|
||||
@@ -0,0 +1,20 @@
|
||||
/*
|
||||
* Look4Sat. Amateur radio satellite tracker and pass predictor.
|
||||
* Copyright (C) 2019-2026 Arty Bishop and contributors.
|
||||
*
|
||||
* 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.core.domain.model
|
||||
|
||||
data class SatItem(val catnum: Int, val name: String, val isSelected: Boolean = false)
|
||||
@@ -0,0 +1,32 @@
|
||||
/*
|
||||
* Look4Sat. Amateur radio satellite tracker and pass predictor.
|
||||
* Copyright (C) 2019-2026 Arty Bishop and contributors.
|
||||
*
|
||||
* 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.core.domain.model
|
||||
|
||||
data class SatRadio(
|
||||
val uuid: String,
|
||||
val info: String,
|
||||
val isAlive: Boolean,
|
||||
var downlinkLow: Long?,
|
||||
var downlinkHigh: Long?,
|
||||
val downlinkMode: String?,
|
||||
var uplinkLow: Long?,
|
||||
var uplinkHigh: Long?,
|
||||
val uplinkMode: String?,
|
||||
val isInverted: Boolean,
|
||||
val catnum: Int?
|
||||
)
|
||||
@@ -0,0 +1,65 @@
|
||||
/*
|
||||
* Look4Sat. Amateur radio satellite tracker and pass predictor.
|
||||
* Copyright (C) 2019-2026 Arty Bishop and contributors.
|
||||
*
|
||||
* 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.core.domain.model
|
||||
|
||||
data class DatabaseState(
|
||||
val numberOfRadios: Int,
|
||||
val numberOfSatellites: Int,
|
||||
val updateTimestamp: Long
|
||||
)
|
||||
|
||||
data class PassesSettings(
|
||||
val hoursAhead: Int,
|
||||
val minElevation: Double,
|
||||
val selectedModes: List<String>
|
||||
)
|
||||
|
||||
data class RCSettings(
|
||||
val rotatorState: Boolean,
|
||||
val rotatorAddress: String,
|
||||
val rotatorPort: String,
|
||||
val rotatorFormat: String,
|
||||
val frequencyState: Boolean,
|
||||
val frequencyAddress: String,
|
||||
val frequencyPort: String,
|
||||
val frequencyFormat: String,
|
||||
val bluetoothRotatorState: Boolean,
|
||||
val bluetoothRotatorFormat: String,
|
||||
val bluetoothRotatorName: String,
|
||||
val bluetoothRotatorAddress: String,
|
||||
val bluetoothFrequencyState: Boolean,
|
||||
val bluetoothFrequencyFormat: String,
|
||||
val bluetoothFrequencyAddress: String
|
||||
)
|
||||
|
||||
data class OtherSettings(
|
||||
val stateOfAutoUpdate: Boolean,
|
||||
val stateOfSensors: Boolean,
|
||||
val stateOfSweep: Boolean,
|
||||
val stateOfUtc: Boolean,
|
||||
val stateOfLightTheme: Boolean,
|
||||
val shouldSeeWarning: Boolean,
|
||||
val shouldSeeWhatsNew: Boolean
|
||||
)
|
||||
|
||||
data class DataSourcesSettings(
|
||||
val useCustomTLE: Boolean,
|
||||
val useCustomTransceivers: Boolean,
|
||||
val tleUrl: String,
|
||||
val transceiversUrl: String
|
||||
)
|
||||
@@ -0,0 +1,37 @@
|
||||
/*
|
||||
* Look4Sat. Amateur radio satellite tracker and pass predictor.
|
||||
* Copyright (C) 2019-2026 Arty Bishop and contributors.
|
||||
*
|
||||
* 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.core.domain.predict
|
||||
|
||||
const val ASTRONOMICAL_UNIT = 1.49597870691E8
|
||||
const val DEG2RAD = 0.017453292519943295
|
||||
const val RAD2DEG = 57.29577951308232
|
||||
const val EARTH_RADIUS = 6378.137
|
||||
const val EPSILON = 1.0E-12
|
||||
const val FLAT_FACT = 3.35281066474748E-3
|
||||
const val J3_HARMONIC = -2.53881E-6
|
||||
const val MIN_PER_DAY = 1.44E3
|
||||
const val SEC_PER_DAY = 8.6400E4
|
||||
const val SOLAR_RADIUS = 6.96000E5
|
||||
const val SPEED_OF_LIGHT = 2.99792458E8
|
||||
const val PI = 3.141592653589793
|
||||
const val PI_2 = PI / 2.0
|
||||
const val TWO_PI = PI * 2.0
|
||||
const val TWO_THIRDS = 2.0 / 3.0
|
||||
const val CK2 = 5.413079E-4
|
||||
const val CK4 = 6.209887E-7
|
||||
const val XKE = 7.43669161E-2
|
||||
+853
@@ -0,0 +1,853 @@
|
||||
/*
|
||||
* Look4Sat. Amateur radio satellite tracker and pass predictor.
|
||||
* Copyright (C) 2019-2026 Arty Bishop and contributors.
|
||||
*
|
||||
* 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.core.domain.predict
|
||||
|
||||
import kotlin.math.abs
|
||||
import kotlin.math.atan2
|
||||
import kotlin.math.cos
|
||||
import kotlin.math.floor
|
||||
import kotlin.math.pow
|
||||
import kotlin.math.sin
|
||||
import kotlin.math.sqrt
|
||||
|
||||
class DeepSpaceObject(data: OrbitalData) : OrbitalObject(data) {
|
||||
|
||||
private val c1: Double
|
||||
private val c4: Double
|
||||
private val x1mth2: Double
|
||||
private val x3thm1: Double
|
||||
private val xlcof: Double
|
||||
private val xnodcf: Double
|
||||
private val t2cof: Double
|
||||
private val aycof: Double
|
||||
private val x7thm1: Double
|
||||
private val deep: DeepSpaceCalculator
|
||||
private val dsv = DeepSpaceValueObject()
|
||||
|
||||
init {
|
||||
// Recover original mean motion (xnodp) and semimajor axis (aodp) from input elements
|
||||
val a1 = (XKE / data.xno).pow(TWO_THIRDS)
|
||||
dsv.cosio = cos(data.xincl)
|
||||
dsv.theta2 = dsv.cosio * dsv.cosio
|
||||
x3thm1 = 3.0 * dsv.theta2 - 1
|
||||
dsv.eosq = data.eccn * data.eccn
|
||||
dsv.betao2 = 1.0 - dsv.eosq
|
||||
dsv.betao = sqrt(dsv.betao2)
|
||||
val del1 = 1.5 * CK2 * x3thm1 / (a1 * a1 * dsv.betao * dsv.betao2)
|
||||
val ao = a1 * (1.0 - del1 * (0.5 * TWO_THIRDS + del1 * (1.0 + 134.0 / 81.0 * del1)))
|
||||
val delo = 1.5 * CK2 * x3thm1 / (ao * ao * dsv.betao * dsv.betao2)
|
||||
dsv.xnodp = data.xno / (1.0 + delo)
|
||||
dsv.aodp = ao / (1.0 - delo)
|
||||
// For perigee below 156 km, the values of S and QOMS2T are altered
|
||||
setPerigee((dsv.aodp * (1.0 - data.eccn) - 1.0) * EARTH_RADIUS)
|
||||
val pinvsq = invert(dsv.aodp * dsv.aodp * dsv.betao2 * dsv.betao2)
|
||||
dsv.sing = sin(data.omegao)
|
||||
dsv.cosg = cos(data.omegao)
|
||||
val tsi = invert(dsv.aodp - s4)
|
||||
val eta = dsv.aodp * data.eccn * tsi
|
||||
val etasq = eta * eta
|
||||
val eeta = data.eccn * eta
|
||||
val psisq = abs(1.0 - etasq)
|
||||
val coef = qoms24 * tsi.pow(4.0)
|
||||
val coef1 = coef / psisq.pow(3.5)
|
||||
val c2 = coef1 * dsv.xnodp * (dsv.aodp * (1.0 + 1.5 * etasq + eeta * (4.0 + etasq))
|
||||
+ 0.75 * CK2 * tsi / psisq * x3thm1 * (8.0 + 3.0 * etasq * (8.0 + etasq)))
|
||||
c1 = data.bstar * c2
|
||||
dsv.sinio = sin(data.xincl)
|
||||
val a3ovk2 = -J3_HARMONIC / CK2
|
||||
x1mth2 = 1.0 - dsv.theta2
|
||||
c4 =
|
||||
2 * dsv.xnodp * coef1 * dsv.aodp * dsv.betao2 * (eta * (2.0 + 0.5 * etasq) + data.eccn
|
||||
* (0.5 + 2 * etasq) - 2 * CK2 * tsi / (dsv.aodp * psisq)
|
||||
* (-3 * x3thm1 * (1.0 - 2 * eeta + etasq * (1.5 - 0.5 * eeta)) + (0.75 * x1mth2
|
||||
* (2.0 * etasq - eeta * (1.0 + etasq)) * cos(2.0 * data.omegao))))
|
||||
val theta4 = dsv.theta2 * dsv.theta2
|
||||
val temp1 = 3.0 * CK2 * pinvsq * dsv.xnodp
|
||||
val temp2 = temp1 * CK2 * pinvsq
|
||||
val temp3 = 1.25 * CK4 * pinvsq * pinvsq * dsv.xnodp
|
||||
dsv.xmdot =
|
||||
dsv.xnodp + 0.5 * temp1 * dsv.betao * x3thm1 + 0.0625 * temp2 * dsv.betao * (13 - 78 * dsv.theta2 + 137 * theta4)
|
||||
val x1m5th = 1.0 - 5 * dsv.theta2
|
||||
dsv.omgdot =
|
||||
-0.5 * temp1 * x1m5th + 0.0625 * temp2 * (7.0 - 114 * dsv.theta2 + 395 * theta4) + temp3 * (3.0 - 36 * dsv.theta2 + 49 * theta4)
|
||||
val xhdot1 = -temp1 * dsv.cosio
|
||||
dsv.xnodot =
|
||||
xhdot1 + (0.5 * temp2 * (4.0 - 19 * dsv.theta2) + 2 * temp3 * (3.0 - 7 * dsv.theta2)) * dsv.cosio
|
||||
xnodcf = 3.5 * dsv.betao2 * xhdot1 * c1
|
||||
t2cof = 1.5 * c1
|
||||
xlcof = 0.125 * a3ovk2 * dsv.sinio * (3.0 + 5 * dsv.cosio) / (1.0 + dsv.cosio)
|
||||
aycof = 0.25 * a3ovk2 * dsv.sinio
|
||||
x7thm1 = 7.0 * dsv.theta2 - 1
|
||||
deep = DeepSpaceCalculator(dsv)
|
||||
}
|
||||
|
||||
internal fun calculateSDP4(tSince: Double) {
|
||||
synchronized(this) {
|
||||
val temp = DoubleArray(12)
|
||||
val xmdf = data.xmo + dsv.xmdot * tSince
|
||||
val tsq = tSince * tSince
|
||||
val templ = t2cof * tsq
|
||||
dsv.xll = xmdf + dsv.xnodp * templ
|
||||
dsv.omgadf = data.omegao + dsv.omgdot * tSince
|
||||
val xnoddf = data.xnodeo + dsv.xnodot * tSince
|
||||
dsv.xnode = xnoddf + xnodcf * tsq
|
||||
val tempa = 1.0 - c1 * tSince
|
||||
val tempe = data.bstar * c4 * tSince
|
||||
dsv.xn = dsv.xnodp
|
||||
dsv.t = tSince
|
||||
deep.dpsec(data)
|
||||
val a = (XKE / dsv.xn).pow(TWO_THIRDS) * tempa * tempa
|
||||
dsv.em -= tempe
|
||||
deep.dpper()
|
||||
val xl = dsv.xll + dsv.omgadf + dsv.xnode
|
||||
val beta = sqrt(1.0 - dsv.em * dsv.em)
|
||||
dsv.xn = XKE / a.pow(1.5)
|
||||
// Long period periodics
|
||||
val axn = dsv.em * cos(dsv.omgadf)
|
||||
temp[0] = invert(a * beta * beta)
|
||||
val xll = temp[0] * xlcof * axn
|
||||
val aynl = temp[0] * aycof
|
||||
val xlt = xl + xll
|
||||
val ayn = dsv.em * sin(dsv.omgadf) + aynl
|
||||
// Solve Kepler's equation
|
||||
val capu = mod2PI(xlt - dsv.xnode)
|
||||
temp[2] = capu
|
||||
converge(temp, axn, ayn, capu)
|
||||
calculatePosAndVel(temp, a, axn, ayn)
|
||||
calculatePhase(xlt, dsv.xnode, dsv.omgadf)
|
||||
}
|
||||
}
|
||||
|
||||
private fun calculatePosAndVel(temp: DoubleArray, a: Double, axn: Double, ayn: Double) {
|
||||
val ecose = temp[5] + temp[6]
|
||||
val esine = temp[3] - temp[4]
|
||||
val elsq = axn * axn + ayn * ayn
|
||||
temp[0] = 1.0 - elsq
|
||||
val pl = a * temp[0]
|
||||
temp[9] = a * (1.0 - ecose)
|
||||
temp[1] = invert(temp[9])
|
||||
temp[10] = XKE * sqrt(a) * esine * temp[1]
|
||||
temp[11] = XKE * sqrt(pl) * temp[1]
|
||||
temp[2] = a * temp[1]
|
||||
val betal = sqrt(temp[0])
|
||||
temp[3] = invert(1.0 + betal)
|
||||
val cosu = temp[2] * (temp[8] - axn + ayn * esine * temp[3])
|
||||
val sinu = temp[2] * (temp[7] - ayn - axn * esine * temp[3])
|
||||
val u = atan2(sinu, cosu)
|
||||
val sin2u = 2.0 * sinu * cosu
|
||||
val cos2u = 2.0 * cosu * cosu - 1
|
||||
temp[0] = invert(pl)
|
||||
temp[1] = CK2 * temp[0]
|
||||
temp[2] = temp[1] * temp[0]
|
||||
// Update for short periodics
|
||||
val rk = temp[9] * (1.0 - 1.5 * temp[2] * betal * x3thm1) + 0.5 * temp[1] * x1mth2 * cos2u
|
||||
val uk = u - 0.25 * temp[2] * x7thm1 * sin2u
|
||||
val xnodek = dsv.xnode + 1.5 * temp[2] * dsv.cosio * sin2u
|
||||
val xinck = dsv.xinc + 1.5 * temp[2] * dsv.cosio * dsv.sinio * cos2u
|
||||
val rdotk = temp[10] - dsv.xn * temp[1] * x1mth2 * sin2u
|
||||
val rfdotk = temp[11] + dsv.xn * temp[1] * (x1mth2 * cos2u + 1.5 * x3thm1)
|
||||
super.calculatePosAndVel(rk, uk, xnodek, xinck, rdotk, rfdotk)
|
||||
}
|
||||
|
||||
class DeepSpaceValueObject {
|
||||
|
||||
var eosq = 0.0
|
||||
var sinio = 0.0
|
||||
var cosio = 0.0
|
||||
var betao = 0.0
|
||||
var aodp = 0.0
|
||||
var theta2 = 0.0
|
||||
var sing = 0.0
|
||||
var cosg = 0.0
|
||||
var betao2 = 0.0
|
||||
var xmdot = 0.0
|
||||
var omgdot = 0.0
|
||||
var xnodot = 0.0
|
||||
var xnodp = 0.0
|
||||
|
||||
// Used by dpsec and dpper parts of Deep()
|
||||
var xll = 0.0
|
||||
var omgadf = 0.0
|
||||
var xnode = 0.0
|
||||
var em = 0.0
|
||||
var xinc = 0.0
|
||||
var xn = 0.0
|
||||
var t = 0.0
|
||||
|
||||
// Used by thetg and Deep()
|
||||
var ds50 = 0.0
|
||||
}
|
||||
|
||||
inner class DeepSpaceCalculator(private val dsv: DeepSpaceValueObject) {
|
||||
|
||||
private val zSinis = 3.9785416E-1
|
||||
private val zSings = -9.8088458E-1
|
||||
private val zNs = 1.19459E-5
|
||||
private val c1ss = 2.9864797E-6
|
||||
private val zEs = 1.675E-2
|
||||
private val zNl = 1.5835218E-4
|
||||
private val c1l = 4.7968065E-7
|
||||
private val zEl = 5.490E-2
|
||||
private val root22 = 1.7891679E-6
|
||||
private val root32 = 3.7393792E-7
|
||||
private val root44 = 7.3636953E-9
|
||||
private val root52 = 1.1428639E-7
|
||||
private val root54 = 2.1765803E-9
|
||||
private val tHdt = 4.3752691E-3
|
||||
private val q22 = 1.7891679E-6
|
||||
private val q31 = 2.1460748E-6
|
||||
private val q33 = 2.2123015E-7
|
||||
private val g22 = 5.7686396
|
||||
private val g32 = 9.5240898E-1
|
||||
private val g44 = 1.8014998
|
||||
private val g52 = 1.0508330
|
||||
private val g54 = 4.4108898
|
||||
private val thgr: Double
|
||||
private val xnq: Double
|
||||
private val xqncl: Double
|
||||
private val omegaq: Double
|
||||
private var zmol = 0.0
|
||||
private var zmos = 0.0
|
||||
|
||||
// Many fields below cannot be final because they are iteratively refined
|
||||
private var savtsn = 0.0
|
||||
private var ee2 = 0.0
|
||||
private var e3 = 0.0
|
||||
private var xi2 = 0.0
|
||||
private var xl2 = 0.0
|
||||
private var xl3 = 0.0
|
||||
private var xl4 = 0.0
|
||||
private var xgh2 = 0.0
|
||||
private var xgh3 = 0.0
|
||||
private var xgh4 = 0.0
|
||||
private var xh2 = 0.0
|
||||
private var xh3 = 0.0
|
||||
private var sse = 0.0
|
||||
private var ssi = 0.0
|
||||
private var ssg = 0.0
|
||||
private var xi3 = 0.0
|
||||
private var se2 = 0.0
|
||||
private var si2 = 0.0
|
||||
private var sl2 = 0.0
|
||||
private var sgh2 = 0.0
|
||||
private var sh2 = 0.0
|
||||
private var se3 = 0.0
|
||||
private var si3 = 0.0
|
||||
private var sl3 = 0.0
|
||||
private var sgh3 = 0.0
|
||||
private var sh3 = 0.0
|
||||
private var sl4 = 0.0
|
||||
private var sgh4 = 0.0
|
||||
private var ssl = 0.0
|
||||
private var ssh = 0.0
|
||||
private var d3210 = 0.0
|
||||
private var d3222 = 0.0
|
||||
private var d4410 = 0.0
|
||||
private var d4422 = 0.0
|
||||
private var d5220 = 0.0
|
||||
private var d5232 = 0.0
|
||||
private var d5421 = 0.0
|
||||
private var d5433 = 0.0
|
||||
private var del1 = 0.0
|
||||
private var del2 = 0.0
|
||||
private var del3 = 0.0
|
||||
private var fasx2 = 0.0
|
||||
private var fasx4 = 0.0
|
||||
private var fasx6 = 0.0
|
||||
private var xlamo = 0.0
|
||||
private val xfact: Double
|
||||
private var xni: Double
|
||||
private var atime: Double
|
||||
private val stepp: Double
|
||||
private val stepn: Double
|
||||
private val step2: Double
|
||||
private var preep = 0.0
|
||||
private var pl = 0.0
|
||||
private var sghs = 0.0
|
||||
private var xli: Double
|
||||
private var d2201 = 0.0
|
||||
private var d2211 = 0.0
|
||||
private var sghl = 0.0
|
||||
private var sh1 = 0.0
|
||||
private var pinc = 0.0
|
||||
private var pe = 0.0
|
||||
private var shs = 0.0
|
||||
private var zsingl = 0.0
|
||||
private var zcosgl = 0.0
|
||||
private var zsinhl = 0.0
|
||||
private var zcoshl = 0.0
|
||||
private var zsinil = 0.0
|
||||
private var zcosil = 0.0
|
||||
private var a1 = 0.0
|
||||
private var a2 = 0.0
|
||||
private var a3 = 0.0
|
||||
private var a4 = 0.0
|
||||
private var a5 = 0.0
|
||||
private var a6 = 0.0
|
||||
private var a7 = 0.0
|
||||
private var a8 = 0.0
|
||||
private var a9 = 0.0
|
||||
private var a10 = 0.0
|
||||
private var ainv2 = 0.0
|
||||
private var alfdp = 0.0
|
||||
private val aqnv: Double
|
||||
private var sgh = 0.0
|
||||
private var sini2 = 0.0
|
||||
private var sinis = 0.0
|
||||
private var sinok = 0.0
|
||||
private var sh = 0.0
|
||||
private var si = 0.0
|
||||
private var sil = 0.0
|
||||
private val day: Double
|
||||
private var betdp = 0.0
|
||||
private var dalf = 0.0
|
||||
private var bfact = 0.0
|
||||
private var c = 0.0
|
||||
private var cc = 0.0
|
||||
private var cosis = 0.0
|
||||
private var cosok = 0.0
|
||||
private val cosq: Double
|
||||
private var ctem = 0.0
|
||||
private var f322 = 0.0
|
||||
private var zx = 0.0
|
||||
private var zy = 0.0
|
||||
private var dbet = 0.0
|
||||
private var dls = 0.0
|
||||
private var eoc = 0.0
|
||||
private val eq: Double
|
||||
private var f2 = 0.0
|
||||
private var f220 = 0.0
|
||||
private var f221 = 0.0
|
||||
private var f3 = 0.0
|
||||
private var f311 = 0.0
|
||||
private var f321 = 0.0
|
||||
private var xnoh = 0.0
|
||||
private var f330 = 0.0
|
||||
private var f441 = 0.0
|
||||
private var f442 = 0.0
|
||||
private var f522 = 0.0
|
||||
private var f523 = 0.0
|
||||
private var f542 = 0.0
|
||||
private var f543 = 0.0
|
||||
private var g200 = 0.0
|
||||
private var g201 = 0.0
|
||||
private var g211 = 0.0
|
||||
private var pgh = 0.0
|
||||
private var ph = 0.0
|
||||
private var s1 = 0.0
|
||||
private var s2 = 0.0
|
||||
private var s3 = 0.0
|
||||
private var s4 = 0.0
|
||||
private var s5 = 0.0
|
||||
private var s6 = 0.0
|
||||
private var s7 = 0.0
|
||||
private var se = 0.0
|
||||
private var sel = 0.0
|
||||
private var ses = 0.0
|
||||
private var xls = 0.0
|
||||
private var g300 = 0.0
|
||||
private var g310 = 0.0
|
||||
private var g322 = 0.0
|
||||
private var g410 = 0.0
|
||||
private var g422 = 0.0
|
||||
private var g520 = 0.0
|
||||
private var g521 = 0.0
|
||||
private var g532 = 0.0
|
||||
private var g533 = 0.0
|
||||
private var gam = 0.0
|
||||
private val sinq: Double
|
||||
private var sinzf = 0.0
|
||||
private var sis = 0.0
|
||||
private var sl = 0.0
|
||||
private var sll = 0.0
|
||||
private var sls = 0.0
|
||||
private var stem = 0.0
|
||||
private var temp = 0.0
|
||||
private var temp1 = 0.0
|
||||
private var x1 = 0.0
|
||||
private var x2 = 0.0
|
||||
private var x2li = 0.0
|
||||
private var x2omi = 0.0
|
||||
private var x3 = 0.0
|
||||
private var x4 = 0.0
|
||||
private var x5 = 0.0
|
||||
private var x6 = 0.0
|
||||
private var x7 = 0.0
|
||||
private var x8 = 0.0
|
||||
private var xl = 0.0
|
||||
private var xldot = 0.0
|
||||
private val xmao: Double
|
||||
private var xnddt = 0.0
|
||||
private var xndot = 0.0
|
||||
private var xno2 = 0.0
|
||||
private var xnodce = 0.0
|
||||
private var xnoi = 0.0
|
||||
private var xomi = 0.0
|
||||
private val xpidot: Double
|
||||
private var z1 = 0.0
|
||||
private var z11 = 0.0
|
||||
private var z12 = 0.0
|
||||
private var z13 = 0.0
|
||||
private var z2 = 0.0
|
||||
private var z21 = 0.0
|
||||
private var z22 = 0.0
|
||||
private var z23 = 0.0
|
||||
private var z3 = 0.0
|
||||
private var z31 = 0.0
|
||||
private var z32 = 0.0
|
||||
private var z33 = 0.0
|
||||
private var ze = 0.0
|
||||
private var zf = 0.0
|
||||
private var zm = 0.0
|
||||
private var zn = 0.0
|
||||
private var zsing = 0.0
|
||||
private var zsinh = 0.0
|
||||
private var zsini = 0.0
|
||||
private var zcosg = 0.0
|
||||
private var zcosh = 0.0
|
||||
private var zcosi = 0.0
|
||||
private var delt = 0.0
|
||||
private var ft = 0.0
|
||||
private var resonance: Boolean
|
||||
private var synchronous: Boolean
|
||||
private var doLoop = false
|
||||
private var epochRestart = false
|
||||
|
||||
init {
|
||||
thgr = thetaG(data.epoch)
|
||||
eq = data.eccn
|
||||
xnq = dsv.xnodp
|
||||
aqnv = invert(dsv.aodp)
|
||||
xqncl = data.xincl
|
||||
xmao = data.xmo
|
||||
xpidot = dsv.omgdot + dsv.xnodot
|
||||
sinq = sin(data.xnodeo)
|
||||
cosq = cos(data.xnodeo)
|
||||
omegaq = data.omegao
|
||||
// Initialize lunar solar terms, days since 1900 Jan 0.5
|
||||
day = dsv.ds50 + 18261.5
|
||||
if (abs(day - preep) > 1.0E-6) {
|
||||
preep = day
|
||||
xnodce = 4.5236020 - 9.2422029E-4 * day
|
||||
stem = sin(xnodce)
|
||||
ctem = cos(xnodce)
|
||||
zcosil = 0.91375164 - 0.03568096 * ctem
|
||||
zsinil = sqrt(1.0 - zcosil * zcosil)
|
||||
zsinhl = 0.089683511 * stem / zsinil
|
||||
zcoshl = sqrt(1.0 - zsinhl * zsinhl)
|
||||
c = 4.7199672 + 0.22997150 * day
|
||||
gam = 5.8351514 + 0.0019443680 * day
|
||||
zmol = mod2PI(c - gam)
|
||||
zx = 0.39785416 * stem / zsinil
|
||||
zy = zcoshl * ctem + 0.91744867 * zsinhl * stem
|
||||
zx = atan2(zx, zy)
|
||||
zx = gam + zx - xnodce
|
||||
zcosgl = cos(zx)
|
||||
zsingl = sin(zx)
|
||||
zmos = mod2PI(6.2565837 + 0.017201977 * day)
|
||||
} else {
|
||||
zmol = 0.0
|
||||
zmos = 0.0
|
||||
}
|
||||
doSolarTerms()
|
||||
|
||||
// Geopotential resonance initialization for 12 hour orbits
|
||||
resonance = false
|
||||
synchronous = false
|
||||
if (!(xnq < 0.0052359877 && xnq > 0.0034906585)) {
|
||||
if (xnq !in 0.00826..0.00924)
|
||||
if (eq < 0.5)
|
||||
// calculateResonance
|
||||
resonance = true
|
||||
eoc = eq * dsv.eosq
|
||||
g201 = -0.306 - (eq - 0.64) * 0.440
|
||||
if (eq <= 0.65) {
|
||||
g211 = 3.616 - 13.247 * eq + 16.290 * dsv.eosq
|
||||
g310 = -19.302 + 117.390 * eq - 228.419 * dsv.eosq + 156.591 * eoc
|
||||
g322 = -18.9068 + 109.7927 * eq - 214.6334 * dsv.eosq + 146.5816 * eoc
|
||||
g410 = -41.122 + 242.694 * eq - 471.094 * dsv.eosq + 313.953 * eoc
|
||||
g422 = -146.407 + 841.880 * eq - 1629.014 * dsv.eosq + 1083.435 * eoc
|
||||
g520 = -532.114 + 3017.977 * eq - 5740 * dsv.eosq + 3708.276 * eoc
|
||||
} else {
|
||||
g211 = -72.099 + 331.819 * eq - 508.738 * dsv.eosq + 266.724 * eoc
|
||||
g310 = -346.844 + 1582.851 * eq - 2415.925 * dsv.eosq + 1246.113 * eoc
|
||||
g322 = -342.585 + 1554.908 * eq - 2366.899 * dsv.eosq + 1215.972 * eoc
|
||||
g410 = -1052.797 + 4758.686 * eq - 7193.992 * dsv.eosq + 3651.957 * eoc
|
||||
g422 = -3581.69 + 16178.11 * eq - 24462.77 * dsv.eosq + 12422.52 * eoc
|
||||
g520 =
|
||||
if (eq <= 0.715) 1464.74 - 4664.75 * eq + 3763.64 * dsv.eosq
|
||||
else -5149.66 + 29936.92 * eq - 54087.36 * dsv.eosq + 31324.56 * eoc
|
||||
}
|
||||
if (eq < 0.7) {
|
||||
g533 = -919.2277 + 4988.61 * eq - 9064.77 * dsv.eosq + 5542.21 * eoc
|
||||
g521 = -822.71072 + 4568.6173 * eq - 8491.4146 * dsv.eosq + 5337.524 * eoc
|
||||
g532 = -853.666 + 4690.25 * eq - 8624.77 * dsv.eosq + 5341.4 * eoc
|
||||
} else {
|
||||
g533 = -37995.78 + 161616.52 * eq - 229838.2 * dsv.eosq + 109377.94 * eoc
|
||||
g521 = -51752.104 + 218913.95 * eq - 309468.16 * dsv.eosq + 146349.42 * eoc
|
||||
g532 = -40023.88 + 170470.89 * eq - 242699.48 * dsv.eosq + 115605.82 * eoc
|
||||
}
|
||||
sini2 = dsv.sinio * dsv.sinio
|
||||
f220 = 0.75 * (1.0 + 2 * dsv.cosio + dsv.theta2)
|
||||
f221 = 1.5 * sini2
|
||||
f321 = 1.875 * dsv.sinio * (1.0 - 2 * dsv.cosio - 3.0 * dsv.theta2)
|
||||
f322 = -1.875 * dsv.sinio * (1.0 + 2 * dsv.cosio - 3.0 * dsv.theta2)
|
||||
f441 = 35 * sini2 * f220
|
||||
f442 = 39.3750 * sini2 * sini2
|
||||
f522 =
|
||||
9.84375 * dsv.sinio * (sini2 * (1.0 - 2 * dsv.cosio - 5 * dsv.theta2) + 0.33333333 * (-2 + 4 * dsv.cosio + 6 * dsv.theta2))
|
||||
f523 =
|
||||
dsv.sinio * (4.92187512 * sini2 * (-2 - 4 * dsv.cosio + 10 * dsv.theta2) + 6.56250012 * (1.0 + 2 * dsv.cosio - 3.0 * dsv.theta2))
|
||||
f542 =
|
||||
29.53125 * dsv.sinio * (2.0 - 8 * dsv.cosio + dsv.theta2 * (-12 + 8 * dsv.cosio + 10 * dsv.theta2))
|
||||
f543 =
|
||||
29.53125 * dsv.sinio * (-2 - 8 * dsv.cosio + dsv.theta2 * (12 + 8 * dsv.cosio - 10 * dsv.theta2))
|
||||
xno2 = xnq * xnq
|
||||
ainv2 = aqnv * aqnv
|
||||
temp1 = 3.0 * xno2 * ainv2
|
||||
temp = temp1 * root22
|
||||
d2201 = temp * f220 * g201
|
||||
d2211 = temp * f221 * g211
|
||||
temp1 *= aqnv
|
||||
temp = temp1 * root32
|
||||
d3210 = temp * f321 * g310
|
||||
d3222 = temp * f322 * g322
|
||||
temp1 *= aqnv
|
||||
temp = 2.0 * temp1 * root44
|
||||
d4410 = temp * f441 * g410
|
||||
d4422 = temp * f442 * g422
|
||||
temp1 *= aqnv
|
||||
temp = temp1 * root52
|
||||
d5220 = temp * f522 * g520
|
||||
d5232 = temp * f523 * g532
|
||||
temp = 2.0 * temp1 * root54
|
||||
d5421 = temp * f542 * g521
|
||||
d5433 = temp * f543 * g533
|
||||
xlamo = xmao + data.xnodeo + data.xnodeo - thgr - thgr
|
||||
bfact = dsv.xmdot + dsv.xnodot + dsv.xnodot - tHdt - tHdt
|
||||
bfact += ssl + ssh + ssh
|
||||
} else {
|
||||
// Init synchronous resonance terms
|
||||
resonance = true
|
||||
synchronous = true
|
||||
g200 = 1.0 + dsv.eosq * (-2.5 + 0.8125 * dsv.eosq)
|
||||
g310 = 1.0 + 2 * dsv.eosq
|
||||
g300 = 1.0 + dsv.eosq * (-6 + 6.60937 * dsv.eosq)
|
||||
f220 = 0.75 * (1.0 + dsv.cosio) * (1.0 + dsv.cosio)
|
||||
f311 =
|
||||
0.9375 * dsv.sinio * dsv.sinio * (1.0 + 3.0 * dsv.cosio) - 0.75 * (1.0 + dsv.cosio)
|
||||
f330 = 1.0 + dsv.cosio
|
||||
f330 *= 1.875 * f330 * f330
|
||||
del1 = 3.0 * xnq * xnq * aqnv * aqnv
|
||||
del2 = 2.0 * del1 * f220 * g200 * q22
|
||||
del3 = 3.0 * del1 * f330 * g300 * q33 * aqnv
|
||||
del1 *= f311 * g310 * q31 * aqnv
|
||||
fasx2 = 0.13130908
|
||||
fasx4 = 2.8843198
|
||||
fasx6 = 0.37448087
|
||||
xlamo = xmao + data.xnodeo + data.omegao - thgr
|
||||
bfact = dsv.xmdot + xpidot - tHdt
|
||||
bfact += ssl + ssg + ssh
|
||||
}
|
||||
xfact = bfact - xnq
|
||||
// Init integrator
|
||||
xli = xlamo
|
||||
xni = xnq
|
||||
atime = 0.0
|
||||
stepp = 720.0
|
||||
stepn = -720.0
|
||||
step2 = 259200.0
|
||||
}
|
||||
|
||||
// Entrance for lunar-solar periodics
|
||||
fun dpper() {
|
||||
sinis = sin(dsv.xinc)
|
||||
cosis = cos(dsv.xinc)
|
||||
if (abs(savtsn - dsv.t) >= 30) {
|
||||
savtsn = dsv.t
|
||||
zm = zmos + zNs * dsv.t
|
||||
zf = zm + 2 * zEs * sin(zm)
|
||||
sinzf = sin(zf)
|
||||
f2 = 0.5 * sinzf * sinzf - 0.25
|
||||
f3 = -0.5 * sinzf * cos(zf)
|
||||
ses = se2 * f2 + se3 * f3
|
||||
sis = si2 * f2 + si3 * f3
|
||||
sls = sl2 * f2 + sl3 * f3 + sl4 * sinzf
|
||||
sghs = sgh2 * f2 + sgh3 * f3 + sgh4 * sinzf
|
||||
shs = sh2 * f2 + sh3 * f3
|
||||
zm = zmol + zNl * dsv.t
|
||||
zf = zm + 2 * zEl * sin(zm)
|
||||
sinzf = sin(zf)
|
||||
f2 = 0.5 * sinzf * sinzf - 0.25
|
||||
f3 = -0.5 * sinzf * cos(zf)
|
||||
sel = ee2 * f2 + e3 * f3
|
||||
sil = xi2 * f2 + xi3 * f3
|
||||
sll = xl2 * f2 + xl3 * f3 + xl4 * sinzf
|
||||
sghl = xgh2 * f2 + xgh3 * f3 + xgh4 * sinzf
|
||||
sh1 = xh2 * f2 + xh3 * f3
|
||||
pe = ses + sel
|
||||
pinc = sis + sil
|
||||
pl = sls + sll
|
||||
}
|
||||
pgh = sghs + sghl
|
||||
ph = shs + sh1
|
||||
dsv.xinc += pinc
|
||||
dsv.em += pe
|
||||
if (xqncl >= 0.2) {
|
||||
/* Apply periodics directly */
|
||||
ph /= dsv.sinio
|
||||
pgh -= dsv.cosio * ph
|
||||
dsv.omgadf += pgh
|
||||
dsv.xnode += ph
|
||||
dsv.xll += pl
|
||||
} else {
|
||||
applyPeriodics()
|
||||
// This is a patch to Lyddane modification suggested by Rob Matson
|
||||
if (abs(xnoh - dsv.xnode) > PI) {
|
||||
if (dsv.xnode < xnoh) dsv.xnode += TWO_PI else dsv.xnode -= TWO_PI
|
||||
}
|
||||
dsv.xll += pl
|
||||
dsv.omgadf = xls - dsv.xll - cos(dsv.xinc) * dsv.xnode
|
||||
}
|
||||
}
|
||||
|
||||
// Entrance for deep space secular effects
|
||||
fun dpsec(params: OrbitalData) {
|
||||
dsv.xll += ssl * dsv.t
|
||||
dsv.omgadf += ssg * dsv.t
|
||||
dsv.xnode += ssh * dsv.t
|
||||
dsv.em = params.eccn + sse * dsv.t
|
||||
dsv.xinc = params.xincl + ssi * dsv.t
|
||||
if (dsv.xinc < 0) {
|
||||
dsv.xinc = -dsv.xinc
|
||||
dsv.xnode += PI
|
||||
dsv.omgadf -= PI
|
||||
}
|
||||
if (!resonance) return
|
||||
do processEpochRestartLoop() while (doLoop && epochRestart)
|
||||
dsv.xn = xni + xndot * ft + xnddt * ft * ft * 0.5
|
||||
xl = xli + xldot * ft + xndot * ft * ft * 0.5
|
||||
temp = -dsv.xnode + thgr + dsv.t * tHdt
|
||||
if (synchronous) dsv.xll = xl - dsv.omgadf + temp else dsv.xll = xl + temp + temp
|
||||
}
|
||||
|
||||
private fun doSolarTerms() {
|
||||
savtsn = 1E20
|
||||
zcosg = 1.945905E-1
|
||||
zsing = zSings
|
||||
zcosi = 9.1744867E-1
|
||||
zsini = zSinis
|
||||
zcosh = cosq
|
||||
zsinh = sinq
|
||||
cc = c1ss
|
||||
zn = zNs
|
||||
ze = zEs
|
||||
xnoi = invert(xnq)
|
||||
calculateSolarTerms()
|
||||
calculateLunarTerms()
|
||||
calculateSolarTerms() // Solar terms done again after Lunar terms are done
|
||||
sse += se
|
||||
ssi += si
|
||||
ssl += sl
|
||||
ssg = ssg + sgh - dsv.cosio / dsv.sinio * sh
|
||||
ssh += sh / dsv.sinio
|
||||
}
|
||||
|
||||
private fun calculateLunarTerms() {
|
||||
sse = se
|
||||
ssi = si
|
||||
ssl = sl
|
||||
ssh = sh / dsv.sinio
|
||||
ssg = sgh - dsv.cosio * ssh
|
||||
se2 = ee2
|
||||
si2 = xi2
|
||||
sl2 = xl2
|
||||
sgh2 = xgh2
|
||||
sh2 = xh2
|
||||
se3 = e3
|
||||
si3 = xi3
|
||||
sl3 = xl3
|
||||
sgh3 = xgh3
|
||||
sh3 = xh3
|
||||
sl4 = xl4
|
||||
sgh4 = xgh4
|
||||
zcosg = zcosgl
|
||||
zsing = zsingl
|
||||
zcosi = zcosil
|
||||
zsini = zsinil
|
||||
zcosh = zcoshl * cosq + zsinhl * sinq
|
||||
zsinh = sinq * zcoshl - cosq * zsinhl
|
||||
zn = zNl
|
||||
cc = c1l
|
||||
ze = zEl
|
||||
}
|
||||
|
||||
private fun calculateSolarTerms() {
|
||||
a1 = zcosg * zcosh + zsing * zcosi * zsinh
|
||||
a3 = -zsing * zcosh + zcosg * zcosi * zsinh
|
||||
a7 = -zcosg * zsinh + zsing * zcosi * zcosh
|
||||
a8 = zsing * zsini
|
||||
a9 = zsing * zsinh + zcosg * zcosi * zcosh
|
||||
a10 = zcosg * zsini
|
||||
a2 = dsv.cosio * a7 + dsv.sinio * a8
|
||||
a4 = dsv.cosio * a9 + dsv.sinio * a10
|
||||
a5 = -dsv.sinio * a7 + dsv.cosio * a8
|
||||
a6 = -dsv.sinio * a9 + dsv.cosio * a10
|
||||
x1 = a1 * dsv.cosg + a2 * dsv.sing
|
||||
x2 = a3 * dsv.cosg + a4 * dsv.sing
|
||||
x3 = -a1 * dsv.sing + a2 * dsv.cosg
|
||||
x4 = -a3 * dsv.sing + a4 * dsv.cosg
|
||||
x5 = a5 * dsv.sing
|
||||
x6 = a6 * dsv.sing
|
||||
x7 = a5 * dsv.cosg
|
||||
x8 = a6 * dsv.cosg
|
||||
z31 = 12 * x1 * x1 - 3.0 * x3 * x3
|
||||
z32 = 24 * x1 * x2 - 6 * x3 * x4
|
||||
z33 = 12 * x2 * x2 - 3.0 * x4 * x4
|
||||
z1 = 3.0 * (a1 * a1 + a2 * a2) + z31 * dsv.eosq
|
||||
z2 = 6.0 * (a1 * a3 + a2 * a4) + z32 * dsv.eosq
|
||||
z3 = 3.0 * (a3 * a3 + a4 * a4) + z33 * dsv.eosq
|
||||
z11 = -6 * a1 * a5 + dsv.eosq * (-24 * x1 * x7 - 6 * x3 * x5)
|
||||
z12 =
|
||||
-6 * (a1 * a6 + a3 * a5) + dsv.eosq * (-24 * (x2 * x7 + x1 * x8) - 6 * (x3 * x6 + x4 * x5))
|
||||
z13 = -6 * a3 * a6 + dsv.eosq * (-24 * x2 * x8 - 6 * x4 * x6)
|
||||
z21 = 6.0 * a2 * a5 + dsv.eosq * (24 * x1 * x5 - 6 * x3 * x7)
|
||||
z22 =
|
||||
6.0 * (a4 * a5 + a2 * a6) + dsv.eosq * (24 * (x2 * x5 + x1 * x6) - 6 * (x4 * x7 + x3 * x8))
|
||||
z23 = 6.0 * a4 * a6 + dsv.eosq * (24 * x2 * x6 - 6 * x4 * x8)
|
||||
z1 += z1 + dsv.betao2 * z31
|
||||
z2 += z2 + dsv.betao2 * z32
|
||||
z3 += z3 + dsv.betao2 * z33
|
||||
s3 = cc * xnoi
|
||||
s2 = -0.5 * s3 / dsv.betao
|
||||
s4 = s3 * dsv.betao
|
||||
s1 = -15 * eq * s4
|
||||
s5 = x1 * x3 + x2 * x4
|
||||
s6 = x2 * x3 + x1 * x4
|
||||
s7 = x2 * x4 - x1 * x3
|
||||
se = s1 * zn * s5
|
||||
si = s2 * zn * (z11 + z13)
|
||||
sl = -zn * s3 * (z1 + z3 - 14 - 6 * dsv.eosq)
|
||||
sgh = s4 * zn * (z31 + z33 - 6)
|
||||
sh = -zn * s2 * (z21 + z23)
|
||||
if (xqncl < 5.2359877E-2) sh = 0.0
|
||||
ee2 = 2.0 * s1 * s6
|
||||
e3 = 2.0 * s1 * s7
|
||||
xi2 = 2.0 * s2 * z12
|
||||
xi3 = 2.0 * s2 * (z13 - z11)
|
||||
xl2 = -2 * s3 * z2
|
||||
xl3 = -2 * s3 * (z3 - z1)
|
||||
xl4 = -2 * s3 * (-21 - 9 * dsv.eosq) * ze
|
||||
xgh2 = 2.0 * s4 * z32
|
||||
xgh3 = 2.0 * s4 * (z33 - z31)
|
||||
xgh4 = -18 * s4 * ze
|
||||
xh2 = -2 * s2 * z22
|
||||
xh3 = -2 * s2 * (z23 - z21)
|
||||
}
|
||||
|
||||
private fun processEpochRestartLoop() {
|
||||
if (atime == 0.0 || dsv.t >= 0 && atime < 0 || dsv.t < 0 && atime >= 0) {
|
||||
calculateDelta()
|
||||
atime = 0.0
|
||||
xni = xnq
|
||||
xli = xlamo
|
||||
} else if (abs(dsv.t) >= abs(atime)) calculateDelta()
|
||||
processNotEpochRestartLoop()
|
||||
}
|
||||
|
||||
private fun calculateDelta() {
|
||||
delt = if (dsv.t < 0) stepn else stepp
|
||||
}
|
||||
|
||||
private fun processNotEpochRestartLoop() {
|
||||
do {
|
||||
if (abs(dsv.t - atime) >= stepp) {
|
||||
doLoop = true
|
||||
epochRestart = false
|
||||
} else {
|
||||
ft = dsv.t - atime
|
||||
doLoop = false
|
||||
}
|
||||
if (abs(dsv.t) < abs(atime)) {
|
||||
delt = if (dsv.t >= 0) stepn else stepp
|
||||
doLoop = doLoop or epochRestart
|
||||
}
|
||||
if (synchronous) {
|
||||
xndot = del1 * sin(xli - fasx2) + del2 * sin(2.0 * (xli - fasx4)) +
|
||||
del3 * sin(3.0 * (xli - fasx6))
|
||||
xnddt = del1 * cos(xli - fasx2) + 2 * del2 * cos(2.0 * (xli - fasx4)) +
|
||||
3.0 * del3 * cos(3.0 * (xli - fasx6))
|
||||
} else {
|
||||
xomi = omegaq + dsv.omgdot * atime
|
||||
x2omi = xomi + xomi
|
||||
x2li = xli + xli
|
||||
xndot =
|
||||
d2201 * sin(x2omi + xli - g22) + d2211 * sin(xli - g22) + (d3210
|
||||
* sin(xomi + xli - g32)) + d3222 * sin(-xomi + xli - g32) + (d4410
|
||||
* sin(x2omi + x2li - g44)) + d4422 * sin(x2li - g44) + (d5220
|
||||
* sin(xomi + xli - g52)) + d5232 * sin(-xomi + xli - g52) + (d5421
|
||||
* sin(xomi + x2li - g54)) + d5433 * sin(-xomi + x2li - g54)
|
||||
xnddt =
|
||||
d2201 * cos(x2omi + xli - g22) + d2211 * cos(xli - g22) + (d3210
|
||||
* cos(xomi + xli - g32)) + d3222 * cos(-xomi + xli - g32) + (d5220
|
||||
* cos(xomi + xli - g52)) + d5232 * cos(-xomi + xli - g52) + (2
|
||||
* (d4410 * cos(x2omi + x2li - g44) + d4422 * cos(x2li - g44) + (d5421
|
||||
* cos(xomi + x2li - g54)) + d5433 * cos(-xomi + x2li - g54)))
|
||||
}
|
||||
xldot = xni + xfact
|
||||
xnddt *= xldot
|
||||
if (doLoop) {
|
||||
xli += xldot * delt + xndot * step2
|
||||
xni += xndot * delt + xnddt * step2
|
||||
atime += delt
|
||||
}
|
||||
} while (doLoop && !epochRestart)
|
||||
}
|
||||
|
||||
// Apply periodics with Lyddane modification
|
||||
private fun applyPeriodics() {
|
||||
sinok = sin(dsv.xnode)
|
||||
cosok = cos(dsv.xnode)
|
||||
alfdp = sinis * sinok
|
||||
betdp = sinis * cosok
|
||||
dalf = ph * cosok + pinc * cosis * sinok
|
||||
dbet = -ph * sinok + pinc * cosis * cosok
|
||||
alfdp += dalf
|
||||
betdp += dbet
|
||||
dsv.xnode = mod2PI(dsv.xnode)
|
||||
xls = dsv.xll + dsv.omgadf + cosis * dsv.xnode
|
||||
dls = pl + pgh - pinc * dsv.xnode * sinis
|
||||
xls += dls
|
||||
xnoh = dsv.xnode
|
||||
dsv.xnode = atan2(alfdp, betdp)
|
||||
}
|
||||
|
||||
// Calculates the Greenwich Mean Sidereal Time for an epoch, valid 1957 through 2056
|
||||
private fun thetaG(epoch: Double): Double {
|
||||
var year = floor(epoch * 1E-3)
|
||||
var dayOfYear = (epoch * 1E-3 - year) * 1000.0
|
||||
year = if (year < 57) year + 2000 else year + 1900
|
||||
val dayFloor = floor(dayOfYear)
|
||||
val dayFraction = dayOfYear - dayFloor
|
||||
dayOfYear = dayFloor
|
||||
val jd = julianDateOfYear(year) + dayOfYear
|
||||
dsv.ds50 = jd - 2433281.5 + dayFraction
|
||||
return mod2PI(6.3003880987 * dsv.ds50 + 1.72944494)
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,26 @@
|
||||
/*
|
||||
* Look4Sat. Amateur radio satellite tracker and pass predictor.
|
||||
* Copyright (C) 2019-2026 Arty Bishop and contributors.
|
||||
*
|
||||
* 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.core.domain.predict
|
||||
|
||||
data class GeoPos(
|
||||
val latitude: Double,
|
||||
val longitude: Double,
|
||||
val altitude: Double = 0.0,
|
||||
val qthLocator: String = "null",
|
||||
val timestamp: Long = 0L
|
||||
)
|
||||
+226
@@ -0,0 +1,226 @@
|
||||
/*
|
||||
* Look4Sat. Amateur radio satellite tracker and pass predictor.
|
||||
* Copyright (C) 2019-2026 Arty Bishop and contributors.
|
||||
*
|
||||
* 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.core.domain.predict
|
||||
|
||||
import kotlin.math.abs
|
||||
import kotlin.math.atan2
|
||||
import kotlin.math.cos
|
||||
import kotlin.math.pow
|
||||
import kotlin.math.sin
|
||||
import kotlin.math.sqrt
|
||||
|
||||
class NearEarthObject(data: OrbitalData) : OrbitalObject(data) {
|
||||
|
||||
private val aodp: Double
|
||||
private val aycof: Double
|
||||
private val c1: Double
|
||||
private val c4: Double
|
||||
private val c5: Double
|
||||
private val cosio: Double
|
||||
private var d2 = 0.0
|
||||
private var d3 = 0.0
|
||||
private var d4 = 0.0
|
||||
private val delmo: Double
|
||||
private val omgcof: Double
|
||||
private val eta: Double
|
||||
private val omgdot: Double
|
||||
private val sinio: Double
|
||||
private val xnodp: Double
|
||||
private val sinmo: Double
|
||||
private val t2cof: Double
|
||||
private var t3cof = 0.0
|
||||
private var t4cof = 0.0
|
||||
private var t5cof = 0.0
|
||||
private val x1mth2: Double
|
||||
private val x3thm1: Double
|
||||
private val x7thm1: Double
|
||||
private val xmcof: Double
|
||||
private val xmdot: Double
|
||||
private val xnodcf: Double
|
||||
private val xnodot: Double
|
||||
private val xlcof: Double
|
||||
private val sgp4Simple: Boolean
|
||||
|
||||
init {
|
||||
// Recover original mean motion (xnodp) and semimajor axis (aodp) from input elements
|
||||
val a1 = (XKE / data.xno).pow(TWO_THIRDS)
|
||||
cosio = cos(data.xincl)
|
||||
val theta2 = sqr(cosio)
|
||||
x3thm1 = 3.0 * theta2 - 1.0
|
||||
val eo = data.eccn
|
||||
val eosq = sqr(eo)
|
||||
val betao2 = 1.0 - eosq
|
||||
val betao = sqrt(betao2)
|
||||
val del1 = 1.5 * CK2 * x3thm1 / (sqr(a1) * betao * betao2)
|
||||
val ao = a1 * (1.0 - del1 * (0.5 * TWO_THIRDS + del1 * (1.0 + 134.0 / 81.0 * del1)))
|
||||
val delo = 1.5 * CK2 * x3thm1 / (sqr(ao) * betao * betao2)
|
||||
xnodp = data.xno / (1.0 + delo)
|
||||
aodp = ao / (1.0 - delo)
|
||||
|
||||
// For perigee less than 220 kilometers, the "simple" flag is set
|
||||
sgp4Simple = aodp * (1.0 - eo) < 220 / EARTH_RADIUS + 1.0
|
||||
|
||||
// For perigees below 156 km, the values of S and QOMS2T are altered
|
||||
setPerigee((aodp * (1.0 - eo) - 1.0) * EARTH_RADIUS)
|
||||
val pinvsq = invert(sqr(aodp) * sqr(betao2))
|
||||
val tsi = invert(aodp - s4)
|
||||
eta = aodp * eo * tsi
|
||||
val etasq = eta * eta
|
||||
val eeta = eo * eta
|
||||
val psisq = abs(1.0 - etasq)
|
||||
val coef = qoms24 * tsi.pow(4.0)
|
||||
val coef1 = coef / psisq.pow(3.5)
|
||||
val bstar = data.bstar
|
||||
val c2 = coef1 * xnodp * (aodp * (1.0 + 1.5 * etasq + eeta * (4.0 + etasq)) + 0.75
|
||||
* CK2 * tsi / psisq * x3thm1 * (8.0 + 3.0 * etasq * (8.0 + etasq)))
|
||||
c1 = bstar * c2
|
||||
sinio = sin(data.xincl)
|
||||
val a3ovk2 = -J3_HARMONIC / CK2
|
||||
val c3 = coef * tsi * a3ovk2 * xnodp * sinio / eo
|
||||
x1mth2 = 1.0 - theta2
|
||||
val omegao = data.omegao
|
||||
c4 = 2 * xnodp * coef1 * aodp * betao2 * (eta * (2.0 + 0.5 * etasq) + eo * (0.5 + 2 * etasq)
|
||||
- 2 * CK2 * tsi / (aodp * psisq) * (-3 * x3thm1 * (1.0 - 2 * eeta + etasq
|
||||
* (1.5 - 0.5 * eeta)) + 0.75 * x1mth2 * (2.0 * etasq - eeta * (1.0 + etasq))
|
||||
* cos(2.0 * omegao)))
|
||||
c5 = 2.0 * coef1 * aodp * betao2 * (1.0 + 2.75 * (etasq + eeta) + eeta * etasq)
|
||||
val theta4 = sqr(theta2)
|
||||
val temp1 = 3.0 * CK2 * pinvsq * xnodp
|
||||
val temp2 = temp1 * CK2 * pinvsq
|
||||
val temp3 = 1.25 * CK4 * pinvsq * pinvsq * xnodp
|
||||
xmdot =
|
||||
xnodp + 0.5 * temp1 * betao * x3thm1 + (0.0625 * temp2 * betao * (13.0 - 78.0 * theta2 + 137.0 * theta4))
|
||||
val x1m5th = 1.0 - 5.0 * theta2
|
||||
omgdot =
|
||||
-0.5 * temp1 * x1m5th + 0.0625 * temp2 * (7.0 - 114.0 * theta2 + 395.0 * theta4) + temp3 * (3.0 - 36.0 * theta2 + 49.0 * theta4)
|
||||
val xhdot1 = -temp1 * cosio
|
||||
xnodot =
|
||||
xhdot1 + (0.5 * temp2 * (4.0 - 19.0 * theta2) + 2.0 * temp3 * (3.0 - 7.0 * theta2)) * cosio
|
||||
omgcof = bstar * c3 * cos(omegao)
|
||||
xmcof = -TWO_THIRDS * coef * bstar / eeta
|
||||
xnodcf = 3.5 * betao2 * xhdot1 * c1
|
||||
t2cof = 1.5 * c1
|
||||
xlcof = 0.125 * a3ovk2 * sinio * (3.0 + 5 * cosio) / (1.0 + cosio)
|
||||
aycof = 0.25 * a3ovk2 * sinio
|
||||
val xmo = data.xmo
|
||||
delmo = (1.0 + eta * cos(xmo)).pow(3.0)
|
||||
sinmo = sin(xmo)
|
||||
x7thm1 = 7.0 * theta2 - 1
|
||||
if (!sgp4Simple) {
|
||||
val c1sq = sqr(c1)
|
||||
d2 = 4.0 * aodp * tsi * c1sq
|
||||
val temp = d2 * tsi * c1 / 3.0
|
||||
d3 = (17 * aodp + s4) * temp
|
||||
d4 = 0.5 * temp * aodp * tsi * (221 * aodp + 31 * s4) * c1
|
||||
t3cof = d2 + 2 * c1sq
|
||||
t4cof = 0.25 * (3.0 * d3 + c1 * (12 * d2 + 10 * c1sq))
|
||||
t5cof = 0.2 * (3.0 * d4 + 12 * c1 * d3 + 6 * d2 * d2 + 15 * c1sq * (2.0 * d2 + c1sq))
|
||||
} else {
|
||||
d2 = 0.0
|
||||
d3 = 0.0
|
||||
d4 = 0.0
|
||||
t3cof = 0.0
|
||||
t4cof = 0.0
|
||||
t5cof = 0.0
|
||||
}
|
||||
}
|
||||
|
||||
internal fun calculateSGP4(tSince: Double) {
|
||||
synchronized(this) {
|
||||
val temp = DoubleArray(9)
|
||||
val xmdf = data.xmo + xmdot * tSince
|
||||
val omgadf = data.omegao + omgdot * tSince
|
||||
val xnoddf = data.xnodeo + xnodot * tSince
|
||||
var omega = omgadf
|
||||
var xmp = xmdf
|
||||
val tsq = sqr(tSince)
|
||||
val xnode = xnoddf + xnodcf * tsq
|
||||
val bstar = data.bstar
|
||||
var tempa = 1.0 - c1 * tSince
|
||||
var tempe = bstar * c4 * tSince
|
||||
var templ = t2cof * tsq
|
||||
if (!sgp4Simple) {
|
||||
val delomg = omgcof * tSince
|
||||
val delm = xmcof * ((1.0 + eta * cos(xmdf)).pow(3.0) - delmo)
|
||||
temp[0] = delomg + delm
|
||||
xmp = xmdf + temp[0]
|
||||
omega = omgadf - temp[0]
|
||||
val tcube = tsq * tSince
|
||||
val tfour = tSince * tcube
|
||||
tempa = tempa - d2 * tsq - d3 * tcube - d4 * tfour
|
||||
tempe += bstar * c5 * (sin(xmp) - sinmo)
|
||||
templ += t3cof * tcube + tfour * (t4cof + tSince * t5cof)
|
||||
}
|
||||
val a = aodp * tempa.pow(2.0)
|
||||
val eo = data.eccn
|
||||
val e = eo - tempe
|
||||
val xl = xmp + omega + xnode + xnodp * templ
|
||||
val beta = sqrt(1.0 - e * e)
|
||||
val xn = XKE / a.pow(1.5)
|
||||
|
||||
// Long period periodics
|
||||
val axn = e * cos(omega)
|
||||
temp[0] = invert(a * sqr(beta))
|
||||
val xll = temp[0] * xlcof * axn
|
||||
val aynl = temp[0] * aycof
|
||||
val xlt = xl + xll
|
||||
val ayn = e * sin(omega) + aynl
|
||||
|
||||
// Solve Kepler's equation
|
||||
val capu = mod2PI(xlt - xnode)
|
||||
temp[2] = capu
|
||||
converge(temp, axn, ayn, capu)
|
||||
calculatePosAndVel(temp, xnode, a, xn, axn, ayn)
|
||||
calculatePhase(xlt, xnode, omgadf)
|
||||
}
|
||||
}
|
||||
|
||||
private fun calculatePosAndVel(
|
||||
temp: DoubleArray, xnode: Double, a: Double,
|
||||
xn: Double, axn: Double, ayn: Double
|
||||
) {
|
||||
val ecose = temp[5] + temp[6]
|
||||
val esine = temp[3] - temp[4]
|
||||
val elsq = sqr(axn) + sqr(ayn)
|
||||
temp[0] = 1.0 - elsq
|
||||
val pl = a * temp[0]
|
||||
val r = a * (1.0 - ecose)
|
||||
temp[1] = invert(r)
|
||||
val rdot = XKE * sqrt(a) * esine * temp[1]
|
||||
val rfdot = XKE * sqrt(pl) * temp[1]
|
||||
temp[2] = a * temp[1]
|
||||
val betal = sqrt(temp[0])
|
||||
temp[3] = invert(1.0 + betal)
|
||||
val cosu = temp[2] * (temp[8] - axn + ayn * esine * temp[3])
|
||||
val sinu = temp[2] * (temp[7] - ayn - axn * esine * temp[3])
|
||||
val u = atan2(sinu, cosu)
|
||||
val sin2u = 2.0 * sinu * cosu
|
||||
val cos2u = 2.0 * cosu * cosu - 1
|
||||
temp[0] = invert(pl)
|
||||
temp[1] = CK2 * temp[0]
|
||||
temp[2] = temp[1] * temp[0]
|
||||
// Update for short periodics
|
||||
val rk = r * (1.0 - 1.5 * temp[2] * betal * x3thm1) + 0.5 * temp[1] * x1mth2 * cos2u
|
||||
val uk = u - 0.25 * temp[2] * x7thm1 * sin2u
|
||||
val xnodek = xnode + 1.5 * temp[2] * cosio * sin2u
|
||||
val xinck = data.xincl + 1.5 * temp[2] * cosio * sinio * cos2u
|
||||
val rdotk = rdot - xn * temp[1] * x1mth2 * sin2u
|
||||
val rfdotk = rfdot + xn * temp[1] * (x1mth2 * cos2u + 1.5 * x3thm1)
|
||||
super.calculatePosAndVel(rk, uk, xnodek, xinck, rdotk, rfdotk)
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,40 @@
|
||||
/*
|
||||
* Look4Sat. Amateur radio satellite tracker and pass predictor.
|
||||
* Copyright (C) 2019-2026 Arty Bishop and contributors.
|
||||
*
|
||||
* 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.core.domain.predict
|
||||
|
||||
data class OrbitalData(
|
||||
val name: String,
|
||||
val epoch: Double,
|
||||
val meanmo: Double,
|
||||
val eccn: Double,
|
||||
val incl: Double,
|
||||
val raan: Double,
|
||||
val argper: Double,
|
||||
val meanan: Double,
|
||||
val catnum: Int,
|
||||
val bstar: Double
|
||||
) {
|
||||
val xincl: Double = incl * DEG2RAD
|
||||
val xnodeo: Double = raan * DEG2RAD
|
||||
val omegao: Double = argper * DEG2RAD
|
||||
val xmo: Double = meanan * DEG2RAD
|
||||
val xno: Double = meanmo * TWO_PI / MIN_PER_DAY
|
||||
val orbitalPeriod: Double = MIN_PER_DAY / meanmo
|
||||
val isDeepSpace: Boolean = orbitalPeriod >= 225.0 // NearEarth (period < 225 min) or DeepSpace (period >= 225 min)
|
||||
fun getObject(): OrbitalObject = if (isDeepSpace) DeepSpaceObject(this) else NearEarthObject(this)
|
||||
}
|
||||
@@ -0,0 +1,423 @@
|
||||
/*
|
||||
* Look4Sat. Amateur radio satellite tracker and pass predictor.
|
||||
* Copyright (C) 2019-2026 Arty Bishop and contributors.
|
||||
*
|
||||
* 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.core.domain.predict
|
||||
|
||||
import kotlin.math.abs
|
||||
import kotlin.math.acos
|
||||
import kotlin.math.asin
|
||||
import kotlin.math.atan
|
||||
import kotlin.math.atan2
|
||||
import kotlin.math.cos
|
||||
import kotlin.math.exp
|
||||
import kotlin.math.floor
|
||||
import kotlin.math.ln
|
||||
import kotlin.math.pow
|
||||
import kotlin.math.sin
|
||||
import kotlin.math.sqrt
|
||||
|
||||
abstract class OrbitalObject(val data: OrbitalData) {
|
||||
|
||||
private val position = Vector4()
|
||||
private val velocity = Vector4()
|
||||
private var orbitalPos = OrbitalPos()
|
||||
private var eclipseDepth = 0.0
|
||||
private var gsPosTheta = 0.0
|
||||
private var julUTC = 0.0
|
||||
private var perigee = 0.0
|
||||
var qoms24 = 0.0
|
||||
var s4 = 0.0
|
||||
|
||||
fun willBeSeen(pos: GeoPos): Boolean {
|
||||
return if (data.meanmo < 1e-8) false
|
||||
else {
|
||||
val sma = 331.25 * exp(ln(MIN_PER_DAY / data.meanmo) * (2.0 / 3.0))
|
||||
val apogee = sma * (1.0 + data.eccn) - EARTH_RADIUS
|
||||
var lin = data.incl
|
||||
if (lin >= 90.0) lin = 180.0 - lin
|
||||
acos(EARTH_RADIUS / (apogee + EARTH_RADIUS)) + lin * DEG2RAD > abs(pos.latitude * DEG2RAD)
|
||||
}
|
||||
}
|
||||
|
||||
fun getPosition(pos: GeoPos, time: Long): OrbitalPos {
|
||||
orbitalPos = OrbitalPos()
|
||||
// Date/time at which the position and velocity were calculated
|
||||
julUTC = calcCurrentDaynum(time) + 2444238.5
|
||||
// Convert satellite's epoch time to Julian and calculate time since epoch in minutes
|
||||
val julEpoch = juliandDateOfEpoch(data.epoch)
|
||||
val tsince = (julUTC - julEpoch) * MIN_PER_DAY
|
||||
calculateSDP4orSGP4(tsince)
|
||||
// Scale position and velocity vectors to km and km/sec
|
||||
convertSatState(position, velocity)
|
||||
// Calculate velocity of satellite
|
||||
magnitude(velocity)
|
||||
val squintVector = Vector4()
|
||||
// Angles in rads, dist in km, vel in km/S. Calculate sat Az, El, Range and Range-rate.
|
||||
calculateObs(julUTC, position, velocity, pos, squintVector)
|
||||
calculateLatLonAlt(julUTC)
|
||||
orbitalPos.time = time
|
||||
orbitalPos.eclipsed = isEclipsed()
|
||||
orbitalPos.eclipseDepth = eclipseDepth
|
||||
return orbitalPos
|
||||
}
|
||||
|
||||
private fun calcCurrentDaynum(now: Long): Double {
|
||||
val then = 315446400000 // time in millis on 31Dec79 00:00:00 UTC (daynum 0)
|
||||
return (now - then) / 1000.0 / 60.0 / 60.0 / 24.0
|
||||
}
|
||||
|
||||
private fun juliandDateOfEpoch(epoch: Double): Double {
|
||||
var year = floor(epoch * 1E-3)
|
||||
val day = (epoch * 1E-3 - year) * 1000.0
|
||||
year = if (year < 57) year + 2000 else year + 1900
|
||||
return julianDateOfYear(year) + day
|
||||
}
|
||||
|
||||
internal fun julianDateOfYear(theYear: Double): Double {
|
||||
val aYear = theYear - 1
|
||||
var i = floor(aYear / 100).toLong()
|
||||
val a = i
|
||||
i = a / 4
|
||||
val b = 2 - a + i
|
||||
i = floor(365.25 * aYear).toLong()
|
||||
i += (30.6001 * 14).toLong()
|
||||
return i + 1720994.5 + b
|
||||
}
|
||||
|
||||
private fun calculateSDP4orSGP4(tsince: Double) {
|
||||
if (data.isDeepSpace) (this as DeepSpaceObject).calculateSDP4(tsince)
|
||||
else (this as NearEarthObject).calculateSGP4(tsince)
|
||||
}
|
||||
|
||||
// Converts the sat position and velocity vectors to km and km/sec
|
||||
private fun convertSatState(pos: Vector4, vel: Vector4) {
|
||||
scaleVector(EARTH_RADIUS, pos)
|
||||
scaleVector(EARTH_RADIUS * MIN_PER_DAY / SEC_PER_DAY, vel)
|
||||
}
|
||||
|
||||
// Calculates the topocentric coordinates of the object with ECI pos and vel at time
|
||||
private fun calculateObs(
|
||||
julianUTC: Double,
|
||||
positionVector: Vector4,
|
||||
velocityVector: Vector4,
|
||||
gsPos: GeoPos,
|
||||
squintVector: Vector4
|
||||
) {
|
||||
val obsPos = Vector4()
|
||||
val obsVel = Vector4()
|
||||
val range = Vector4()
|
||||
val rgvel = Vector4()
|
||||
calculateUserPosVel(julianUTC, gsPos, obsPos, obsVel)
|
||||
range.setXYZ(
|
||||
positionVector.x - obsPos.x,
|
||||
positionVector.y - obsPos.y,
|
||||
positionVector.z - obsPos.z
|
||||
)
|
||||
// Save these values globally for calculating squint angles later
|
||||
squintVector.setXYZ(range.x, range.y, range.z)
|
||||
rgvel.setXYZ(
|
||||
velocityVector.x - obsVel.x,
|
||||
velocityVector.y - obsVel.y,
|
||||
velocityVector.z - obsVel.z
|
||||
)
|
||||
magnitude(range)
|
||||
val sinLat = sin(DEG2RAD * gsPos.latitude)
|
||||
val cosLat = cos(DEG2RAD * gsPos.latitude)
|
||||
val sinTheta = sin(gsPosTheta)
|
||||
val cosTheta = cos(gsPosTheta)
|
||||
val topS = sinLat * cosTheta * range.x + sinLat * sinTheta * range.y - cosLat * range.z
|
||||
val topE = -sinTheta * range.x + cosTheta * range.y
|
||||
val topZ = cosLat * cosTheta * range.x + cosLat * sinTheta * range.y + sinLat * range.z
|
||||
var azim = atan(-topE / topS)
|
||||
if (topS > 0.0) azim += PI
|
||||
if (azim < 0.0) azim += TWO_PI
|
||||
orbitalPos.azimuth = azim
|
||||
orbitalPos.elevation = asin(topZ / range.w)
|
||||
orbitalPos.distance = range.w
|
||||
orbitalPos.distanceRate = dot(range, rgvel) / range.w
|
||||
var elevation = orbitalPos.elevation / TWO_PI * 360.0
|
||||
if (elevation > 90) elevation = 180 - elevation
|
||||
orbitalPos.aboveHorizon = elevation - 0 > EPSILON
|
||||
}
|
||||
|
||||
// Returns the ECI position and velocity of the observer
|
||||
private fun calculateUserPosVel(
|
||||
time: Double,
|
||||
gsPos: GeoPos,
|
||||
obsPos: Vector4,
|
||||
obsVel: Vector4
|
||||
) {
|
||||
val mFactor = 7.292115E-5
|
||||
gsPosTheta = mod2PI(thetaGJD(time) + DEG2RAD * gsPos.longitude)
|
||||
val c = invert(sqrt(1.0 + FLAT_FACT * (FLAT_FACT - 2) * sqr(sin(DEG2RAD * gsPos.latitude))))
|
||||
val sq = sqr(1.0 - FLAT_FACT) * c
|
||||
val achcp = (EARTH_RADIUS * c + gsPos.altitude / 1000.0) * cos(DEG2RAD * gsPos.latitude)
|
||||
obsPos.setXYZ(
|
||||
achcp * cos(gsPosTheta), achcp * sin(gsPosTheta),
|
||||
(EARTH_RADIUS * sq + gsPos.altitude / 1000.0) * sin(DEG2RAD * gsPos.latitude)
|
||||
)
|
||||
obsVel.setXYZ(-mFactor * obsPos.y, mFactor * obsPos.x, 0.0)
|
||||
magnitude(obsPos)
|
||||
magnitude(obsVel)
|
||||
}
|
||||
|
||||
// Calculate the geodetic position of an object given its ECI pos and time
|
||||
private fun calculateLatLonAlt(time: Double) {
|
||||
orbitalPos.theta = atan2(position.y, position.x)
|
||||
orbitalPos.longitude = mod2PI(orbitalPos.theta - thetaGJD(time))
|
||||
val r = sqrt(sqr(position.x) + sqr(position.y))
|
||||
val e2 = FLAT_FACT * (2.0 - FLAT_FACT)
|
||||
orbitalPos.latitude = atan2(position.z, r)
|
||||
var phi: Double
|
||||
var c: Double
|
||||
var i = 0
|
||||
var converged: Boolean
|
||||
do {
|
||||
phi = orbitalPos.latitude
|
||||
c = invert(sqrt(1.0 - e2 * sqr(sin(phi))))
|
||||
orbitalPos.latitude = atan2(position.z + EARTH_RADIUS * c * e2 * sin(phi), r)
|
||||
converged = abs(orbitalPos.latitude - phi) < EPSILON
|
||||
} while (i++ < 10 && !converged)
|
||||
orbitalPos.altitude = r / cos(orbitalPos.latitude) - EARTH_RADIUS * c
|
||||
var temp = orbitalPos.latitude
|
||||
if (temp > PI_2) {
|
||||
temp -= TWO_PI
|
||||
orbitalPos.latitude = temp
|
||||
}
|
||||
}
|
||||
|
||||
internal fun calculatePosAndVel(
|
||||
rk: Double, uk: Double, xnodek: Double,
|
||||
xinck: Double, rdotk: Double, rfdotk: Double
|
||||
) {
|
||||
// Orientation vectors
|
||||
val sinuk = sin(uk)
|
||||
val cosuk = cos(uk)
|
||||
val sinik = sin(xinck)
|
||||
val cosik = cos(xinck)
|
||||
val sinnok = sin(xnodek)
|
||||
val cosnok = cos(xnodek)
|
||||
val xmx = -sinnok * cosik
|
||||
val xmy = cosnok * cosik
|
||||
val ux = xmx * sinuk + cosnok * cosuk
|
||||
val uy = xmy * sinuk + sinnok * cosuk
|
||||
val uz = sinik * sinuk
|
||||
val vx = xmx * cosuk - cosnok * sinuk
|
||||
val vy = xmy * cosuk - sinnok * sinuk
|
||||
val vz = sinik * cosuk
|
||||
// Position and velocity
|
||||
position.setXYZ(ux, uy, uz)
|
||||
position.multiply(rk)
|
||||
velocity.x = rdotk * ux + rfdotk * vx
|
||||
velocity.y = rdotk * uy + rfdotk * vy
|
||||
velocity.z = rdotk * uz + rfdotk * vz
|
||||
}
|
||||
|
||||
internal class Vector4(
|
||||
var w: Double = 0.0,
|
||||
var x: Double = 0.0,
|
||||
var y: Double = 0.0,
|
||||
var z: Double = 0.0
|
||||
) {
|
||||
|
||||
fun multiply(multiplier: Double) {
|
||||
x *= multiplier
|
||||
y *= multiplier
|
||||
z *= multiplier
|
||||
}
|
||||
|
||||
fun setXYZ(xValue: Double, yValue: Double, zValue: Double) {
|
||||
x = xValue
|
||||
y = yValue
|
||||
z = zValue
|
||||
}
|
||||
}
|
||||
|
||||
internal fun sqr(arg: Double): Double {
|
||||
return arg * arg
|
||||
}
|
||||
|
||||
internal fun invert(value: Double): Double {
|
||||
return 1.0 / value
|
||||
}
|
||||
|
||||
// Calculates the modulus of 2 * PI
|
||||
internal fun mod2PI(value: Double): Double {
|
||||
var retVal = value
|
||||
val i = (retVal / TWO_PI).toInt()
|
||||
retVal -= i * TWO_PI
|
||||
if (retVal < 0.0) retVal += TWO_PI
|
||||
return retVal
|
||||
}
|
||||
|
||||
// Solves Keplers' Equation
|
||||
internal fun converge(temp: DoubleArray, axn: Double, ayn: Double, capu: Double) {
|
||||
var converged = false
|
||||
var i = 0
|
||||
do {
|
||||
temp[7] = sin(temp[2])
|
||||
temp[8] = cos(temp[2])
|
||||
temp[3] = axn * temp[7]
|
||||
temp[4] = ayn * temp[8]
|
||||
temp[5] = axn * temp[8]
|
||||
temp[6] = ayn * temp[7]
|
||||
val epw = (capu - temp[4] + temp[3] - temp[2]) / (1.0 - temp[5] - temp[6]) + temp[2]
|
||||
if (abs(epw - temp[2]) <= EPSILON) converged = true else temp[2] = epw
|
||||
} while (i++ < 10 && !converged)
|
||||
}
|
||||
|
||||
internal fun calculatePhase(xlt: Double, xnode: Double, omgadf: Double) {
|
||||
var phaseValue = xlt - xnode - omgadf + TWO_PI
|
||||
if (phaseValue < 0.0) phaseValue += TWO_PI
|
||||
orbitalPos.phase = mod2PI(phaseValue)
|
||||
}
|
||||
|
||||
// Sets perigee and checks and adjusts the calculation if the perigee is less tan 156KM
|
||||
internal fun setPerigee(perigee: Double) {
|
||||
this.perigee = perigee
|
||||
checkPerigee()
|
||||
}
|
||||
|
||||
// Checks and adjusts the calculation if the perigee is less tan 156KM
|
||||
private fun checkPerigee() {
|
||||
s4 = 1.012229
|
||||
qoms24 = 1.880279E-09
|
||||
if (perigee < 156.0) {
|
||||
s4 = if (perigee <= 98.0) 20.0 else perigee - 78.0
|
||||
qoms24 = ((120 - s4) / EARTH_RADIUS).pow(4.0)
|
||||
s4 = s4 / EARTH_RADIUS + 1.0
|
||||
}
|
||||
}
|
||||
|
||||
// Checks if the satellite is in sunlight
|
||||
private fun isEclipsed(): Boolean {
|
||||
val sunVector = calculateSunVector()
|
||||
val sdEarth = asin(EARTH_RADIUS / position.w)
|
||||
val rho = subtract(sunVector, position)
|
||||
val sdSun = asin(SOLAR_RADIUS / rho.w)
|
||||
val earth = scalarNegMultiply(position)
|
||||
val delta = angle(sunVector, earth)
|
||||
eclipseDepth = sdEarth - sdSun - delta
|
||||
return if (sdEarth < sdSun) false else eclipseDepth >= 0
|
||||
}
|
||||
|
||||
private fun calculateSunVector(): Vector4 {
|
||||
val mjd = julUTC - 2415020.0
|
||||
val year = 1900 + mjd / 365.25
|
||||
val solTime = (mjd + deltaEt(year) / SEC_PER_DAY) / 36525.0
|
||||
val mTemp = modulus(35999.04975 * solTime, 360.0)
|
||||
val m = radians(
|
||||
modulus(358.47583 + mTemp - (0.000150 + 0.0000033 * solTime) * sqr(solTime), 360.0)
|
||||
)
|
||||
val lTemp = modulus(36000.76892 * solTime, 360.0)
|
||||
val l = radians(
|
||||
modulus(279.69668 + lTemp + 0.0003025 * sqr(solTime), 360.0)
|
||||
)
|
||||
val e = 0.01675104 - (0.0000418 + 0.000000126 * solTime) * solTime
|
||||
val c = radians(
|
||||
((1.919460 - (0.004789 + 0.000014 * solTime) * solTime) * sin(m))
|
||||
+ ((0.020094 - 0.000100 * solTime) * sin(2 * m)) + 0.000293 * sin(3 * m)
|
||||
)
|
||||
val o = radians(modulus(259.18 - 1934.142 * solTime, 360.0))
|
||||
val lsa = modulus(l + c - radians(0.00569 - 0.00479 * sin(o)), TWO_PI)
|
||||
val nu = modulus(m + c, TWO_PI)
|
||||
var r = (1.0000002 * (1.0 - sqr(e)) / (1.0 + e * cos(nu)))
|
||||
val eps = radians(
|
||||
23.452294 - (0.0130125 + (0.00000164 - 0.000000503 * solTime) * solTime)
|
||||
* solTime + 0.00256 * cos(o)
|
||||
)
|
||||
r *= ASTRONOMICAL_UNIT
|
||||
return Vector4(r, r * cos(lsa), r * sin(lsa) * cos(eps), r * sin(lsa) * sin(eps))
|
||||
}
|
||||
|
||||
private fun subtract(v1: Vector4, v2: Vector4): Vector4 {
|
||||
val v3 = Vector4()
|
||||
v3.x = v1.x - v2.x
|
||||
v3.y = v1.y - v2.y
|
||||
v3.z = v1.z - v2.z
|
||||
magnitude(v3)
|
||||
return v3
|
||||
}
|
||||
|
||||
private fun scalarNegMultiply(vector: Vector4): Vector4 {
|
||||
val neg = -1.0
|
||||
return Vector4(vector.w * abs(neg), vector.x * neg, vector.y * neg, vector.z * neg)
|
||||
}
|
||||
|
||||
private fun angle(v1: Vector4, v2: Vector4): Double {
|
||||
magnitude(v1)
|
||||
magnitude(v2)
|
||||
return acos(dot(v1, v2) / (v1.w * v2.w))
|
||||
}
|
||||
|
||||
/**
|
||||
* The function Delta_ET has been added to allow calculations on the
|
||||
* position of the sun. It provides the difference between UT (approximately
|
||||
* the same as UTC) and ET (now referred to as TDT) This function is based
|
||||
* on a least squares fit of data from 1950 to 1991 and will need to be
|
||||
* updated periodically.
|
||||
*
|
||||
* Values determined using data from 1950-1991 in the 1990 Astronomical
|
||||
* Almanac. See DELTA_ET.WQ1 for details.
|
||||
*/
|
||||
private fun deltaEt(year: Double): Double {
|
||||
return 26.465 + 0.747622 * (year - 1950) + (1.886913 * sin(TWO_PI * (year - 1975) / 33))
|
||||
}
|
||||
|
||||
private fun radians(degrees: Double): Double {
|
||||
return degrees * DEG2RAD
|
||||
}
|
||||
|
||||
// Calculates the dot product of two vectors
|
||||
private fun dot(v1: Vector4, v2: Vector4): Double {
|
||||
return v1.x * v2.x + v1.y * v2.y + v1.z * v2.z
|
||||
}
|
||||
|
||||
// Returns fractional part of double argument
|
||||
private fun fraction(arg: Double): Double {
|
||||
return arg - floor(arg)
|
||||
}
|
||||
|
||||
// Calculates scalar magnitude of a vector4 argument
|
||||
private fun magnitude(v: Vector4) {
|
||||
v.w = sqrt(sqr(v.x) + sqr(v.y) + sqr(v.z))
|
||||
}
|
||||
|
||||
private fun modulus(arg1: Double, arg2: Double = SEC_PER_DAY): Double {
|
||||
var returnValue = arg1
|
||||
val i = floor(returnValue / arg2).toInt()
|
||||
returnValue -= i * arg2
|
||||
if (returnValue < 0.0) returnValue += arg2
|
||||
return returnValue
|
||||
}
|
||||
|
||||
// Multiplies the vector v1 by the scalar k
|
||||
private fun scaleVector(k: Double, v: Vector4) {
|
||||
v.multiply(k)
|
||||
magnitude(v)
|
||||
}
|
||||
|
||||
private fun thetaGJD(theJD: Double): Double {
|
||||
val earthRotPerSidDay = 1.00273790934
|
||||
val ut = fraction(theJD + 0.5)
|
||||
val aJD = theJD - ut
|
||||
val tu = (aJD - 2451545.0) / 36525.0
|
||||
var gmst = 24110.54841 + tu * (8640184.812866 + tu * (0.093104 - tu * 6.2E-6))
|
||||
gmst = modulus(gmst + SEC_PER_DAY * earthRotPerSidDay * ut)
|
||||
return TWO_PI * gmst / SEC_PER_DAY
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,33 @@
|
||||
/*
|
||||
* Look4Sat. Amateur radio satellite tracker and pass predictor.
|
||||
* Copyright (C) 2019-2026 Arty Bishop and contributors.
|
||||
*
|
||||
* 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.core.domain.predict
|
||||
|
||||
data class OrbitalPass(
|
||||
val aosTime: Long = 0L,
|
||||
val aosAzimuth: Double = 90.0,
|
||||
val losTime: Long = 0L,
|
||||
val losAzimuth: Double = 270.0,
|
||||
val altitude: Int = 1000,
|
||||
val maxElevation: Double = 75.0,
|
||||
val orbitalObject: OrbitalObject,
|
||||
var progress: Float = 0.0f
|
||||
) {
|
||||
val catNum: Int = orbitalObject.data.catnum
|
||||
val name: String = orbitalObject.data.name
|
||||
val isDeepSpace: Boolean = orbitalObject.data.isDeepSpace
|
||||
}
|
||||
@@ -0,0 +1,72 @@
|
||||
/*
|
||||
* Look4Sat. Amateur radio satellite tracker and pass predictor.
|
||||
* Copyright (C) 2019-2026 Arty Bishop and contributors.
|
||||
*
|
||||
* 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.core.domain.predict
|
||||
|
||||
import kotlin.math.acos
|
||||
import kotlin.math.asin
|
||||
import kotlin.math.atan2
|
||||
import kotlin.math.cos
|
||||
import kotlin.math.pow
|
||||
import kotlin.math.sin
|
||||
import kotlin.math.sqrt
|
||||
|
||||
data class OrbitalPos(
|
||||
var azimuth: Double = 0.0,
|
||||
var elevation: Double = 0.0,
|
||||
var latitude: Double = 0.0,
|
||||
var longitude: Double = 0.0,
|
||||
var altitude: Double = 0.0,
|
||||
var distance: Double = 0.0,
|
||||
var distanceRate: Double = 0.0,
|
||||
var theta: Double = 0.0,
|
||||
var time: Long = 0L,
|
||||
var phase: Double = 0.0,
|
||||
var eclipseDepth: Double = 0.0,
|
||||
var eclipsed: Boolean = false,
|
||||
var aboveHorizon: Boolean = false
|
||||
) {
|
||||
|
||||
fun getDownlinkFreq(freq: Long): Long {
|
||||
return (freq.toDouble() * (SPEED_OF_LIGHT - distanceRate * 1000.0) / SPEED_OF_LIGHT).toLong()
|
||||
}
|
||||
|
||||
fun getUplinkFreq(freq: Long): Long {
|
||||
return (freq.toDouble() * (SPEED_OF_LIGHT + distanceRate * 1000.0) / SPEED_OF_LIGHT).toLong()
|
||||
}
|
||||
|
||||
fun getOrbitalVelocity(): Double {
|
||||
val earthG = 6.674 * 10.0.pow(-11)
|
||||
val earthM = 5.98 * 10.0.pow(24)
|
||||
val radius = 6.37 * 10.0.pow(6) + altitude * 10.0.pow(3)
|
||||
return sqrt(earthG * earthM / radius) / 1000
|
||||
}
|
||||
|
||||
fun getRangeCircle(): List<GeoPos> {
|
||||
val rangeCirclePoints = mutableListOf<GeoPos>()
|
||||
val beta = acos(EARTH_RADIUS / (EARTH_RADIUS + altitude)) // * EARTH_RADIUS = radiusKm
|
||||
for (azimuth in 0..720) {
|
||||
val rads = azimuth * DEG2RAD
|
||||
val lat = asin(sin(latitude) * cos(beta) + (cos(latitude) * sin(beta) * cos(rads)))
|
||||
val lon = (longitude + atan2(
|
||||
sin(rads) * sin(beta) * cos(latitude), cos(beta) - sin(latitude) * sin(lat)
|
||||
))
|
||||
rangeCirclePoints.add(GeoPos(lat * RAD2DEG, lon * RAD2DEG))
|
||||
}
|
||||
return rangeCirclePoints
|
||||
}
|
||||
}
|
||||
+25
@@ -0,0 +1,25 @@
|
||||
/*
|
||||
* Look4Sat. Amateur radio satellite tracker and pass predictor.
|
||||
* Copyright (C) 2019-2026 Arty Bishop and contributors.
|
||||
*
|
||||
* 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.core.domain.repository
|
||||
|
||||
interface IDatabaseRepo {
|
||||
suspend fun updateTLEFromFile(uri: String)
|
||||
suspend fun updateTransceiversFromFile(uri: String)
|
||||
suspend fun updateFromRemote()
|
||||
suspend fun clearAllData()
|
||||
}
|
||||
+22
@@ -0,0 +1,22 @@
|
||||
package com.rtbishop.look4sat.core.domain.repository
|
||||
|
||||
import com.rtbishop.look4sat.core.domain.usecase.IAddToCalendar
|
||||
import com.rtbishop.look4sat.core.domain.usecase.IShowToast
|
||||
import kotlinx.coroutines.CoroutineScope
|
||||
|
||||
interface IMainContainer {
|
||||
val appScope: CoroutineScope
|
||||
val settingsRepo: ISettingsRepo
|
||||
val selectionRepo: ISelectionRepo
|
||||
val satelliteRepo: ISatelliteRepo
|
||||
val databaseRepo: IDatabaseRepo
|
||||
fun provideAddToCalendar(): IAddToCalendar
|
||||
fun provideShowToast(): IShowToast
|
||||
fun provideBluetoothReporter(): IReporterRepo<WithoutExtParams>
|
||||
fun provideNetworkReporter(): IReporterRepo<ExtendedParams>
|
||||
fun provideSensorsRepo(): ISensorsRepo
|
||||
}
|
||||
|
||||
interface IContainerProvider {
|
||||
fun getMainContainer(): IMainContainer
|
||||
}
|
||||
+34
@@ -0,0 +1,34 @@
|
||||
/*
|
||||
* Look4Sat. Amateur radio satellite tracker and pass predictor.
|
||||
* Copyright (C) 2019-2026 Arty Bishop and contributors.
|
||||
*
|
||||
* 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.core.domain.repository
|
||||
|
||||
interface IReporterParams
|
||||
|
||||
interface IReporterRepo<T : IReporterParams> {
|
||||
fun isConnected(service: BtService): Boolean
|
||||
fun isConnecting(service: BtService): Boolean
|
||||
fun connect(service: BtService, deviceId: String)
|
||||
fun reportRotation(format: String, azimuth: Double, elevation: Double, params: T)
|
||||
fun reportFrequency(format: String, frequency: Long, params: T)
|
||||
}
|
||||
|
||||
data class ExtendedParams(val server: String, val port: Int) : IReporterParams
|
||||
|
||||
data class WithoutExtParams(val dummy: Int) : IReporterParams
|
||||
|
||||
enum class BtService { ROTATOR, FREQUENCY }
|
||||
+37
@@ -0,0 +1,37 @@
|
||||
/*
|
||||
* Look4Sat. Amateur radio satellite tracker and pass predictor.
|
||||
* Copyright (C) 2019-2026 Arty Bishop and contributors.
|
||||
*
|
||||
* 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.core.domain.repository
|
||||
|
||||
import com.rtbishop.look4sat.core.domain.model.SatRadio
|
||||
import com.rtbishop.look4sat.core.domain.predict.GeoPos
|
||||
import com.rtbishop.look4sat.core.domain.predict.OrbitalObject
|
||||
import com.rtbishop.look4sat.core.domain.predict.OrbitalPass
|
||||
import com.rtbishop.look4sat.core.domain.predict.OrbitalPos
|
||||
import kotlinx.coroutines.flow.StateFlow
|
||||
|
||||
interface ISatelliteRepo {
|
||||
val passes: StateFlow<List<OrbitalPass>>
|
||||
val satellites: StateFlow<List<OrbitalObject>>
|
||||
suspend fun getRadiosWithId(id: Int): List<SatRadio>
|
||||
suspend fun initRepository()
|
||||
suspend fun getPosition(sat: OrbitalObject, pos: GeoPos, time: Long): OrbitalPos
|
||||
suspend fun getTrack(sat: OrbitalObject, pos: GeoPos, start: Long, end: Long): List<OrbitalPos>
|
||||
suspend fun getRadios(sat: OrbitalObject, pos: GeoPos, radios: List<SatRadio>, time: Long): List<SatRadio>
|
||||
suspend fun processPasses(passList: List<OrbitalPass>, time: Long): List<OrbitalPass>
|
||||
suspend fun calculatePasses(time: Long, hoursAhead: Int, minElevation: Double, modes: List<String>)
|
||||
}
|
||||
+32
@@ -0,0 +1,32 @@
|
||||
/*
|
||||
* Look4Sat. Amateur radio satellite tracker and pass predictor.
|
||||
* Copyright (C) 2019-2026 Arty Bishop and contributors.
|
||||
*
|
||||
* 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.core.domain.repository
|
||||
|
||||
import com.rtbishop.look4sat.core.domain.model.SatItem
|
||||
import kotlinx.coroutines.flow.Flow
|
||||
|
||||
interface ISelectionRepo {
|
||||
fun getCurrentTypes(): List<String>
|
||||
fun getTypesList(): List<String>
|
||||
suspend fun getEntriesFlow(): Flow<List<SatItem>>
|
||||
suspend fun setTypes(types: List<String>)
|
||||
suspend fun setQuery(query: String)
|
||||
suspend fun setSelection(selectAll: Boolean)
|
||||
suspend fun setSelection(ids: List<Int>, isTicked: Boolean)
|
||||
suspend fun saveSelection()
|
||||
}
|
||||
+28
@@ -0,0 +1,28 @@
|
||||
/*
|
||||
* Look4Sat. Amateur radio satellite tracker and pass predictor.
|
||||
* Copyright (C) 2019-2026 Arty Bishop and contributors.
|
||||
*
|
||||
* 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.core.domain.repository
|
||||
|
||||
import com.rtbishop.look4sat.core.domain.predict.GeoPos
|
||||
import kotlinx.coroutines.flow.StateFlow
|
||||
|
||||
interface ISensorsRepo {
|
||||
val orientation: StateFlow<Pair<Float, Float>>
|
||||
fun getMagDeclination(geoPos: GeoPos, time: Long = System.currentTimeMillis()): Float
|
||||
fun enableSensor()
|
||||
fun disableSensor()
|
||||
}
|
||||
+95
@@ -0,0 +1,95 @@
|
||||
/*
|
||||
* Look4Sat. Amateur radio satellite tracker and pass predictor.
|
||||
* Copyright (C) 2019-2026 Arty Bishop and contributors.
|
||||
*
|
||||
* 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.core.domain.repository
|
||||
|
||||
import com.rtbishop.look4sat.core.domain.model.DataSourcesSettings
|
||||
import com.rtbishop.look4sat.core.domain.model.DatabaseState
|
||||
import com.rtbishop.look4sat.core.domain.model.OtherSettings
|
||||
import com.rtbishop.look4sat.core.domain.model.PassesSettings
|
||||
import com.rtbishop.look4sat.core.domain.model.RCSettings
|
||||
import com.rtbishop.look4sat.core.domain.predict.GeoPos
|
||||
import kotlinx.coroutines.flow.StateFlow
|
||||
|
||||
interface ISettingsRepo {
|
||||
|
||||
val appVersionName: String
|
||||
|
||||
//region # Satellites selection settings
|
||||
val selectedIds: StateFlow<List<Int>>
|
||||
val selectedTypes: StateFlow<List<String>>
|
||||
fun setSelectedIds(ids: List<Int>)
|
||||
fun setSelectedTypes(types: List<String>)
|
||||
//endregion
|
||||
|
||||
//region # Passes filter settings
|
||||
val passesSettings: StateFlow<PassesSettings>
|
||||
fun setPassesSettings(settings: PassesSettings)
|
||||
//endregion
|
||||
|
||||
//region # Station position settings
|
||||
val stationPosition: StateFlow<GeoPos>
|
||||
fun setStationPosition(latitude: Double, longitude: Double, altitude: Double): Boolean
|
||||
fun setStationPosition(): Boolean
|
||||
fun setStationPosition(locator: String): Boolean
|
||||
//endregion
|
||||
|
||||
//region # Database update settings
|
||||
val databaseState: StateFlow<DatabaseState>
|
||||
fun getSatelliteTypesIds(types: List<String>): List<Int>
|
||||
fun setSatelliteTypeIds(type: String, ids: List<Int>)
|
||||
fun updateDatabaseState(state: DatabaseState)
|
||||
//endregion
|
||||
|
||||
//region # RC settings
|
||||
val rcSettings: StateFlow<RCSettings>
|
||||
fun setBluetoothRotatorAddress(value: String)
|
||||
fun setBluetoothRotatorFormat(value: String)
|
||||
fun setBluetoothRotatorName(value: String)
|
||||
fun setBluetoothRotatorState(value: Boolean)
|
||||
fun setBluetoothFrequencyAddress(value: String)
|
||||
fun setBluetoothFrequencyFormat(value: String)
|
||||
fun setBluetoothFrequencyState(value: Boolean)
|
||||
fun setRotatorAddress(value: String)
|
||||
fun setRotatorPort(value: String)
|
||||
fun setRotatorState(value: Boolean)
|
||||
fun setRotatorFormat(value: String)
|
||||
fun setFrequencyAddress(value: String)
|
||||
fun setFrequencyPort(value: String)
|
||||
fun setFrequencyState(value: Boolean)
|
||||
fun setFrequencyFormat(value: String)
|
||||
//endregion
|
||||
|
||||
//region # Other settings
|
||||
val otherSettings: StateFlow<OtherSettings>
|
||||
fun setStateOfAutoUpdate(value: Boolean)
|
||||
fun setStateOfSensors(value: Boolean)
|
||||
fun setStateOfSweep(value: Boolean)
|
||||
fun setStateOfUtc(value: Boolean)
|
||||
fun setStateOfLightTheme(value: Boolean)
|
||||
fun setWarningDismissed()
|
||||
fun setWhatsNewDismissed()
|
||||
//endregion
|
||||
|
||||
//region # Transceivers settings
|
||||
val dataSourcesSettings: StateFlow<DataSourcesSettings>
|
||||
fun setUseCustomTle(value: Boolean)
|
||||
fun setUseCustomTransceivers(value: Boolean)
|
||||
fun setTleUrl(value: String)
|
||||
fun setTransceiversUrl(value: String)
|
||||
//endregion
|
||||
}
|
||||
@@ -0,0 +1,36 @@
|
||||
/*
|
||||
* Look4Sat. Amateur radio satellite tracker and pass predictor.
|
||||
* Copyright (C) 2019-2026 Arty Bishop and contributors.
|
||||
*
|
||||
* 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.core.domain.source
|
||||
|
||||
import com.rtbishop.look4sat.core.domain.model.SatItem
|
||||
import com.rtbishop.look4sat.core.domain.model.SatRadio
|
||||
import com.rtbishop.look4sat.core.domain.predict.OrbitalData
|
||||
import com.rtbishop.look4sat.core.domain.predict.OrbitalObject
|
||||
|
||||
interface ILocalSource {
|
||||
suspend fun getEntriesTotal(): Int
|
||||
suspend fun getEntriesList(): List<SatItem>
|
||||
suspend fun getEntriesWithIds(ids: List<Int>): List<OrbitalObject>
|
||||
suspend fun insertEntries(entries: List<OrbitalData>)
|
||||
suspend fun deleteEntries()
|
||||
suspend fun getIdsWithModes(modes: List<String>): List<Int>
|
||||
suspend fun getRadiosTotal(): Int
|
||||
suspend fun getRadiosWithId(id: Int): List<SatRadio>
|
||||
suspend fun insertRadios(radios: List<SatRadio>)
|
||||
suspend fun deleteRadios()
|
||||
}
|
||||
@@ -0,0 +1,25 @@
|
||||
/*
|
||||
* Look4Sat. Amateur radio satellite tracker and pass predictor.
|
||||
* Copyright (C) 2019-2026 Arty Bishop and contributors.
|
||||
*
|
||||
* 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.core.domain.source
|
||||
|
||||
import java.io.InputStream
|
||||
|
||||
interface IRemoteSource {
|
||||
suspend fun getFileStream(uri: String): InputStream?
|
||||
suspend fun getNetworkStream(url: String): InputStream?
|
||||
}
|
||||
@@ -0,0 +1,52 @@
|
||||
/*
|
||||
* Look4Sat. Amateur radio satellite tracker and pass predictor.
|
||||
* Copyright (C) 2019-2026 Arty Bishop and contributors.
|
||||
*
|
||||
* 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.core.domain.source
|
||||
|
||||
object Sources {
|
||||
const val RADIO_DATA_URL = "https://db.satnogs.org/api/transmitters/?format=json&status=active"
|
||||
val satelliteDataUrls = mapOf(
|
||||
"All" to "https://celestrak.org/NORAD/elements/gp.php?GROUP=active&FORMAT=csv",
|
||||
"Amateur" to "https://celestrak.org/NORAD/elements/gp.php?GROUP=amateur&FORMAT=csv",
|
||||
"Brightest" to "https://celestrak.org/NORAD/elements/gp.php?GROUP=visual&FORMAT=csv",
|
||||
"Cubesat" to "https://celestrak.org/NORAD/elements/gp.php?GROUP=cubesat&FORMAT=csv",
|
||||
"Education" to "https://celestrak.org/NORAD/elements/gp.php?GROUP=education&FORMAT=csv",
|
||||
"Engineer" to "https://celestrak.org/NORAD/elements/gp.php?GROUP=engineering&FORMAT=csv",
|
||||
"Geostationary" to "https://celestrak.org/NORAD/elements/gp.php?GROUP=geo&FORMAT=csv",
|
||||
"Globalstar" to "https://celestrak.org/NORAD/elements/gp.php?GROUP=globalstar&FORMAT=csv",
|
||||
"GNSS" to "https://celestrak.org/NORAD/elements/gp.php?GROUP=gnss&FORMAT=csv",
|
||||
"Intelsat" to "https://celestrak.org/NORAD/elements/gp.php?GROUP=intelsat&FORMAT=csv",
|
||||
"Iridium" to "https://celestrak.org/NORAD/elements/gp.php?GROUP=iridium-NEXT&FORMAT=csv",
|
||||
"Military" to "https://celestrak.org/NORAD/elements/gp.php?GROUP=military&FORMAT=csv",
|
||||
"New" to "https://celestrak.org/NORAD/elements/gp.php?GROUP=last-30-days&FORMAT=csv",
|
||||
"OneWeb" to "https://celestrak.org/NORAD/elements/gp.php?GROUP=oneweb&FORMAT=csv",
|
||||
"Orbcomm" to "https://celestrak.org/NORAD/elements/gp.php?GROUP=orbcomm&FORMAT=csv",
|
||||
"Resource" to "https://celestrak.org/NORAD/elements/gp.php?GROUP=resource&FORMAT=csv",
|
||||
"SatNOGS" to "https://celestrak.org/NORAD/elements/gp.php?GROUP=satnogs&FORMAT=csv",
|
||||
"Science" to "https://celestrak.org/NORAD/elements/gp.php?GROUP=science&FORMAT=csv",
|
||||
"Spire" to "https://celestrak.org/NORAD/elements/gp.php?GROUP=spire&FORMAT=csv",
|
||||
"Starlink" to "https://celestrak.org/NORAD/elements/gp.php?GROUP=starlink&FORMAT=csv",
|
||||
"Swarm" to "https://celestrak.org/NORAD/elements/gp.php?GROUP=swarm&FORMAT=csv",
|
||||
"Weather" to "https://celestrak.org/NORAD/elements/gp.php?GROUP=weather&FORMAT=csv",
|
||||
"X-Comm" to "https://celestrak.org/NORAD/elements/gp.php?GROUP=x-comm&FORMAT=csv",
|
||||
"Amsat" to "https://amsat.org/tle/current/nasabare.txt",
|
||||
"Classified" to "https://www.mmccants.org/tles/classfd.zip",
|
||||
"McCants" to "https://www.mmccants.org/tles/inttles.zip",
|
||||
"R4UAB" to "https://r4uab.ru/satonline.txt",
|
||||
"Other" to "" // key for sats filter
|
||||
)
|
||||
}
|
||||
@@ -0,0 +1,22 @@
|
||||
/*
|
||||
* Look4Sat. Amateur radio satellite tracker and pass predictor.
|
||||
* Copyright (C) 2019-2026 Arty Bishop and contributors.
|
||||
*
|
||||
* 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.core.domain.usecase
|
||||
|
||||
interface IAddToCalendar {
|
||||
operator fun invoke(name: String, aosTime: Long, losTime: Long)
|
||||
}
|
||||
@@ -0,0 +1,22 @@
|
||||
/*
|
||||
* Look4Sat. Amateur radio satellite tracker and pass predictor.
|
||||
* Copyright (C) 2019-2026 Arty Bishop and contributors.
|
||||
*
|
||||
* 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.core.domain.usecase
|
||||
|
||||
interface IShowToast {
|
||||
operator fun invoke(message: String)
|
||||
}
|
||||
@@ -0,0 +1,154 @@
|
||||
/*
|
||||
* Look4Sat. Amateur radio satellite tracker and pass predictor.
|
||||
* Copyright (C) 2019-2026 Arty Bishop and contributors.
|
||||
*
|
||||
* 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.core.domain.utility
|
||||
|
||||
import com.rtbishop.look4sat.core.domain.model.SatRadio
|
||||
import com.rtbishop.look4sat.core.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) {
|
||||
return@withContext parsedItems
|
||||
}
|
||||
}
|
||||
|
||||
fun isLeapYear(year: Int): Boolean {
|
||||
return ((year % 4 == 0) && (year % 100 != 0)) || (year % 400 == 0)
|
||||
}
|
||||
|
||||
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()
|
||||
OrbitalData(name, epoch, meanmo, eccn, incl, raan, argper, meanan, catnum, bstar)
|
||||
} catch (exception: Exception) {
|
||||
println("CSV parsing exception: $exception")
|
||||
null
|
||||
}
|
||||
|
||||
private fun parseTLE(tle: List<String>): OrbitalData? = 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())
|
||||
OrbitalData(name, epoch, meanmo, eccn, incl, raan, argper, meanan, catnum, bstar)
|
||||
} catch (exception: Exception) {
|
||||
println("TLE parsing exception: $exception")
|
||||
null
|
||||
}
|
||||
|
||||
private fun parseJSON(json: JSONObject): SatRadio? = try {
|
||||
val uuid = json.getString("uuid")
|
||||
val info = json.getString("description")
|
||||
val alive = json.getBoolean("alive")
|
||||
val dlinkLow = if (json.isNull("downlink_low")) null else json.getLong("downlink_low")
|
||||
val dlinkHigh = if (json.isNull("downlink_high")) null else json.getLong("downlink_high")
|
||||
val dlinkMode = if (json.isNull("mode")) null else json.getString("mode")
|
||||
val ulinkLow = if (json.isNull("uplink_low")) null else json.getLong("uplink_low")
|
||||
val ulinkHigh = if (json.isNull("uplink_high")) null else json.getLong("uplink_high")
|
||||
val ulinkMode = if (json.isNull("uplink_mode")) null else json.getString("uplink_mode")
|
||||
val inverted = json.getBoolean("invert")
|
||||
val catnum = if (json.isNull("norad_cat_id")) null else json.getInt("norad_cat_id")
|
||||
SatRadio(uuid, info, alive, dlinkLow, dlinkHigh, dlinkMode, ulinkLow, ulinkHigh, ulinkMode, inverted, catnum)
|
||||
} catch (exception: Exception) {
|
||||
println("JSON parsing exception: $exception")
|
||||
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 (isLeapYear(year)) daysArray[1]++
|
||||
for (i in 0 until month - 1) {
|
||||
dayOfYear += daysArray[i]
|
||||
}
|
||||
return dayOfYear
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,69 @@
|
||||
/*
|
||||
* Look4Sat. Amateur radio satellite tracker and pass predictor.
|
||||
* Copyright (C) 2019-2026 Arty Bishop and contributors.
|
||||
*
|
||||
* 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.core.domain.utility
|
||||
|
||||
import java.util.Locale
|
||||
import java.util.concurrent.TimeUnit
|
||||
|
||||
fun Long.toTimerString(): String {
|
||||
val format = "%02d:%02d:%02d"
|
||||
val hours = TimeUnit.MILLISECONDS.toHours(this)
|
||||
val minutes = TimeUnit.MILLISECONDS.toMinutes(this) % 60
|
||||
val seconds = TimeUnit.MILLISECONDS.toSeconds(this) % 60
|
||||
return String.format(Locale.ENGLISH, format, hours, minutes, seconds)
|
||||
}
|
||||
|
||||
fun Float.round(decimals: Int): Float {
|
||||
var multiplier = 1.0f
|
||||
repeat(decimals) { multiplier *= 10 }
|
||||
return kotlin.math.round(this * multiplier) / multiplier
|
||||
}
|
||||
|
||||
fun Double.round(decimals: Int): Double {
|
||||
var multiplier = 1.0
|
||||
repeat(decimals) { multiplier *= 10 }
|
||||
return kotlin.math.round(this * multiplier) / multiplier
|
||||
}
|
||||
|
||||
//fun String.getHash(type: String = "SHA-256"): String {
|
||||
// val hexChars = "0123456789ABCDEF"
|
||||
// val bytes = MessageDigest.getInstance(type).digest(this.toByteArray())
|
||||
// val result = StringBuilder(bytes.size * 2)
|
||||
// bytes.forEach {
|
||||
// val i = it.toInt()
|
||||
// result.append(hexChars[i shr 4 and 0x0f])
|
||||
// result.append(hexChars[i and 0x0f])
|
||||
// }
|
||||
// return result.toString()
|
||||
//}
|
||||
|
||||
//fun String.isValidEmail(): Boolean {
|
||||
// val expression = "^[\\w.-]+@([\\w\\-]+\\.)+[A-Z]{2,8}$"
|
||||
// val pattern = Pattern.compile(expression, Pattern.CASE_INSENSITIVE)
|
||||
// return pattern.matcher(this).matches()
|
||||
//}
|
||||
|
||||
//fun String.isValidIPv4(): Boolean {
|
||||
// val ip4 = "^((\\d|[1-9]\\d|1\\d\\d|2[0-4]\\d|25[0-5])(\\.(?!\$)|\$)){4}\$"
|
||||
// return this.matches(ip4.toRegex())
|
||||
//}
|
||||
|
||||
//fun String.isValidPort(): Boolean {
|
||||
// val port = "([1-9]|[1-9]\\d{1,3}|[1-5]\\d{4}|6[0-4]\\d{3}|65[0-4]\\d{2}|655[0-2]\\d|6553[0-5])"
|
||||
// return this.matches(port.toRegex()) && this.toInt() in 1024..65535
|
||||
//}
|
||||
@@ -0,0 +1,64 @@
|
||||
/*
|
||||
* Look4Sat. Amateur radio satellite tracker and pass predictor.
|
||||
* Copyright (C) 2019-2026 Arty Bishop and contributors.
|
||||
*
|
||||
* 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.core.domain.utility
|
||||
|
||||
import com.rtbishop.look4sat.core.domain.predict.DEG2RAD
|
||||
import com.rtbishop.look4sat.core.domain.predict.RAD2DEG
|
||||
import kotlin.math.max
|
||||
import kotlin.math.min
|
||||
|
||||
private const val MIN_LATITUDE = -85.05112877980658
|
||||
private const val MAX_LATITUDE = 85.05112877980658
|
||||
private const val MIN_LONGITUDE = -180.0
|
||||
private const val MAX_LONGITUDE = 180.0
|
||||
|
||||
fun Double.toDegrees(): Double = this * RAD2DEG
|
||||
|
||||
fun Double.toRadians(): Double = this * DEG2RAD
|
||||
|
||||
//fun Double.latToY01(): Double {
|
||||
// val sinus = sin(clipLat(this) * PI / MAX_LONGITUDE)
|
||||
// return 0.5 - ln((1 + sinus) / (1 - sinus)) / (4 * PI)
|
||||
//}
|
||||
|
||||
//fun Double.lonToX01(): Double {
|
||||
// return (clipLon(this) - MIN_LONGITUDE) / (MAX_LONGITUDE - MIN_LONGITUDE)
|
||||
//}
|
||||
|
||||
//fun Double.y01ToLat(): Double {
|
||||
// return 90 - 360 * atan(exp((this - 0.5) * 2 * PI)) / PI
|
||||
//}
|
||||
|
||||
//fun Double.x01ToLon(): Double {
|
||||
// return MIN_LONGITUDE + (MAX_LONGITUDE - MIN_LONGITUDE) * this
|
||||
//}
|
||||
|
||||
fun clipLat(latitude: Double): Double {
|
||||
return clip(latitude, MIN_LATITUDE, MAX_LATITUDE)
|
||||
}
|
||||
|
||||
fun clipLon(longitude: Double): Double {
|
||||
var result = longitude
|
||||
while (result < MIN_LONGITUDE) result += 360.0
|
||||
while (result > MAX_LONGITUDE) result -= 360.0
|
||||
return clip(result, MIN_LONGITUDE, MAX_LONGITUDE)
|
||||
}
|
||||
|
||||
private fun clip(currentValue: Double, minValue: Double, maxValue: Double): Double {
|
||||
return min(max(currentValue, minValue), maxValue)
|
||||
}
|
||||
@@ -0,0 +1,55 @@
|
||||
/*
|
||||
* Look4Sat. Amateur radio satellite tracker and pass predictor.
|
||||
* Copyright (C) 2019-2026 Arty Bishop and contributors.
|
||||
*
|
||||
* 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.core.domain.utility
|
||||
|
||||
import com.rtbishop.look4sat.core.domain.predict.GeoPos
|
||||
|
||||
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 newLongitude = if (longitude > 180.0) longitude else longitude + 180
|
||||
val newLatitude = latitude + 90
|
||||
val lonFirst = (65 + (newLongitude / 20)).toInt().toChar()
|
||||
val latFirst = (65 + (newLatitude / 10)).toInt().toChar()
|
||||
val lonSecond = ((newLongitude / 2) % 10).toInt()
|
||||
val latSecond = (newLatitude % 10).toInt()
|
||||
val lonThird = (65 + (newLongitude % 2) * 12).toInt().toChar().lowercaseChar()
|
||||
val latThird = (65 + (newLatitude % 1) * 24).toInt().toChar().lowercaseChar()
|
||||
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())
|
||||
}
|
||||
@@ -0,0 +1,105 @@
|
||||
/*
|
||||
* Look4Sat. Amateur radio satellite tracker and pass predictor.
|
||||
* Copyright (C) 2019-2026 Arty Bishop and contributors.
|
||||
*
|
||||
* 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.core.domain
|
||||
|
||||
import com.rtbishop.look4sat.core.domain.utility.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
|
||||
""".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
|
||||
""".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) {
|
||||
assert(dataParser.parseCSVStream(invalidCSVStream).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) {
|
||||
assert(dataParser.parseTLEStream(invalidTLEStream).isEmpty())
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `Given valid JSON stream returns valid data`() = runTest(testDispatcher) {
|
||||
assert(dataParser.parseJSONStream(validJSONStream)[0].downlinkLow == 136658500L)
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `Given invalid JSON stream returns empty list`() = runTest(testDispatcher) {
|
||||
assert(dataParser.parseJSONStream(invalidJSONStream).isEmpty())
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `Given valid data streams parsed results match`() = runTest(testDispatcher) {
|
||||
assert(dataParser.parseCSVStream(validCSVStream) == dataParser.parseTLEStream(validTLEStream))
|
||||
}
|
||||
|
||||
@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,51 @@
|
||||
/*
|
||||
* Look4Sat. Amateur radio satellite tracker and pass predictor.
|
||||
* Copyright (C) 2019-2026 Arty Bishop and contributors.
|
||||
*
|
||||
* 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.core.domain
|
||||
|
||||
import com.rtbishop.look4sat.core.domain.utility.positionToQth
|
||||
import com.rtbishop.look4sat.core.domain.utility.qthToPosition
|
||||
import org.junit.Test
|
||||
|
||||
class QthConverterTest {
|
||||
|
||||
@Test
|
||||
fun `Given valid QTH returns correct POS`() {
|
||||
var result = qthToPosition("io91VL39FX")
|
||||
assert(result?.latitude == 51.4792 && result.longitude == -0.2083)
|
||||
result = qthToPosition("gf15vc")
|
||||
assert(result?.latitude == -34.8958 && result.longitude == -56.2083)
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `Given invalid QTH returns null`() {
|
||||
assert(qthToPosition("ZZ00zz") == null)
|
||||
assert(qthToPosition("JN58") == null)
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `Given valid POS returns correct QTH`() {
|
||||
assert(positionToQth(51.4878, -0.2146) == "IO91vl")
|
||||
assert(positionToQth(48.1466, 11.6083) == "JN58td")
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `Given invalid POS returns null`() {
|
||||
assert(positionToQth(91.0542, -170.1142) == null)
|
||||
assert(positionToQth(89.0542, -240.1142) == null)
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1 @@
|
||||
/build
|
||||
@@ -0,0 +1,7 @@
|
||||
plugins {
|
||||
alias(libs.plugins.convention.composeLibraryPlugin)
|
||||
}
|
||||
|
||||
android {
|
||||
namespace = "com.rtbishop.look4sat.core.presentation"
|
||||
}
|
||||
@@ -0,0 +1,4 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<manifest>
|
||||
|
||||
</manifest>
|
||||
+414
@@ -0,0 +1,414 @@
|
||||
/*
|
||||
* Look4Sat. Amateur radio satellite tracker and pass predictor.
|
||||
* Copyright (C) 2019-2026 Arty Bishop and contributors.
|
||||
*
|
||||
* 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.core.presentation
|
||||
|
||||
import androidx.compose.foundation.MarqueeSpacing
|
||||
import androidx.compose.foundation.basicMarquee
|
||||
import androidx.compose.foundation.clickable
|
||||
import androidx.compose.foundation.layout.Arrangement
|
||||
import androidx.compose.foundation.layout.Box
|
||||
import androidx.compose.foundation.layout.Column
|
||||
import androidx.compose.foundation.layout.PaddingValues
|
||||
import androidx.compose.foundation.layout.Row
|
||||
import androidx.compose.foundation.layout.RowScope
|
||||
import androidx.compose.foundation.layout.Spacer
|
||||
import androidx.compose.foundation.layout.fillMaxSize
|
||||
import androidx.compose.foundation.layout.fillMaxWidth
|
||||
import androidx.compose.foundation.layout.height
|
||||
import androidx.compose.foundation.layout.padding
|
||||
import androidx.compose.foundation.layout.size
|
||||
import androidx.compose.foundation.layout.statusBarsPadding
|
||||
import androidx.compose.foundation.layout.width
|
||||
import androidx.compose.material3.ButtonDefaults
|
||||
import androidx.compose.material3.CardDefaults
|
||||
import androidx.compose.material3.CircularProgressIndicator
|
||||
import androidx.compose.material3.ElevatedButton
|
||||
import androidx.compose.material3.ElevatedCard
|
||||
import androidx.compose.material3.Icon
|
||||
import androidx.compose.material3.MaterialTheme
|
||||
import androidx.compose.material3.Surface
|
||||
import androidx.compose.material3.Text
|
||||
import androidx.compose.material3.adaptive.currentWindowAdaptiveInfo
|
||||
import androidx.compose.runtime.Composable
|
||||
import androidx.compose.ui.Alignment
|
||||
import androidx.compose.ui.Modifier
|
||||
import androidx.compose.ui.res.painterResource
|
||||
import androidx.compose.ui.res.stringResource
|
||||
import androidx.compose.ui.text.font.FontWeight
|
||||
import androidx.compose.ui.text.style.TextAlign
|
||||
import androidx.compose.ui.text.style.TextOverflow
|
||||
import androidx.compose.ui.tooling.preview.Preview
|
||||
import androidx.compose.ui.unit.dp
|
||||
import androidx.compose.ui.unit.sp
|
||||
import androidx.compose.ui.window.Dialog
|
||||
import com.rtbishop.look4sat.core.domain.predict.NearEarthObject
|
||||
import com.rtbishop.look4sat.core.domain.predict.OrbitalData
|
||||
import com.rtbishop.look4sat.core.domain.predict.OrbitalPass
|
||||
import java.text.SimpleDateFormat
|
||||
import java.util.Date
|
||||
import java.util.Locale
|
||||
|
||||
private val sdfTime = SimpleDateFormat("HH:mm:ss", Locale.ENGLISH)
|
||||
|
||||
@Composable
|
||||
@Preview(showBackground = true)
|
||||
private fun TopBarPreview() = MainTheme {
|
||||
TopBar {
|
||||
IconCard(action = {}, resId = R.drawable.ic_filter)
|
||||
TimerRow(timeString = "88:88:88", isTimeAos = true)
|
||||
IconCard(action = {}, resId = R.drawable.ic_radios)
|
||||
}
|
||||
}
|
||||
|
||||
@Composable
|
||||
fun TopBar(content: @Composable (RowScope.() -> Unit)) {
|
||||
Row(
|
||||
modifier = Modifier.height(48.dp),
|
||||
horizontalArrangement = Arrangement.spacedBy(6.dp),
|
||||
verticalAlignment = Alignment.CenterVertically
|
||||
) { content() }
|
||||
}
|
||||
|
||||
@Composable
|
||||
fun RowScope.TimerRow(timeString: String, isTimeAos: Boolean) {
|
||||
val colorScheme = MaterialTheme.colorScheme
|
||||
val aosColor = if (isTimeAos) colorScheme.primary else colorScheme.onSurface
|
||||
val losColor = if (!isTimeAos) colorScheme.primary else colorScheme.onSurface
|
||||
ElevatedCard(modifier = Modifier.weight(1f)) {
|
||||
Row(
|
||||
verticalAlignment = Alignment.CenterVertically,
|
||||
horizontalArrangement = Arrangement.SpaceEvenly,
|
||||
modifier = Modifier.fillMaxSize()
|
||||
) {
|
||||
Text(text = "AOS", fontSize = 16.sp, fontWeight = FontWeight.Medium, color = aosColor)
|
||||
Text(
|
||||
text = timeString,
|
||||
fontSize = 32.sp,
|
||||
fontWeight = FontWeight.Bold,
|
||||
color = MaterialTheme.colorScheme.primary
|
||||
)
|
||||
Text(text = "LOS", fontSize = 16.sp, fontWeight = FontWeight.Medium, color = losColor)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@Composable
|
||||
@Preview(showBackground = true)
|
||||
private fun NextPassRowPreview() = MainTheme {
|
||||
TopBar { NextPassRow(pass = getDefaultPass()) }
|
||||
}
|
||||
|
||||
@Composable
|
||||
fun RowScope.NextPassRow(pass: OrbitalPass, modifier: Modifier = Modifier) {
|
||||
ElevatedCard(
|
||||
modifier = modifier
|
||||
.height(48.dp)
|
||||
.weight(1f)
|
||||
) {
|
||||
Column(
|
||||
verticalArrangement = Arrangement.spacedBy((-2).dp),
|
||||
modifier = Modifier
|
||||
.fillMaxSize()
|
||||
// .background(color = MaterialTheme.colorScheme.background)
|
||||
.padding(start = 6.dp, top = 1.dp, end = 6.dp, bottom = 0.dp)
|
||||
) {
|
||||
val passSatId = stringResource(id = R.string.pass_satId, pass.catNum)
|
||||
Row(verticalAlignment = Alignment.CenterVertically) {
|
||||
Text(
|
||||
text = "$passSatId - ",
|
||||
color = MaterialTheme.colorScheme.primary
|
||||
)
|
||||
Text(
|
||||
text = pass.name,
|
||||
modifier = Modifier
|
||||
.weight(1f)
|
||||
.padding(end = 6.dp)
|
||||
.infiniteMarquee(),
|
||||
fontWeight = FontWeight.Medium,
|
||||
maxLines = 1,
|
||||
overflow = TextOverflow.Ellipsis
|
||||
)
|
||||
Icon(
|
||||
painter = painterResource(id = R.drawable.ic_elevation),
|
||||
contentDescription = null,
|
||||
tint = MaterialTheme.colorScheme.primary,
|
||||
modifier = Modifier.size(16.dp)
|
||||
)
|
||||
Spacer(modifier = Modifier.width(4.dp))
|
||||
Text(
|
||||
text = "${pass.maxElevation}°",
|
||||
color = MaterialTheme.colorScheme.primary
|
||||
)
|
||||
}
|
||||
Row(
|
||||
verticalAlignment = Alignment.CenterVertically,
|
||||
horizontalArrangement = Arrangement.SpaceBetween,
|
||||
modifier = Modifier.fillMaxWidth()
|
||||
) {
|
||||
Row(
|
||||
modifier = Modifier.weight(1f),
|
||||
horizontalArrangement = Arrangement.Start,
|
||||
verticalAlignment = Alignment.CenterVertically
|
||||
) {
|
||||
Text(
|
||||
text = sdfTime.format(Date(pass.aosTime)),
|
||||
fontSize = 15.sp,
|
||||
modifier = Modifier.weight(1f)
|
||||
)
|
||||
}
|
||||
Row(
|
||||
modifier = Modifier.weight(1f),
|
||||
horizontalArrangement = Arrangement.Center,
|
||||
verticalAlignment = Alignment.CenterVertically
|
||||
) {
|
||||
Icon(
|
||||
painter = painterResource(id = R.drawable.ic_altitude),
|
||||
contentDescription = null,
|
||||
modifier = Modifier.size(16.dp)
|
||||
)
|
||||
Spacer(modifier = Modifier.width(4.dp))
|
||||
Text(
|
||||
text = "${pass.altitude} km",
|
||||
fontSize = 15.sp
|
||||
)
|
||||
}
|
||||
Row(
|
||||
modifier = Modifier.weight(1f),
|
||||
horizontalArrangement = Arrangement.End,
|
||||
verticalAlignment = Alignment.CenterVertically
|
||||
) {
|
||||
Text(
|
||||
text = stringResource(
|
||||
id = R.string.pass_aosLos,
|
||||
pass.aosAzimuth.toInt(),
|
||||
pass.losAzimuth.toInt()
|
||||
),
|
||||
fontSize = 15.sp
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@Composable
|
||||
fun CardButton(onClick: () -> Unit, text: String, modifier: Modifier = Modifier) {
|
||||
ElevatedButton(
|
||||
onClick = { onClick() }, colors = ButtonDefaults.buttonColors(
|
||||
containerColor = MaterialTheme.colorScheme.surfaceVariant,
|
||||
contentColor = MaterialTheme.colorScheme.onSurfaceVariant
|
||||
), shape = MaterialTheme.shapes.small, modifier = modifier,
|
||||
contentPadding = PaddingValues(horizontal = 8.dp, vertical = 8.dp)
|
||||
) { Text(text = text, fontSize = 16.sp, textAlign = TextAlign.Center) }
|
||||
}
|
||||
|
||||
@Composable
|
||||
fun IconCard(action: () -> Unit, resId: Int, modifier: Modifier = Modifier) {
|
||||
val clickableMod = Modifier.clickable { action() }
|
||||
ElevatedCard(modifier = Modifier.size(48.dp)) {
|
||||
Box(modifier = clickableMod.fillMaxSize(), contentAlignment = Alignment.Center) {
|
||||
Icon(painter = painterResource(resId), contentDescription = null, modifier = modifier)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@Composable
|
||||
fun PrimaryIconCard(modifier: Modifier = Modifier, resId: Int, onClick: () -> Unit) {
|
||||
val clickableMod = modifier.clickable { onClick() }
|
||||
ElevatedCard(
|
||||
modifier = Modifier.size(102.dp, 48.dp),
|
||||
colors = CardDefaults.elevatedCardColors(containerColor = MaterialTheme.colorScheme.primary)
|
||||
) {
|
||||
Box(modifier = clickableMod.fillMaxSize(), contentAlignment = Alignment.Center) {
|
||||
Icon(painter = painterResource(id = resId), contentDescription = null)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@Composable
|
||||
fun CardLoadingIndicator() {
|
||||
Box(contentAlignment = Alignment.Center, modifier = Modifier.fillMaxSize()) {
|
||||
CircularProgressIndicator(modifier = Modifier.size(80.dp))
|
||||
}
|
||||
}
|
||||
|
||||
@Composable
|
||||
fun EmptyListCard(message: String) {
|
||||
Box(contentAlignment = Alignment.Center, modifier = Modifier.fillMaxSize()) {
|
||||
Column(
|
||||
horizontalAlignment = Alignment.CenterHorizontally,
|
||||
verticalArrangement = Arrangement.spacedBy(24.dp),
|
||||
modifier = Modifier.padding(32.dp)
|
||||
) {
|
||||
Text(text = """¯\_(ツ)_/¯""", fontSize = 32.sp)
|
||||
Text(text = stringResource(R.string.empty_list_message), fontSize = 21.sp, textAlign = TextAlign.Center)
|
||||
Text(text = message, fontSize = 18.sp, textAlign = TextAlign.Center)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fun getDefaultPass(): OrbitalPass {
|
||||
val orbitalData = OrbitalData(
|
||||
""" ¯\_(ツ)_/¯ ⚠️""",
|
||||
0.0,
|
||||
0.0,
|
||||
0.0,
|
||||
0.0,
|
||||
0.0,
|
||||
0.0,
|
||||
0.0,
|
||||
0,
|
||||
0.0
|
||||
)
|
||||
val satellite = NearEarthObject(orbitalData)
|
||||
return OrbitalPass(0L, 0.0, Long.MAX_VALUE, 0.0, 0, 0.0, satellite, 0f)
|
||||
}
|
||||
|
||||
@Composable
|
||||
fun SharedDialog(
|
||||
title: String, onCancel: () -> Unit, onAccept: () -> Unit, content: @Composable () -> Unit
|
||||
) {
|
||||
val padding = LocalSpacing.current.large
|
||||
Dialog(onDismissRequest = { onCancel() }) {
|
||||
ElevatedCard {
|
||||
Column(
|
||||
horizontalAlignment = Alignment.CenterHorizontally,
|
||||
verticalArrangement = Arrangement.spacedBy(padding)
|
||||
) {
|
||||
Text(
|
||||
text = title,
|
||||
fontSize = 16.sp,
|
||||
fontWeight = FontWeight.Medium,
|
||||
color = MaterialTheme.colorScheme.primary,
|
||||
modifier = Modifier.padding(start = padding, top = padding, end = padding)
|
||||
)
|
||||
content()
|
||||
Row(modifier = Modifier.padding(start = padding, bottom = padding, end = padding)) {
|
||||
CardButton(onClick = onCancel, text = stringResource(id = R.string.btn_cancel))
|
||||
Spacer(modifier = Modifier.weight(1f))
|
||||
CardButton(onClick = onAccept, text = stringResource(id = R.string.btn_accept))
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@Composable
|
||||
fun InfoDialog(title: String, text: String, onDismiss: () -> Unit) {
|
||||
val padding = LocalSpacing.current.large
|
||||
Dialog(onDismissRequest = {}) {
|
||||
ElevatedCard {
|
||||
Column(
|
||||
horizontalAlignment = Alignment.CenterHorizontally,
|
||||
verticalArrangement = Arrangement.spacedBy(padding)
|
||||
) {
|
||||
Text(
|
||||
text = title,
|
||||
fontSize = 18.sp,
|
||||
fontWeight = FontWeight.Medium,
|
||||
color = MaterialTheme.colorScheme.primary,
|
||||
modifier = Modifier.padding(start = padding, top = padding, end = padding)
|
||||
)
|
||||
Text(
|
||||
text = text,
|
||||
fontSize = 16.sp,
|
||||
color = MaterialTheme.colorScheme.onSurface,
|
||||
modifier = Modifier.padding(horizontal = padding)
|
||||
)
|
||||
Row(modifier = Modifier.padding(start = padding, bottom = padding, end = padding)) {
|
||||
Spacer(modifier = Modifier.weight(1f))
|
||||
CardButton(
|
||||
onClick = onDismiss,
|
||||
text = stringResource(id = R.string.btn_accept)
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@Composable
|
||||
fun hasEnoughHeight(): Boolean {
|
||||
return currentWindowAdaptiveInfo().windowSizeClass.isHeightAtLeastBreakpoint(480)
|
||||
}
|
||||
|
||||
@Composable
|
||||
fun hasEnoughWidth(): Boolean {
|
||||
return currentWindowAdaptiveInfo().windowSizeClass.isWidthAtLeastBreakpoint(600)
|
||||
}
|
||||
|
||||
@Composable
|
||||
fun isVerticalLayout(): Boolean {
|
||||
return currentWindowAdaptiveInfo().windowSizeClass.isWidthAtLeastBreakpoint(600).not()
|
||||
}
|
||||
|
||||
@Composable
|
||||
fun Modifier.infiniteMarquee(): Modifier {
|
||||
return basicMarquee(iterations = Int.MAX_VALUE, spacing = MarqueeSpacing(16.dp))
|
||||
}
|
||||
|
||||
@Composable
|
||||
fun Modifier.layoutPadding(): Modifier {
|
||||
val statusBarMod = this.statusBarsPadding()
|
||||
val spacing = LocalSpacing.current.extraSmall
|
||||
return statusBarMod.padding(start = spacing, top = 0.dp, end = spacing, bottom = spacing)
|
||||
}
|
||||
|
||||
@Composable
|
||||
fun ScreenColumn(
|
||||
topBar: @Composable (Boolean) -> Unit = {},
|
||||
floatingBar: @Composable () -> Unit = {},
|
||||
content: @Composable (Boolean) -> Unit = {}
|
||||
) {
|
||||
val isVerticalLayout = isVerticalLayout()
|
||||
Surface(color = MaterialTheme.colorScheme.background) {
|
||||
Box(modifier = Modifier.layoutPadding(), contentAlignment = Alignment.BottomCenter) {
|
||||
Column(verticalArrangement = Arrangement.spacedBy(LocalSpacing.current.extraSmall)) {
|
||||
topBar(isVerticalLayout)
|
||||
Surface(
|
||||
modifier = Modifier.fillMaxSize(),
|
||||
color = MaterialTheme.colorScheme.surfaceContainer
|
||||
) { content(isVerticalLayout) }
|
||||
}
|
||||
floatingBar()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@Composable
|
||||
fun TopBar(
|
||||
isVerticalLayout: Boolean,
|
||||
startAction: @Composable () -> Unit,
|
||||
topInfo: @Composable (RowScope.() -> Unit),
|
||||
bottomInfo: @Composable (RowScope.() -> Unit),
|
||||
endAction: @Composable () -> Unit
|
||||
) {
|
||||
val topBarRow = @Composable { content: @Composable RowScope.() -> Unit ->
|
||||
Row(
|
||||
modifier = Modifier.height(48.dp),
|
||||
horizontalArrangement = Arrangement.spacedBy(6.dp),
|
||||
verticalAlignment = Alignment.CenterVertically
|
||||
) { content() }
|
||||
}
|
||||
if (isVerticalLayout) {
|
||||
topBarRow { startAction().also { topInfo() }.also { endAction() } }
|
||||
topBarRow { bottomInfo() }
|
||||
} else {
|
||||
topBarRow { startAction().also { topInfo() }.also { bottomInfo() }.also { endAction() } }
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,161 @@
|
||||
/*
|
||||
* Look4Sat. Amateur radio satellite tracker and pass predictor.
|
||||
* Copyright (C) 2019-2026 Arty Bishop and contributors.
|
||||
*
|
||||
* 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.core.presentation
|
||||
|
||||
import android.os.Build
|
||||
import androidx.activity.ComponentActivity
|
||||
import androidx.compose.foundation.isSystemInDarkTheme
|
||||
import androidx.compose.foundation.shape.RoundedCornerShape
|
||||
import androidx.compose.material3.MaterialTheme
|
||||
import androidx.compose.material3.Shapes
|
||||
import androidx.compose.material3.Typography
|
||||
import androidx.compose.material3.darkColorScheme
|
||||
import androidx.compose.material3.lightColorScheme
|
||||
import androidx.compose.runtime.Composable
|
||||
import androidx.compose.runtime.CompositionLocalProvider
|
||||
import androidx.compose.runtime.SideEffect
|
||||
import androidx.compose.runtime.compositionLocalOf
|
||||
import androidx.compose.ui.graphics.Color
|
||||
import androidx.compose.ui.platform.LocalView
|
||||
import androidx.compose.ui.text.TextStyle
|
||||
import androidx.compose.ui.text.font.FontFamily
|
||||
import androidx.compose.ui.text.font.FontWeight
|
||||
import androidx.compose.ui.unit.dp
|
||||
import androidx.compose.ui.unit.sp
|
||||
import androidx.core.view.WindowCompat
|
||||
|
||||
val LocalSpacing = compositionLocalOf { Spacing }
|
||||
|
||||
data object Spacing {
|
||||
val extraExtraSmall = 4.dp
|
||||
val extraSmall = 6.dp
|
||||
val small = 8.dp
|
||||
val medium = 12.dp
|
||||
val large = 16.dp
|
||||
val extraLarge = 24.dp
|
||||
}
|
||||
|
||||
@Composable
|
||||
fun MainTheme(isDarkTheme: Boolean = isSystemInDarkTheme(), content: @Composable () -> Unit) {
|
||||
val view = LocalView.current
|
||||
if (view.isInEditMode) {
|
||||
MaterialTheme(darkScheme, shapes, typography, content)
|
||||
} else {
|
||||
CompositionLocalProvider(LocalSpacing provides Spacing) {
|
||||
val colorScheme = if (isDarkTheme) darkScheme else lightScheme
|
||||
SideEffect {
|
||||
val window = (view.context as ComponentActivity).window
|
||||
val insetsController = WindowCompat.getInsetsController(window, view)
|
||||
insetsController.isAppearanceLightStatusBars = false
|
||||
insetsController.isAppearanceLightNavigationBars = false
|
||||
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.Q) {
|
||||
window.isNavigationBarContrastEnforced = false
|
||||
}
|
||||
}
|
||||
MaterialTheme(colorScheme, shapes, typography, content)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private val lightScheme = lightColorScheme(
|
||||
primary = Color(0xFF715C0C),
|
||||
onPrimary = Color(0xFFFFFFFF),
|
||||
secondary = Color(0xFF685E40),
|
||||
onSecondary = Color(0xFFFFFFFF),
|
||||
secondaryContainer = Color(0xFFF1E1BB),
|
||||
onSecondaryContainer = Color(0xFF221B04),
|
||||
background = Color(0xFFFFF8F0),
|
||||
onBackground = Color(0xFF1E1B13),
|
||||
surface = Color(0xFFFFF8F0),
|
||||
onSurface = Color(0xFF1E1B13),
|
||||
surfaceTint = Color(0x00000000),
|
||||
surfaceVariant = Color(0xFFEBE2CF),
|
||||
onSurfaceVariant = Color(0xFF4C4639),
|
||||
error = Color(0xFFDC0000),
|
||||
outline = Color(0xFF7D7667),
|
||||
scrim = Color(0xFF000000)
|
||||
)
|
||||
|
||||
private val darkScheme = darkColorScheme(
|
||||
primary = Color(0xFFFFE082), // main accent, switch background, active progress
|
||||
onPrimary = Color(0xFF000000), // on main accent, switch knob
|
||||
// primaryContainer = Color(0xFF121212),
|
||||
// onPrimaryContainer = Color(0xFF121212),
|
||||
// inversePrimary = Color(0xFF121212),
|
||||
secondary = Color(0xFFE0E0E0),
|
||||
onSecondary = Color(0xFF000000),
|
||||
secondaryContainer = Color(0xFF404040), // navBar indicator,
|
||||
onSecondaryContainer = Color(0xFFE0E0E0), // navBar active icon
|
||||
// tertiary = Color(0xFF121212),
|
||||
// onTertiary = Color(0xFF121212),
|
||||
// tertiaryContainer = Color(0xFF121212),
|
||||
// onTertiaryContainer = Color(0xFF121212),
|
||||
background = Color(0xFF121212),
|
||||
onBackground = Color(0xFFE0E0E0),
|
||||
surface = Color(0xFF202020), // card background
|
||||
onSurface = Color(0xFFE0E0E0), // on card background
|
||||
surfaceVariant = Color(0xFF404040), // buttons background
|
||||
onSurfaceVariant = Color(0xFFE0E0E0), // navBar inactive icon
|
||||
surfaceTint = Color(0x00000000),
|
||||
// inverseSurface = Color(0xFF121212),
|
||||
// inverseOnSurface = Color(0xFF121212),
|
||||
error = Color(0xFFDC0000),
|
||||
// onError = Color(0xFFFFFFFF),
|
||||
// errorContainer = Color(0xFF121212),
|
||||
// onErrorContainer = Color(0xFF121212),
|
||||
outline = Color(0xA3E0E0E0),
|
||||
// outlineVariant = Color(0xFF121212),
|
||||
scrim = Color(0xFF000000),
|
||||
// surfaceBright = Color(0xFF121212),
|
||||
surfaceContainer = Color(0xFF121212), // navBar background
|
||||
// surfaceContainerHigh = Color(0xFF121212),
|
||||
surfaceContainerHighest = Color(0xFF242424), // filled card background
|
||||
surfaceContainerLow = Color(0xFF202020), // elevated card background
|
||||
surfaceContainerLowest = Color(0xCC121212),
|
||||
// surfaceDim = Color(0xFF121212),
|
||||
)
|
||||
|
||||
private val shapes = Shapes(
|
||||
extraSmall = RoundedCornerShape(4.dp), // menus, snackbars, text fields
|
||||
small = RoundedCornerShape(8.dp), // buttons, chips
|
||||
medium = RoundedCornerShape(12.dp), // cards, small FABs
|
||||
large = RoundedCornerShape(24.dp), // extended FABs, FABs, nav drawers
|
||||
extraLarge = RoundedCornerShape(32.dp) // large FABs
|
||||
)
|
||||
|
||||
private val typography = Typography(
|
||||
bodyLarge = TextStyle(
|
||||
fontFamily = FontFamily.Default,
|
||||
fontWeight = FontWeight.Normal,
|
||||
fontSize = 16.sp,
|
||||
lineHeight = 24.sp,
|
||||
letterSpacing = 0.5.sp
|
||||
), titleLarge = TextStyle(
|
||||
fontFamily = FontFamily.Default,
|
||||
fontWeight = FontWeight.Normal,
|
||||
fontSize = 22.sp,
|
||||
lineHeight = 28.sp,
|
||||
letterSpacing = 0.sp
|
||||
), labelSmall = TextStyle(
|
||||
fontFamily = FontFamily.Default,
|
||||
fontWeight = FontWeight.Medium,
|
||||
fontSize = 11.sp,
|
||||
lineHeight = 16.sp,
|
||||
letterSpacing = 0.5.sp
|
||||
)
|
||||
)
|
||||
@@ -0,0 +1,9 @@
|
||||
package com.rtbishop.look4sat.core.presentation
|
||||
|
||||
sealed class Screen(val route: String, val iconResId: Int, val titleResId: Int) {
|
||||
data object Satellites : Screen("satellites", R.drawable.ic_satellites, R.string.nav_sat)
|
||||
data object Passes : Screen("passes", R.drawable.ic_passes, R.string.nav_pass)
|
||||
data object Radar : Screen("radar", R.drawable.ic_radar, R.string.nav_radar)
|
||||
data object Map : Screen("map", R.drawable.ic_map, R.string.nav_map)
|
||||
data object Settings : Screen("settings", R.drawable.ic_settings, R.string.nav_prefs)
|
||||
}
|
||||
@@ -0,0 +1,9 @@
|
||||
<vector xmlns:android="http://schemas.android.com/apk/res/android"
|
||||
android:width="24dp"
|
||||
android:height="24dp"
|
||||
android:viewportWidth="24"
|
||||
android:viewportHeight="24">
|
||||
<path
|
||||
android:fillColor="@android:color/white"
|
||||
android:pathData="M4,20h16v2L4,22zM4,2h16v2L4,4zM13,9h3l-4,-4 -4,4h3v6L8,15l4,4 4,-4h-3z" />
|
||||
</vector>
|
||||
@@ -0,0 +1,9 @@
|
||||
<vector xmlns:android="http://schemas.android.com/apk/res/android"
|
||||
android:width="24dp"
|
||||
android:height="24dp"
|
||||
android:viewportWidth="960"
|
||||
android:viewportHeight="960">
|
||||
<path
|
||||
android:fillColor="@android:color/white"
|
||||
android:pathData="M504,480L320,296L376,240L616,480L376,720L320,664L504,480Z" />
|
||||
</vector>
|
||||
@@ -0,0 +1,9 @@
|
||||
<vector xmlns:android="http://schemas.android.com/apk/res/android"
|
||||
android:width="24dp"
|
||||
android:height="24dp"
|
||||
android:viewportWidth="24"
|
||||
android:viewportHeight="24">
|
||||
<path
|
||||
android:fillColor="@android:color/white"
|
||||
android:pathData="M20,11H7.83l5.59,-5.59L12,4l-8,8 8,8 1.41,-1.41L7.83,13H20v-2z" />
|
||||
</vector>
|
||||
@@ -0,0 +1,9 @@
|
||||
<vector xmlns:android="http://schemas.android.com/apk/res/android"
|
||||
android:width="24dp"
|
||||
android:height="24dp"
|
||||
android:viewportWidth="960"
|
||||
android:viewportHeight="960">
|
||||
<path
|
||||
android:fillColor="@android:color/white"
|
||||
android:pathData="M200,320L760,320L760,240Q760,240 760,240Q760,240 760,240L200,240Q200,240 200,240Q200,240 200,240L200,320ZM200,320L200,240Q200,240 200,240Q200,240 200,240L200,240Q200,240 200,240Q200,240 200,240L200,320ZM200,880Q167,880 143.5,856.5Q120,833 120,800L120,240Q120,207 143.5,183.5Q167,160 200,160L240,160L240,80L320,80L320,160L640,160L640,80L720,80L720,160L760,160Q793,160 816.5,183.5Q840,207 840,240L840,467Q821,458 801,452Q781,446 760,443L760,400L200,400L200,800Q200,800 200,800Q200,800 200,800L452,800Q459,822 468.5,842Q478,862 491,880L200,880ZM720,920Q637,920 578.5,861.5Q520,803 520,720Q520,637 578.5,578.5Q637,520 720,520Q803,520 861.5,578.5Q920,637 920,720Q920,803 861.5,861.5Q803,920 720,920ZM787,815L815,787L740,712L740,600L700,600L700,728L787,815Z" />
|
||||
</vector>
|
||||
@@ -0,0 +1,9 @@
|
||||
<vector xmlns:android="http://schemas.android.com/apk/res/android"
|
||||
android:width="24dp"
|
||||
android:height="24dp"
|
||||
android:viewportWidth="24"
|
||||
android:viewportHeight="24">
|
||||
<path
|
||||
android:fillColor="@android:color/white"
|
||||
android:pathData="M19,5v14H5V5h14m0,-2H5c-1.1,0 -2,0.9 -2,2v14c0,1.1 0.9,2 2,2h14c1.1,0 2,-0.9 2,-2V5c0,-1.1 -0.9,-2 -2,-2z" />
|
||||
</vector>
|
||||
@@ -0,0 +1,9 @@
|
||||
<vector xmlns:android="http://schemas.android.com/apk/res/android"
|
||||
android:width="24dp"
|
||||
android:height="24dp"
|
||||
android:viewportWidth="24"
|
||||
android:viewportHeight="24">
|
||||
<path
|
||||
android:fillColor="@android:color/white"
|
||||
android:pathData="M19,3L5,3c-1.11,0 -2,0.9 -2,2v14c0,1.1 0.89,2 2,2h14c1.11,0 2,-0.9 2,-2L21,5c0,-1.1 -0.89,-2 -2,-2zM10,17l-5,-5 1.41,-1.41L10,14.17l7.59,-7.59L19,8l-9,9z" />
|
||||
</vector>
|
||||
@@ -0,0 +1,9 @@
|
||||
<vector xmlns:android="http://schemas.android.com/apk/res/android"
|
||||
android:width="24dp"
|
||||
android:height="24dp"
|
||||
android:viewportWidth="960"
|
||||
android:viewportHeight="960">
|
||||
<path
|
||||
android:fillColor="@android:color/white"
|
||||
android:pathData="M612,668L668,612L520,464L520,280L440,280L440,496L612,668ZM480,880Q397,880 324,848.5Q251,817 197,763Q143,709 111.5,636Q80,563 80,480Q80,397 111.5,324Q143,251 197,197Q251,143 324,111.5Q397,80 480,80Q563,80 636,111.5Q709,143 763,197Q817,251 848.5,324Q880,397 880,480Q880,563 848.5,636Q817,709 763,763Q709,817 636,848.5Q563,880 480,880ZM480,480Q480,480 480,480Q480,480 480,480Q480,480 480,480Q480,480 480,480Q480,480 480,480Q480,480 480,480Q480,480 480,480Q480,480 480,480ZM480,800Q613,800 706.5,706.5Q800,613 800,480Q800,347 706.5,253.5Q613,160 480,160Q347,160 253.5,253.5Q160,347 160,480Q160,613 253.5,706.5Q347,800 480,800Z" />
|
||||
</vector>
|
||||
@@ -0,0 +1,9 @@
|
||||
<vector xmlns:android="http://schemas.android.com/apk/res/android"
|
||||
android:width="24dp"
|
||||
android:height="24dp"
|
||||
android:viewportWidth="24"
|
||||
android:viewportHeight="24">
|
||||
<path
|
||||
android:fillColor="@android:color/white"
|
||||
android:pathData="M19,6.41L17.59,5 12,10.59 6.41,5 5,6.41 10.59,12 5,17.59 6.41,19 12,13.41 17.59,19 19,17.59 13.41,12z" />
|
||||
</vector>
|
||||
@@ -0,0 +1,9 @@
|
||||
<vector xmlns:android="http://schemas.android.com/apk/res/android"
|
||||
android:width="24dp"
|
||||
android:height="24dp"
|
||||
android:viewportWidth="24"
|
||||
android:viewportHeight="24">
|
||||
<path
|
||||
android:fillColor="@android:color/white"
|
||||
android:pathData="M18,7l-1.41,-1.41 -6.34,6.34 1.41,1.41L18,7zM22.24,5.59L11.66,16.17 7.48,12l-1.41,1.41L11.66,19l12,-12 -1.42,-1.41zM0.41,13.41L6,19l1.41,-1.41L1.83,12 0.41,13.41z" />
|
||||
</vector>
|
||||
@@ -0,0 +1,9 @@
|
||||
<vector xmlns:android="http://schemas.android.com/apk/res/android"
|
||||
android:width="24dp"
|
||||
android:height="24dp"
|
||||
android:viewportWidth="512"
|
||||
android:viewportHeight="512">
|
||||
<path
|
||||
android:fillColor="@android:color/white"
|
||||
android:pathData="M495.3,425.74H255.18c-3.58,-52 -23.78,-100.69 -58.12,-140.07L401.37,81.37c6.52,-6.52 6.52,-17.09 0,-23.61c-6.52,-6.52 -17.09,-6.52 -23.61,0L162.23,273.29L4.89,430.63c-4.77,4.77 -6.21,11.96 -3.62,18.2s8.67,10.3 15.42,10.3h222.61h256c9.22,0 16.7,-7.47 16.7,-16.7C512,433.21 504.52,425.74 495.3,425.74z" />
|
||||
</vector>
|
||||
@@ -0,0 +1,10 @@
|
||||
<vector xmlns:android="http://schemas.android.com/apk/res/android"
|
||||
android:width="24dp"
|
||||
android:height="24dp"
|
||||
android:viewportWidth="24"
|
||||
android:viewportHeight="24">
|
||||
<path
|
||||
android:fillColor="@android:color/white"
|
||||
android:fillType="evenOdd"
|
||||
android:pathData="M1.854,1.146a0.5,0.5 0,1 0,-0.708 0.708l2,2A1.494,1.494 0,0 0,3 4.5L3,8a1.5,1.5 0,0 0,1.5 1.5h15A1.5,1.5 0,0 0,21 8L21,4.5c0,-0.232 -0.052,-0.45 -0.146,-0.647l2,-2a0.5,0.5 0,0 0,-0.708 -0.707l-2,2A1.494,1.494 0,0 0,19.5 3h-15c-0.232,0 -0.45,0.052 -0.647,0.146l-2,-2ZM7.5,8C8.328,8 9,7.216 9,6.25S8.328,4.5 7.5,4.5 6,5.284 6,6.25 6.672,8 7.5,8ZM18,6.25C18,7.216 17.328,8 16.5,8S15,7.216 15,6.25s0.672,-1.75 1.5,-1.75 1.5,0.784 1.5,1.75ZM4.5,10.5A1.5,1.5 0,0 0,3 12v9.5A1.5,1.5 0,0 0,4.5 23h15a1.5,1.5 0,0 0,1.5 -1.5L21,12a1.5,1.5 0,0 0,-1.5 -1.5h-15ZM8,16.75a4,4 0,1 1,8 0,4 4,0 0,1 -8,0ZM12,11.75a5,5 0,1 0,0 10,5 5,0 0,0 0,-10ZM12,18.5a1.75,1.75 0,1 0,-1.436 -2.75L9.437,15.75a2.751,2.751 0,1 1,0 2h1.127c0.316,0.453 0.841,0.75 1.436,0.75Z" />
|
||||
</vector>
|
||||
@@ -0,0 +1,9 @@
|
||||
<vector xmlns:android="http://schemas.android.com/apk/res/android"
|
||||
android:width="24dp"
|
||||
android:height="24dp"
|
||||
android:viewportWidth="24"
|
||||
android:viewportHeight="24">
|
||||
<path
|
||||
android:fillColor="@android:color/white"
|
||||
android:pathData="M10,18h4v-2h-4v2zM3,6v2h18L21,6L3,6zM6,13h12v-2L6,11v2z" />
|
||||
</vector>
|
||||
@@ -0,0 +1,9 @@
|
||||
<vector xmlns:android="http://schemas.android.com/apk/res/android"
|
||||
android:width="24dp"
|
||||
android:height="24dp"
|
||||
android:viewportWidth="24"
|
||||
android:viewportHeight="24">
|
||||
<path
|
||||
android:fillColor="@android:color/white"
|
||||
android:pathData="M12,0c-6.626,0 -12,5.373 -12,12 0,5.302 3.438,9.8 8.207,11.387 0.599,0.111 0.793,-0.261 0.793,-0.577v-2.234c-3.338,0.726 -4.033,-1.416 -4.033,-1.416 -0.546,-1.387 -1.333,-1.756 -1.333,-1.756 -1.089,-0.745 0.083,-0.729 0.083,-0.729 1.205,0.084 1.839,1.237 1.839,1.237 1.07,1.834 2.807,1.304 3.492,0.997 0.107,-0.775 0.418,-1.305 0.762,-1.604 -2.665,-0.305 -5.467,-1.334 -5.467,-5.931 0,-1.311 0.469,-2.381 1.236,-3.221 -0.124,-0.303 -0.535,-1.524 0.117,-3.176 0,0 1.008,-0.322 3.301,1.23 0.957,-0.266 1.983,-0.399 3.003,-0.404 1.02,0.005 2.047,0.138 3.006,0.404 2.291,-1.552 3.297,-1.23 3.297,-1.23 0.653,1.653 0.242,2.874 0.118,3.176 0.77,0.84 1.235,1.911 1.235,3.221 0,4.609 -2.807,5.624 -5.479,5.921 0.43,0.372 0.823,1.102 0.823,2.222v3.293c0,0.319 0.192,0.694 0.801,0.576 4.765,-1.589 8.199,-6.086 8.199,-11.386 0,-6.627 -5.373,-12 -12,-12z" />
|
||||
</vector>
|
||||
@@ -0,0 +1,7 @@
|
||||
<shape xmlns:android="http://schemas.android.com/apk/res/android"
|
||||
android:shape="rectangle">
|
||||
<gradient
|
||||
android:angle="90"
|
||||
android:endColor="#2A2A2A"
|
||||
android:startColor="#000000" />
|
||||
</shape>
|
||||
@@ -0,0 +1,25 @@
|
||||
<vector xmlns:android="http://schemas.android.com/apk/res/android"
|
||||
android:width="108dp"
|
||||
android:height="108dp"
|
||||
android:viewportWidth="271.24"
|
||||
android:viewportHeight="271.24">
|
||||
<group android:scaleX="0.92"
|
||||
android:scaleY="0.92"
|
||||
android:translateX="10.8496"
|
||||
android:translateY="10.8496">
|
||||
<group
|
||||
android:translateX="1.3562195"
|
||||
android:translateY="1.3562195">
|
||||
<group
|
||||
android:translateX="70.26573"
|
||||
android:translateY="70.26573">
|
||||
<path
|
||||
android:fillColor="@color/accent"
|
||||
android:pathData="m96.64,31.5c7.91,8 17.14,13.58 25.36,15.58l1.75,-4C123.78,43.05 123.79,43.01 123.8,42.97 125.83,38.99 127,34.49 127,29.72 127,13.88 114.38,1 98.87,1c-5.13,0 -9.92,1.43 -14.06,3.88l-3.4,1.6c0.08,0.31 0.13,0.6 0.22,0.91 2.32,8.1 7.65,16.67 15.02,24.11z" />
|
||||
<path
|
||||
android:fillColor="@color/accent"
|
||||
android:pathData="M93.88,34.3C86.05,26.39 80.37,17.23 77.87,8.49 77.84,8.39 77.82,8.28 77.79,8.18L2.63,43.51c-1.4,0.66 -2.02,2.36 -1.37,3.79 0.47,1.05 1.49,1.66 2.55,1.66 0.39,0 0.79,-0.08 1.17,-0.26L74.04,16.24c-1.87,3.58 -3,7.61 -3.23,11.89L35.57,51.73c-1.29,0.87 -1.65,2.64 -0.81,3.96 0.54,0.84 1.43,1.29 2.34,1.29 0.53,0 1.06,-0.15 1.53,-0.47L71.19,34.71c1.4,8.11 6.14,15.04 12.72,19.28l-54.94,68.34c-0.98,1.22 -0.81,3.02 0.39,4.02 0.52,0.44 1.15,0.65 1.77,0.65 0.81,0 1.61,-0.36 2.17,-1.04L89.06,56.6c3.06,1.17 6.36,1.84 9.81,1.84 4.93,0 9.57,-1.31 13.6,-3.6L84.6,118.32c-0.63,1.44 0,3.13 1.41,3.78 0.37,0.17 0.76,0.25 1.14,0.25 1.07,0 2.09,-0.63 2.56,-1.69L120.4,50.74C111.64,48.42 102.04,42.56 93.88,34.3Z" />
|
||||
</group>
|
||||
</group>
|
||||
</group>
|
||||
</vector>
|
||||
@@ -0,0 +1,25 @@
|
||||
<vector xmlns:android="http://schemas.android.com/apk/res/android"
|
||||
android:width="108dp"
|
||||
android:height="108dp"
|
||||
android:viewportWidth="271.24"
|
||||
android:viewportHeight="271.24">
|
||||
<group android:scaleX="0.92"
|
||||
android:scaleY="0.92"
|
||||
android:translateX="10.8496"
|
||||
android:translateY="10.8496">
|
||||
<group
|
||||
android:translateX="1.3562195"
|
||||
android:translateY="1.3562195">
|
||||
<group
|
||||
android:translateX="70.26573"
|
||||
android:translateY="70.26573">
|
||||
<path
|
||||
android:fillColor="@color/accent"
|
||||
android:pathData="m96.64,31.5c7.91,8 17.14,13.58 25.36,15.58l1.75,-4C123.78,43.05 123.79,43.01 123.8,42.97 125.83,38.99 127,34.49 127,29.72 127,13.88 114.38,1 98.87,1c-5.13,0 -9.92,1.43 -14.06,3.88l-3.4,1.6c0.08,0.31 0.13,0.6 0.22,0.91 2.32,8.1 7.65,16.67 15.02,24.11z" />
|
||||
<path
|
||||
android:fillColor="@color/accent"
|
||||
android:pathData="M93.88,34.3C86.05,26.39 80.37,17.23 77.87,8.49 77.84,8.39 77.82,8.28 77.79,8.18L2.63,43.51c-1.4,0.66 -2.02,2.36 -1.37,3.79 0.47,1.05 1.49,1.66 2.55,1.66 0.39,0 0.79,-0.08 1.17,-0.26L74.04,16.24c-1.87,3.58 -3,7.61 -3.23,11.89L35.57,51.73c-1.29,0.87 -1.65,2.64 -0.81,3.96 0.54,0.84 1.43,1.29 2.34,1.29 0.53,0 1.06,-0.15 1.53,-0.47L71.19,34.71c1.4,8.11 6.14,15.04 12.72,19.28l-54.94,68.34c-0.98,1.22 -0.81,3.02 0.39,4.02 0.52,0.44 1.15,0.65 1.77,0.65 0.81,0 1.61,-0.36 2.17,-1.04L89.06,56.6c3.06,1.17 6.36,1.84 9.81,1.84 4.93,0 9.57,-1.31 13.6,-3.6L84.6,118.32c-0.63,1.44 0,3.13 1.41,3.78 0.37,0.17 0.76,0.25 1.14,0.25 1.07,0 2.09,-0.63 2.56,-1.69L120.4,50.74C111.64,48.42 102.04,42.56 93.88,34.3Z" />
|
||||
</group>
|
||||
</group>
|
||||
</group>
|
||||
</vector>
|
||||
@@ -0,0 +1,9 @@
|
||||
<vector xmlns:android="http://schemas.android.com/apk/res/android"
|
||||
android:width="24dp"
|
||||
android:height="24dp"
|
||||
android:viewportWidth="960"
|
||||
android:viewportHeight="960">
|
||||
<path
|
||||
android:fillColor="@android:color/white"
|
||||
android:pathData="M480,520Q430,520 395,485Q360,450 360,400Q360,350 395,315Q430,280 480,280Q530,280 565,315Q600,350 600,400Q600,450 565,485Q530,520 480,520ZM240,920L240,611Q202,569 181,515Q160,461 160,400Q160,266 253,173Q346,80 480,80Q614,80 707,173Q800,266 800,400Q800,461 779,515Q758,569 720,611L720,920L480,840L240,920ZM480,640Q580,640 650,570Q720,500 720,400Q720,300 650,230Q580,160 480,160Q380,160 310,230Q240,300 240,400Q240,500 310,570Q380,640 480,640ZM320,801L480,760L640,801L640,677Q605,697 564.5,708.5Q524,720 480,720Q436,720 395.5,708.5Q355,697 320,677L320,801ZM480,739L480,739Q480,739 480,739Q480,739 480,739Q480,739 480,739Q480,739 480,739L480,739L480,739Z" />
|
||||
</vector>
|
||||
@@ -0,0 +1,9 @@
|
||||
<vector xmlns:android="http://schemas.android.com/apk/res/android"
|
||||
android:width="24dp"
|
||||
android:height="24dp"
|
||||
android:viewportWidth="960"
|
||||
android:viewportHeight="960">
|
||||
<path
|
||||
android:fillColor="@android:color/white"
|
||||
android:pathData="M600,840L360,756L174,828Q154,836 137,823.5Q120,811 120,790L120,230Q120,217 127.5,207Q135,197 148,192L360,120L600,204L786,132Q806,124 823,136.5Q840,149 840,170L840,730Q840,743 832.5,753Q825,763 812,768L600,840ZM560,742L560,274L400,218L400,686L560,742ZM640,742L760,702L760,228L640,274L640,742ZM200,732L320,686L320,218L200,258L200,732ZM640,274L640,274L640,742L640,742L640,274ZM320,218L320,218L320,686L320,686L320,218Z" />
|
||||
</vector>
|
||||
@@ -0,0 +1,9 @@
|
||||
<vector xmlns:android="http://schemas.android.com/apk/res/android"
|
||||
android:width="24dp"
|
||||
android:height="24dp"
|
||||
android:viewportWidth="960"
|
||||
android:viewportHeight="960">
|
||||
<path
|
||||
android:fillColor="@android:color/white"
|
||||
android:pathData="M80,800L80,640L240,640L240,800L80,800ZM320,800L320,640L880,640L880,800L320,800ZM80,560L80,400L240,400L240,560L80,560ZM320,560L320,400L880,400L880,560L320,560ZM80,320L80,160L240,160L240,320L80,320ZM320,320L320,160L880,160L880,320L320,320Z" />
|
||||
</vector>
|
||||
@@ -0,0 +1,9 @@
|
||||
<vector xmlns:android="http://schemas.android.com/apk/res/android"
|
||||
android:width="24dp"
|
||||
android:height="24dp"
|
||||
android:viewportWidth="960"
|
||||
android:viewportHeight="960">
|
||||
<path
|
||||
android:fillColor="@android:color/white"
|
||||
android:pathData="M480,880Q341,845 250.5,720.5Q160,596 160,444L160,200L480,80L800,200L800,444Q800,529 771,607.5Q742,686 688,746L560,618Q542,629 521.5,634.5Q501,640 480,640Q414,640 367,593Q320,546 320,480Q320,414 367,367Q414,320 480,320Q546,320 593,367Q640,414 640,480Q640,502 634.5,522.5Q629,543 618,562L678,622Q698,581 709,536Q720,491 720,444L720,255L480,165L240,255L240,444Q240,565 308,664Q376,763 480,796Q506,788 529.5,775.5Q553,763 576,746L632,802Q599,829 560.5,849Q522,869 480,880ZM480,560Q513,560 536.5,536.5Q560,513 560,480Q560,447 536.5,423.5Q513,400 480,400Q447,400 423.5,423.5Q400,447 400,480Q400,513 423.5,536.5Q447,560 480,560ZM488,483L488,483Q488,483 488,483Q488,483 488,483L488,483Q488,483 488,483Q488,483 488,483L488,483L488,483L488,483L488,483Q488,483 488,483Q488,483 488,483Q488,483 488,483Q488,483 488,483Z" />
|
||||
</vector>
|
||||
@@ -0,0 +1,9 @@
|
||||
<vector xmlns:android="http://schemas.android.com/apk/res/android"
|
||||
android:width="24dp"
|
||||
android:height="24dp"
|
||||
android:viewportWidth="24"
|
||||
android:viewportHeight="24">
|
||||
<path
|
||||
android:fillColor="@color/accent"
|
||||
android:pathData="M12,2C8.13,2 5,5.13 5,9c0,5.25 7,13 7,13s7,-7.75 7,-13c0,-3.87 -3.13,-7 -7,-7zM12,11.5c-1.38,0 -2.5,-1.12 -2.5,-2.5s1.12,-2.5 2.5,-2.5 2.5,1.12 2.5,2.5 -1.12,2.5 -2.5,2.5z" />
|
||||
</vector>
|
||||
@@ -0,0 +1,9 @@
|
||||
<vector xmlns:android="http://schemas.android.com/apk/res/android"
|
||||
android:width="24dp"
|
||||
android:height="24dp"
|
||||
android:viewportWidth="24"
|
||||
android:viewportHeight="24">
|
||||
<path
|
||||
android:fillColor="@android:color/white"
|
||||
android:pathData="M14,21c1.93,0 3.62,-1.17 4,-3l-1.75,-0.88C16,18.21 15.33,19 14,19l-4.9,0c0.83,-1 1.5,-2.34 1.5,-4c0,-0.35 -0.03,-0.69 -0.08,-1L14,14v-2l-4.18,0C9,10.42 8,9.6 8,8c0,-1.93 1.57,-3.5 3.5,-3.5c1.5,0 2.79,0.95 3.28,2.28L16.63,6c-0.8,-2.05 -2.79,-3.5 -5.13,-3.5C8.46,2.5 6,4.96 6,8c0,1.78 0.79,2.9 1.49,4L6,12v2l2.47,0c0.08,0.31 0.13,0.64 0.13,1c0,2.7 -2.6,4 -2.6,4v2H14z" />
|
||||
</vector>
|
||||
@@ -0,0 +1,9 @@
|
||||
<vector xmlns:android="http://schemas.android.com/apk/res/android"
|
||||
android:width="24dp"
|
||||
android:height="24dp"
|
||||
android:viewportWidth="960"
|
||||
android:viewportHeight="960">
|
||||
<path
|
||||
android:fillColor="@android:color/white"
|
||||
android:pathData="M480,880Q397,880 324,848.5Q251,817 197,763Q143,709 111.5,636Q80,563 80,480Q80,397 111.5,324Q143,251 197,197Q251,143 324,111.5Q397,80 480,80L520,80L520,411Q538,422 549,439.5Q560,457 560,480Q560,513 536.5,536.5Q513,560 480,560Q447,560 423.5,536.5Q400,513 400,480Q400,457 411,439Q422,421 440,411L440,325Q388,339 354,381.5Q320,424 320,480Q320,546 367,593Q414,640 480,640Q546,640 593,593Q640,546 640,480Q640,444 625.5,413.5Q611,383 586,360L643,303Q678,336 699,381.5Q720,427 720,480Q720,580 650,650Q580,720 480,720Q380,720 310,650Q240,580 240,480Q240,390 297,323.5Q354,257 440,243L440,162Q321,177 240.5,267Q160,357 160,480Q160,614 253,707Q346,800 480,800Q614,800 707,707Q800,614 800,480Q800,411 773,351Q746,291 699,247L756,190Q813,245 846.5,319.5Q880,394 880,480Q880,563 848.5,636Q817,709 763,763Q709,817 636,848.5Q563,880 480,880Z" />
|
||||
</vector>
|
||||
@@ -0,0 +1,9 @@
|
||||
<vector xmlns:android="http://schemas.android.com/apk/res/android"
|
||||
android:width="24dp"
|
||||
android:height="24dp"
|
||||
android:viewportWidth="960"
|
||||
android:viewportHeight="960">
|
||||
<path
|
||||
android:fillColor="@android:color/white"
|
||||
android:pathData="M198,682Q141,625 110.5,552.5Q80,480 80,400Q80,320 110.5,247.5Q141,175 198,118L246,166Q199,213 173.5,273.5Q148,334 148,400Q148,466 173.5,526.5Q199,587 246,634L198,682ZM290,590Q252,552 232,503Q212,454 212,400Q212,346 232,297Q252,248 290,210L338,258Q309,287 294.5,323.5Q280,360 280,400Q280,440 294.5,476.5Q309,513 338,542L290,590ZM440,840L440,492Q413,480 396.5,455Q380,430 380,400Q380,358 409,329Q438,300 480,300Q522,300 551,329Q580,358 580,400Q580,430 563.5,455Q547,480 520,492L520,840L440,840ZM670,590L622,542Q651,513 665.5,476.5Q680,440 680,400Q680,360 665.5,323.5Q651,287 622,258L670,210Q708,248 728,297Q748,346 748,400Q748,454 728,503Q708,552 670,590ZM762,682L714,634Q761,587 786.5,526.5Q812,466 812,400Q812,334 786.5,273.5Q761,213 714,166L762,118Q819,175 849.5,247.5Q880,320 880,400Q880,480 849.5,552.5Q819,625 762,682Z" />
|
||||
</vector>
|
||||
@@ -0,0 +1,12 @@
|
||||
<vector xmlns:android="http://schemas.android.com/apk/res/android"
|
||||
android:width="24dp"
|
||||
android:height="24dp"
|
||||
android:viewportWidth="128"
|
||||
android:viewportHeight="128">
|
||||
<path
|
||||
android:fillColor="@android:color/white"
|
||||
android:pathData="m96.64,31.5c7.91,8 17.14,13.58 25.37,15.58l1.75,-4c0.02,-0.04 0.02,-0.08 0.04,-0.12 2.03,-3.97 3.2,-8.47 3.2,-13.25 0,-15.83 -12.62,-28.71 -28.13,-28.71 -5.13,0 -9.92,1.43 -14.06,3.88l-3.4,1.6c0.08,0.31 0.13,0.6 0.22,0.91 2.32,8.1 7.65,16.67 15.02,24.11z" />
|
||||
<path
|
||||
android:fillColor="@android:color/white"
|
||||
android:pathData="m93.88,34.3c-7.82,-7.91 -13.51,-17.07 -16.01,-25.81 -0.03,-0.1 -0.05,-0.2 -0.08,-0.31l-75.16,35.33c-1.4,0.66 -2.02,2.36 -1.37,3.79 0.47,1.05 1.49,1.66 2.55,1.66 0.39,0 0.79,-0.08 1.17,-0.26l69.07,-32.47c-1.87,3.58 -3,7.61 -3.23,11.88l-35.25,23.61c-1.29,0.87 -1.65,2.64 -0.8,3.96 0.54,0.84 1.43,1.29 2.34,1.29 0.53,0 1.06,-0.15 1.53,-0.47l32.55,-21.8c1.4,8.11 6.13,15.04 12.71,19.28l-54.94,68.33c-0.98,1.22 -0.81,3.02 0.39,4.02 0.52,0.44 1.15,0.65 1.78,0.65 0.81,0 1.61,-0.36 2.17,-1.04l55.77,-69.36c3.06,1.17 6.36,1.84 9.81,1.84 4.93,0 9.57,-1.31 13.6,-3.6l-27.87,63.48c-0.63,1.44 0,3.13 1.41,3.78 0.37,0.17 0.76,0.25 1.14,0.25 1.07,0 2.09,-0.63 2.56,-1.69l30.69,-69.92c-8.76,-2.32 -18.36,-8.18 -26.53,-16.44z" />
|
||||
</vector>
|
||||
@@ -0,0 +1,9 @@
|
||||
<vector xmlns:android="http://schemas.android.com/apk/res/android"
|
||||
android:width="24dp"
|
||||
android:height="24dp"
|
||||
android:viewportWidth="24"
|
||||
android:viewportHeight="24">
|
||||
<path
|
||||
android:fillColor="@android:color/white"
|
||||
android:pathData="M15.5,14h-0.79l-0.28,-0.27C15.41,12.59 16,11.11 16,9.5 16,5.91 13.09,3 9.5,3S3,5.91 3,9.5 5.91,16 9.5,16c1.61,0 3.09,-0.59 4.23,-1.57l0.27,0.28v0.79l5,4.99L20.49,19l-4.99,-5zM9.5,14C7.01,14 5,11.99 5,9.5S7.01,5 9.5,5 14,7.01 14,9.5 11.99,14 9.5,14z" />
|
||||
</vector>
|
||||
@@ -0,0 +1,9 @@
|
||||
<vector xmlns:android="http://schemas.android.com/apk/res/android"
|
||||
android:width="24dp"
|
||||
android:height="24dp"
|
||||
android:viewportWidth="960"
|
||||
android:viewportHeight="960">
|
||||
<path
|
||||
android:fillColor="@android:color/white"
|
||||
android:pathData="M370,880L354,752Q341,747 329.5,740Q318,733 307,725L188,775L78,585L181,507Q180,500 180,493.5Q180,487 180,480Q180,473 180,466.5Q180,460 181,453L78,375L188,185L307,235Q318,227 330,220Q342,213 354,208L370,80L590,80L606,208Q619,213 630.5,220Q642,227 653,235L772,185L882,375L779,453Q780,460 780,466.5Q780,473 780,480Q780,487 780,493.5Q780,500 778,507L881,585L771,775L653,725Q642,733 630,740Q618,747 606,752L590,880L370,880ZM440,800L519,800L533,694Q564,686 590.5,670.5Q617,655 639,633L738,674L777,606L691,541Q696,527 698,511.5Q700,496 700,480Q700,464 698,448.5Q696,433 691,419L777,354L738,286L639,328Q617,305 590.5,289.5Q564,274 533,266L520,160L441,160L427,266Q396,274 369.5,289.5Q343,305 321,327L222,286L183,354L269,418Q264,433 262,448Q260,463 260,480Q260,496 262,511Q264,526 269,541L183,606L222,674L321,632Q343,655 369.5,670.5Q396,686 427,694L440,800ZM482,620Q540,620 581,579Q622,538 622,480Q622,422 581,381Q540,340 482,340Q423,340 382.5,381Q342,422 342,480Q342,538 382.5,579Q423,620 482,620ZM480,480L480,480Q480,480 480,480Q480,480 480,480L480,480L480,480L480,480Q480,480 480,480Q480,480 480,480Q480,480 480,480Q480,480 480,480L480,480L480,480L480,480Q480,480 480,480Q480,480 480,480L480,480L480,480L480,480Q480,480 480,480Q480,480 480,480L480,480L480,480L480,480Q480,480 480,480Q480,480 480,480Q480,480 480,480Q480,480 480,480L480,480L480,480L480,480Q480,480 480,480Q480,480 480,480L480,480Z" />
|
||||
</vector>
|
||||
@@ -0,0 +1,9 @@
|
||||
<vector xmlns:android="http://schemas.android.com/apk/res/android"
|
||||
android:width="24dp"
|
||||
android:height="24dp"
|
||||
android:viewportWidth="960"
|
||||
android:viewportHeight="960">
|
||||
<path
|
||||
android:fillColor="@android:color/white"
|
||||
android:pathData="M240,800L280,640L120,640L140,560L300,560L340,400L180,400L200,320L360,320L400,160L480,160L440,320L600,320L640,160L720,160L680,320L840,320L820,400L660,400L620,560L780,560L760,640L600,640L560,800L480,800L520,640L360,640L320,800L240,800ZM380,560L540,560L580,400L420,400L380,560Z" />
|
||||
</vector>
|
||||
@@ -0,0 +1,6 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<adaptive-icon xmlns:android="http://schemas.android.com/apk/res/android">
|
||||
<background android:drawable="@drawable/ic_launcher_bg" />
|
||||
<foreground android:drawable="@drawable/ic_launcher_fg" />
|
||||
<monochrome android:drawable="@drawable/ic_launcher_mc" />
|
||||
</adaptive-icon>
|
||||
@@ -0,0 +1,6 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<adaptive-icon xmlns:android="http://schemas.android.com/apk/res/android">
|
||||
<background android:drawable="@drawable/ic_launcher_bg" />
|
||||
<foreground android:drawable="@drawable/ic_launcher_fg" />
|
||||
<monochrome android:drawable="@drawable/ic_launcher_mc" />
|
||||
</adaptive-icon>
|
||||
Binary file not shown.
|
After Width: | Height: | Size: 4.5 KiB |
Binary file not shown.
|
After Width: | Height: | Size: 8.3 KiB |
@@ -0,0 +1,147 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<resources>
|
||||
<!-- Commmon -->
|
||||
<string name="btn_accept">Aceptar</string>
|
||||
<string name="btn_cancel">Cancelar</string>
|
||||
<string name="empty_list_message">No hay datos para mostrar</string>
|
||||
|
||||
<!-- Navigation -->
|
||||
<string name="nav_sat">Satélites</string>
|
||||
<string name="nav_pass">Passes</string>
|
||||
<string name="nav_radar">Radar</string>
|
||||
<string name="nav_map">Mapa</string>
|
||||
<string name="nav_prefs">Ajustes</string>
|
||||
|
||||
<!-- Satellites screen -->
|
||||
<string name="sat_type_hint">Tipo: %s</string>
|
||||
<string name="sat_type_title">Seleccionar tipo de satélite</string>
|
||||
<string name="sat_search_hint">Id - Nombre</string>
|
||||
<string name="sat_search_clear">Vaciar</string>
|
||||
<string name="sat_clear_all">Vaciar todo</string>
|
||||
<string name="sat_select_all">Seleccionar todo</string>
|
||||
<string name="sat_empty_list_message">
|
||||
Asegúrese de que su consulta de búsqueda sea correcta y que la base de datos esté actualizada</string>
|
||||
<string name="sat_warning_title">Alarma\!</string>
|
||||
<string name="sat_warning_message">
|
||||
Esta aplicación incluye más de 9000 satélites.
|
||||
No tiene sentido rastrearlos todos a la vez.
|
||||
\n\nIntenta siempre limitar la lista a los que te
|
||||
interesen mediante la búsqueda y el selector de tipos.</string>
|
||||
|
||||
<!-- Passes screen -->
|
||||
<string name="pass_filter_title">Filtrar pases</string>
|
||||
<string name="pass_filter_elev">Elevación mínima</string>
|
||||
<string name="pass_filter_hours">Horas por delante</string>
|
||||
<string name="pass_modes_title">Tipo de modulación</string>
|
||||
<string name="pass_elevation">Elevación: %.1f°</string>
|
||||
<string name="pass_altitude">Altitud: %.0f km</string>
|
||||
<string name="pass_empty_list_message">
|
||||
Asegúrate de que tus filtros sean correctos y hayas seleccionado satélites</string>
|
||||
<string name="pass_error_message">No hay pases de satélite próximos para mostrar</string>
|
||||
<string name="pass_welcome_title">Bienvenido a Look4Sat\!</string>
|
||||
<string name="pass_welcome_message">
|
||||
Asegúrate de configurar tu posición mediante GPS, Latitud/Longitud o QTH en Configuración.
|
||||
\n\nActualiza la base de datos al menos una vez por semana para obtener predicciones precisas.</string>
|
||||
|
||||
<!-- Radar screen -->
|
||||
<string name="radar_back">Atrás</string>
|
||||
<string name="radar_notify">Notificar</string>
|
||||
<string name="radar_az_text">Ácimut</string>
|
||||
<string name="radar_el_text">Elevación</string>
|
||||
<string name="radar_alt_text">Altitud</string>
|
||||
<string name="radar_dist_text">Distancia</string>
|
||||
<string name="radar_eclipsed">Eclipsado</string>
|
||||
<string name="radar_downlink">Enlace descendente</string>
|
||||
<string name="radar_uplink">Enlace ascendente</string>
|
||||
<string name="radar_no_data">Este satélite no lleva transmisores</string>
|
||||
<string name="radar_add">Añadir</string>
|
||||
<string name="radar_data">Transmisores satélite:</string>
|
||||
<string name="radar_inverted">Invertido: %s</string>
|
||||
<string name="radar_mode">Modo: %s</string>
|
||||
<string name="radar_string_no">No</string>
|
||||
<string name="radar_string_yes">Si</string>
|
||||
<string name="radar_visible">Visible</string>
|
||||
|
||||
<!-- Map screen -->
|
||||
<string name="map_prev">Anterior</string>
|
||||
<string name="map_next">Siguiente</string>
|
||||
<string name="map_azimuth">Ácimut: %.1f°</string>
|
||||
<string name="map_elevation">Elevación: %.1f°</string>
|
||||
<string name="map_altitude">Altitud: %.0f km</string>
|
||||
<string name="map_distance">Distancia: %.0f km</string>
|
||||
<string name="map_latitude">Latitud: %.1f°</string>
|
||||
<string name="map_longitude">Longitud: %.1f°</string>
|
||||
<string name="map_phase">Fase: %.1f°</string>
|
||||
<string name="map_eclipsed">Eclipsado</string>
|
||||
<string name="map_period">Período: %.0f min</string>
|
||||
<string name="map_velocity">Velocidad: %.2f km/s</string>
|
||||
<string name="map_visibility">Visibilidad: %s</string>
|
||||
<string name="map_visible">Visible</string>
|
||||
|
||||
<!-- Settings screen -->
|
||||
<string name="prefs_donate_title">Soporte</string>
|
||||
<string name="prefs_privacy_title">Política de privacidad</string>
|
||||
|
||||
<string name="prefs_updated_title">Actualizado: %s</string>
|
||||
<string name="prefs_updated_time">d MMM yyyy - HH:mm:ss</string>
|
||||
|
||||
<string name="prefs_loc_title">Ubicación estación</string>
|
||||
<string name="prefs_loc_gps_title">GPS</string>
|
||||
<string name="prefs_loc_gps_error">Revisa los permisos de ubicación</string>
|
||||
<string name="prefs_loc_input_title">Ingresar</string>
|
||||
<string name="prefs_loc_input_error">Ubicación no válida</string>
|
||||
<string name="prefs_loc_qth_title">QTH</string>
|
||||
<string name="prefs_loc_qth_error">Ubicación QTH no válida</string>
|
||||
<string name="prefs_loc_success">Ubicación actualizada con éxito</string>
|
||||
<string name="prefs_station_title">Ajustar ubicaicón de la estación</string>
|
||||
<string name="prefs_station_lat_text">Latitud de tu estación terrestre</string>
|
||||
<string name="prefs_station_lon_text">Longitud de tu estación terrestre</string>
|
||||
<string name="prefs_locator_title">Ajustes ubicación QTH</string>
|
||||
<string name="prefs_locator_text">Ajustar posición de la estación con tu ubicación</string>
|
||||
|
||||
<string name="prefs_data_title">Datos satelitales</string>
|
||||
<string name="prefs_data_entries">Satélites: %s</string>
|
||||
<string name="prefs_data_radios">Transmisores: %s</string>
|
||||
<string name="prefs_data_update">Actualizar</string>
|
||||
<string name="prefs_data_import">Importar</string>
|
||||
<string name="prefs_data_clear">Vaciar</string>
|
||||
<string name="prefs_data_clear_success">Datos eliminados con éxito</string>
|
||||
<string name="prefs_data_update_success">Actualización completada con éxito</string>
|
||||
|
||||
<string name="prefs_data_sources_title">Custom data sources</string>
|
||||
<string name="prefs_data_sources_tle_switch">Custom TLE URL</string>
|
||||
<string name="prefs_data_sources_transceivers_switch">Custom transceivers URL</string>
|
||||
|
||||
<string name="prefs_data_output_title">Data output</string>
|
||||
<string name="prefs_net_output">Network</string>
|
||||
<string name="prefs_bt_output">Bluetooth</string>
|
||||
|
||||
<string name="prefs_net_title">Salida de datos de red</string>
|
||||
<string name="prefs_net_rotator_switch">Enable rotation output</string>
|
||||
<string name="prefs_net_rotator_ip_hint">Dirección IP</string>
|
||||
<string name="prefs_net_rotator_port_hint">Puerto</string>
|
||||
<string name="prefs_net_rotator_format_hint">Formato de datos</string>
|
||||
<string name="prefs_net_frequency_switch">Enable frequency output</string>
|
||||
<string name="prefs_net_frequency_ip_hint">Dirección IP</string>
|
||||
<string name="prefs_net_frequency_port_hint">Puerto</string>
|
||||
<string name="prefs_net_frequency_format_hint">Formato de datos</string>
|
||||
|
||||
<string name="prefs_bt_title">Salida por Bluetooth</string>
|
||||
<string name="prefs_bt_rotator_switch">Enable rotation output</string>
|
||||
<string name="prefs_bt_rotator_device_hint">Id dispositivo</string>
|
||||
<string name="prefs_bt_rotator_output_hint">Formato de datos</string>
|
||||
<string name="prefs_bt_frequency_switch">Enable frequency output</string>
|
||||
<string name="prefs_bt_frequency_device_hint">Id dispositivo</string>
|
||||
<string name="prefs_bt_frequency_output_hint">Formato de datos</string>
|
||||
<string name="prefs_bt_perm_error">Revisa los permisos de Bluetooth</string>
|
||||
|
||||
<string name="prefs_other_title">Otros ajustes</string>
|
||||
<string name="prefs_other_switch_utc">Mostrar tiempos de pase en UTC</string>
|
||||
<string name="prefs_other_switch_update">Habilitar auto actualización de datos</string>
|
||||
<string name="prefs_other_switch_sweep">Habilitar animaciones radar</string>
|
||||
<string name="prefs_other_switch_sensors">Usar sensores para rotar vista de radar</string>
|
||||
|
||||
<string name="prefs_outro_title">Me gustaría dar las gracias a:</string>
|
||||
<string name="prefs_outro_license">La app viene sin garantías de ningún tipo.</string>
|
||||
|
||||
</resources>
|
||||
@@ -0,0 +1,146 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<resources>
|
||||
<!-- Commmon -->
|
||||
<string name="btn_accept">Принять</string>
|
||||
<string name="btn_cancel">Отменить</string>
|
||||
<string name="empty_list_message">Нет данных для отображения</string>
|
||||
|
||||
<!-- Navigation -->
|
||||
<string name="nav_sat">Спутники</string>
|
||||
<string name="nav_pass">Пролеты</string>
|
||||
<string name="nav_radar">Радар</string>
|
||||
<string name="nav_map">Карта</string>
|
||||
<string name="nav_prefs">Настройки</string>
|
||||
|
||||
<!-- Satellites screen -->
|
||||
<string name="sat_type_hint">Тип: %s</string>
|
||||
<string name="sat_type_title">Выберите тип спутника</string>
|
||||
<string name="sat_search_hint">Id - Название</string>
|
||||
<string name="sat_search_clear">Очистить</string>
|
||||
<string name="sat_clear_all">Очистить</string>
|
||||
<string name="sat_select_all">Выбрать</string>
|
||||
<string name="sat_empty_list_message">Убедитесь, что ваш поисковый запрос верен и база данных обновлена</string>
|
||||
<string name="sat_warning_title">Внимание\!</string>
|
||||
<string name="sat_warning_message">
|
||||
В этом приложении перечислено более 9000 спутников.
|
||||
Отслеживать их все одновременно бессмысленно.
|
||||
\n\nВсегда старайтесь сузить список до тех,
|
||||
которые вас интересуют, используя поиск и селектор типов.</string>
|
||||
|
||||
<!-- Passes screen -->
|
||||
<string name="pass_filter_title">Фильтровать пролеты</string>
|
||||
<string name="pass_filter_elev">Минимальная элевация</string>
|
||||
<string name="pass_filter_hours">Количество часов</string>
|
||||
<string name="pass_modes_title">Выберите тип модуляции</string>
|
||||
<string name="pass_elevation">Элевация: %.1f°</string>
|
||||
<string name="pass_altitude">Высота: %d км</string>
|
||||
<string name="pass_empty_list_message">
|
||||
Убедитесь, что ваши фильтры настроены правильно и вы выбрали спутники</string>
|
||||
<string name="pass_error_message">Нет предстоящих пролётов спутников</string>
|
||||
<string name="pass_welcome_title">Добро пожаловать в Look4Sat\!</string>
|
||||
<string name="pass_welcome_message">
|
||||
Обязательно укажите свое местоположение по GPS, широте/долготе или QTH в настройках.
|
||||
\n\nПожалуйста, обновляйте базу данных как минимум раз в неделю, чтобы получать точные прогнозы.</string>
|
||||
|
||||
<!-- Radar screen -->
|
||||
<string name="radar_back">Назад</string>
|
||||
<string name="radar_notify">Уведомить</string>
|
||||
<string name="radar_az_text">Азим.</string>
|
||||
<string name="radar_el_text">Элев.</string>
|
||||
<string name="radar_alt_text">Выс.</string>
|
||||
<string name="radar_dist_text">Дальн.</string>
|
||||
<string name="radar_eclipsed">В тени</string>
|
||||
<string name="radar_downlink">Нисходящий канал</string>
|
||||
<string name="radar_uplink">Восходящий канал</string>
|
||||
<string name="radar_no_data">У этого спутника нет трансиверов</string>
|
||||
<string name="radar_add">Добавить</string>
|
||||
<string name="radar_data">Трансиверы спутника:</string>
|
||||
<string name="radar_inverted">Инверсия: %s</string>
|
||||
<string name="radar_mode">Модуляция: %s</string>
|
||||
<string name="radar_string_no">Нет</string>
|
||||
<string name="radar_string_yes">Да</string>
|
||||
<string name="radar_visible">Видимый</string>
|
||||
|
||||
<!-- Map screen -->
|
||||
<string name="map_prev">Предыдущий</string>
|
||||
<string name="map_next">Следующий</string>
|
||||
<string name="map_azimuth">Азимут: %.1f°</string>
|
||||
<string name="map_elevation">Элевация: %.1f°</string>
|
||||
<string name="map_altitude">Высота: %.0fкм</string>
|
||||
<string name="map_distance">Дистанция: %.0fкм</string>
|
||||
<string name="map_latitude">Широта: %.1f°</string>
|
||||
<string name="map_longitude">Долгота: %.1f°</string>
|
||||
<string name="map_phase">Фаза: %.1f°</string>
|
||||
<string name="map_eclipsed">В тени</string>
|
||||
<string name="map_period">Период: %.0fмин</string>
|
||||
<string name="map_velocity">Скорость: %.2fкм/с</string>
|
||||
<string name="map_visibility">Видимость: %s</string>
|
||||
<string name="map_visible">Видимый</string>
|
||||
|
||||
<!-- Settings screen -->
|
||||
<string name="prefs_donate_title">Поддержать</string>
|
||||
<string name="prefs_privacy_title">Политика конфиденциальности</string>
|
||||
|
||||
<string name="prefs_updated_title">Обновлено: %s</string>
|
||||
<string name="prefs_updated_time">d MMM yyyy - HH:mm:ss</string>
|
||||
|
||||
<string name="prefs_loc_title">Местоположение</string>
|
||||
<string name="prefs_loc_gps_title">GPS</string>
|
||||
<string name="prefs_loc_gps_error">Нет разрешения использовать геоданные</string>
|
||||
<string name="prefs_loc_input_title">Ввести</string>
|
||||
<string name="prefs_loc_input_error">Введены неверные данные</string>
|
||||
<string name="prefs_loc_qth_title">QTH</string>
|
||||
<string name="prefs_loc_qth_error">Неправильный локатор QTH</string>
|
||||
<string name="prefs_loc_success">Позиция успешно обновлена</string>
|
||||
<string name="prefs_station_title">Настройки местоположения</string>
|
||||
<string name="prefs_station_lat_text">Введите широту наземной станции</string>
|
||||
<string name="prefs_station_lon_text">Введите долготу наземной станции</string>
|
||||
<string name="prefs_locator_title">Настройки локатора QTH</string>
|
||||
<string name="prefs_locator_text">Введите позицию через локатор</string>
|
||||
|
||||
<string name="prefs_data_title">Данные спутников</string>
|
||||
<string name="prefs_data_entries">Спутников: %s</string>
|
||||
<string name="prefs_data_radios">Трансиверов: %s</string>
|
||||
<string name="prefs_data_update">Обновить</string>
|
||||
<string name="prefs_data_import">Импорт</string>
|
||||
<string name="prefs_data_clear">Очистить</string>
|
||||
<string name="prefs_data_clear_success">Очистка прошла успешно</string>
|
||||
<string name="prefs_data_update_success">Обновление прошло успешно</string>
|
||||
|
||||
<string name="prefs_data_sources_title">Свои источники данных</string>
|
||||
<string name="prefs_data_sources_tle_switch">Custom TLE URL</string>
|
||||
<string name="prefs_data_sources_transceivers_switch">Custom Transceivers URL</string>
|
||||
|
||||
<string name="prefs_data_output_title">Вывод данных</string>
|
||||
<string name="prefs_net_output">Сеть</string>
|
||||
<string name="prefs_bt_output">Bluetooth</string>
|
||||
|
||||
<string name="prefs_net_title">Вывод сетевых данных</string>
|
||||
<string name="prefs_net_rotator_switch">Включить вывод ротатора</string>
|
||||
<string name="prefs_net_rotator_ip_hint">IP адрес</string>
|
||||
<string name="prefs_net_rotator_port_hint">Порт</string>
|
||||
<string name="prefs_net_rotator_format_hint">Формат</string>
|
||||
<string name="prefs_net_frequency_switch">Включить вывод частоты</string>
|
||||
<string name="prefs_net_frequency_ip_hint">IP адрес</string>
|
||||
<string name="prefs_net_frequency_port_hint">Порт</string>
|
||||
<string name="prefs_net_frequency_format_hint">Формат</string>
|
||||
|
||||
<string name="prefs_bt_title">Вывод данных Bluetooth</string>
|
||||
<string name="prefs_bt_rotator_switch">Включить вывод ротатора</string>
|
||||
<string name="prefs_bt_rotator_device_hint">Id устройства</string>
|
||||
<string name="prefs_bt_rotator_output_hint">Формат</string>
|
||||
<string name="prefs_bt_frequency_switch">Включить вывод частоты</string>
|
||||
<string name="prefs_bt_frequency_device_hint">Id устройства</string>
|
||||
<string name="prefs_bt_frequency_output_hint">Формат</string>
|
||||
<string name="prefs_bt_perm_error">Нет разрешения использовать bluetooth</string>
|
||||
|
||||
<string name="prefs_other_title">Другие настройки</string>
|
||||
<string name="prefs_other_switch_utc">Показывать время по UTC</string>
|
||||
<string name="prefs_other_switch_update">Обновлять данные автоматически</string>
|
||||
<string name="prefs_other_switch_sweep">Показывать анимацию радара</string>
|
||||
<string name="prefs_other_switch_sensors">Использовать сенсоры устройства</string>
|
||||
|
||||
<string name="prefs_outro_title">Я хотел бы сказать спасибо:</string>
|
||||
<string name="prefs_outro_license">Это ПО поставляется без гарантий.</string>
|
||||
|
||||
</resources>
|
||||
@@ -0,0 +1,147 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<resources>
|
||||
<!-- Commmon -->
|
||||
<string name="btn_accept">පිළිගන්න</string>
|
||||
<string name="btn_cancel">අවලංගු ක°</string>
|
||||
<string name="empty_list_message">පෙන්වීමට දත්ත නැත</string>
|
||||
|
||||
<!-- Navigation -->
|
||||
<string name="nav_sat">චන්ද්රිකා</string>
|
||||
<string name="nav_pass">පසුකර යෑම්</string>
|
||||
<string name="nav_radar">රේඩාර්</string>
|
||||
<string name="nav_map">සිතියම</string>
|
||||
<string name="nav_prefs">පසුතල</string>
|
||||
|
||||
<!-- Satellites screen -->
|
||||
<string name="sat_type_hint">වර්ගය: %s</string>
|
||||
<string name="sat_type_title">චන්ද්රිකා ආකාරය තෝරන්න</string>
|
||||
<string name="sat_search_hint">Id - නම</string>
|
||||
<string name="sat_search_clear">පිරිසිදු ක°</string>
|
||||
<string name="sat_clear_all">සියල්ල පිරිසිදු ක°</string>
|
||||
<string name="sat_select_all">සියල්ල තෝරන්න</string>
|
||||
<string name="sat_empty_list_message">
|
||||
ඔබගේ සෙවුම් විමසුම නිවැරදි බවත් දත්ත සමුදාය යාවත්කාලීන කර ඇති බවත් සහතික කර ගන්න</string>
|
||||
<string name="sat_warning_title">අවවාදයයි\!</string>
|
||||
<string name="sat_warning_message">
|
||||
මෙම යෙදුමේ ලැයිස්තුගත කර ඇති චන්ද්රිකා 9000 කට වඩා තිබේ.
|
||||
ඒවා සියල්ලම එකවර නිරීක්ෂණය කිරීම තේරුමක් නැති දෙයක්.
|
||||
\n\nසෙවීම සහ වර්ග තේරීම හරහා ඔබ උනන්දුවක් දක්වන
|
||||
ඒවාට පමණක් ලැයිස්තුව පටු කිරීමට සැමවිටම උත්සාහ කරන්න.</string>
|
||||
|
||||
<!-- Passes screen -->
|
||||
<string name="pass_filter_title">පසුකිරිම් පිරිපහදුව</string>
|
||||
<string name="pass_filter_elev">අවම උත්තෝලනය</string>
|
||||
<string name="pass_filter_hours">ඉදිරි පැය</string>
|
||||
<string name="pass_modes_title">මූර්ජන ආකාරය තෝරන්න</string>
|
||||
<string name="pass_elevation">උත්තෝලනය: %.1f°</string>
|
||||
<string name="pass_altitude">උන්නතාශය: %d km</string>
|
||||
<string name="pass_empty_list_message">
|
||||
ඔබගේ පෙරහන් නිවැරදි බවත් ඔබ චන්ද්රිකා තෝරාගෙන ඇති බවත් සහතික කර ගන්න</string>
|
||||
<string name="pass_error_message">ප්රදර්ශනය කිරීමට ඉදිරියට එන චන්ද්රිකා පසුකර යෑම් නැත.</string>
|
||||
<string name="pass_welcome_title">Look4Sat වෙත සාදරයෙන් පිළිගනිමු\!</string>
|
||||
<string name="pass_welcome_message">
|
||||
සැකසුම් තුළ GPS, Lat/Lon හෝ QTH හරහා ඔබේ ස්ථානය සැකසීමට වග බලා ගන්න.
|
||||
\n\nනිවැරදි අනාවැකි ලබා ගැනීම සඳහා අවම වශයෙන් සතිපතා දත්ත සමුදාය යාවත්කාලීන කරන්න.</string>
|
||||
|
||||
<!-- Radar screen -->
|
||||
<string name="radar_back">ආපසු</string>
|
||||
<string name="radar_notify">දන්වනවා</string>
|
||||
<string name="radar_az_text">දිගංශය</string>
|
||||
<string name="radar_el_text">උත්තෝලනය</string>
|
||||
<string name="radar_alt_text">උන්නතාශය</string>
|
||||
<string name="radar_dist_text">පරතරය</string>
|
||||
<string name="radar_eclipsed">අදෘශ්ය</string>
|
||||
<string name="radar_downlink">පහළ සබැඳිය</string>
|
||||
<string name="radar_uplink">උඩුගත කිරීම</string>
|
||||
<string name="radar_no_data">මෙම චන්ද්රිකාව සම්ප්රෙෂණය නොකරයි</string>
|
||||
<string name="radar_add">එකතු ක°</string>
|
||||
<string name="radar_data">චන්ද්රිකා සම්ප්රේෂණයන්:</string>
|
||||
<string name="radar_inverted">Inverted: %s</string>
|
||||
<string name="radar_mode">ප්රාකාරය: %s</string>
|
||||
<string name="radar_string_no">නෑ</string>
|
||||
<string name="radar_string_yes">ඔව්</string>
|
||||
<string name="radar_visible">දෘශ්යමාන</string>
|
||||
|
||||
<!-- Map screen -->
|
||||
<string name="map_prev">පසුපස</string>
|
||||
<string name="map_next">ඊළග</string>
|
||||
<string name="map_azimuth">දිගංශය: %.1f°</string>
|
||||
<string name="map_elevation">උත්තෝලනය: %.1f°</string>
|
||||
<string name="map_altitude">උන්නතාශය: %.0f km</string>,
|
||||
<string name="map_distance">පරතරය: %.0f km</string>
|
||||
<string name="map_latitude">අක්ෂාංශ: %.1f°</string>
|
||||
<string name="map_longitude">දේශාංශ: %.1f°</string>
|
||||
<string name="map_phase">අංශය: %.1f°</string>
|
||||
<string name="map_eclipsed">අදෘශ්ය</string>
|
||||
<string name="map_period">කාලය: %.0f min</string>
|
||||
<string name="map_velocity">ප්රවේශය: %.2f km/s</string>
|
||||
<string name="map_visibility">දෘශ්යතාව: %s</string>
|
||||
<string name="map_visible">දෘශ්ය</string>
|
||||
|
||||
<!-- Settings screen -->
|
||||
<string name="prefs_donate_title">දායකත්ව</string>
|
||||
<string name="prefs_privacy_title">රහස්යතා ප්රතිපත්තිය</string>
|
||||
|
||||
<string name="prefs_updated_title">යාවත්කාලීන කරන ලදී: %s</string>
|
||||
<string name="prefs_updated_time">d MMM yyyy - HH:mm:ss</string>
|
||||
|
||||
<string name="prefs_loc_title">පොළෙහි පිහිටීම</string>
|
||||
<string name="prefs_loc_gps_title">GPS</string>
|
||||
<string name="prefs_loc_gps_error">ඔබගේ location අවසර පරීක්ෂා ක°</string>
|
||||
<string name="prefs_loc_input_title">ආදානය</string>
|
||||
<string name="prefs_loc_input_error">ඇතුළත් කළ ස්ථානය වලංගු නොවේ</string>
|
||||
<string name="prefs_loc_qth_title">QTH</string>
|
||||
<string name="prefs_loc_qth_error">QTH locator වලංගු නොවේ</string>
|
||||
<string name="prefs_loc_success">පිහිටීම යාවත්කාලීනය සාර්ථකයි</string>
|
||||
<string name="prefs_station_title">පොළෙහි පිහිටීම් පසුතල</string>
|
||||
<string name="prefs_station_lat_text">ඔබගේ පොළෙහි අක්ෂාංශය සකසන්න</string>
|
||||
<string name="prefs_station_lon_text">ඔබගේ පොළෙහි දේශාංශය සකසන්න</string>
|
||||
<string name="prefs_locator_title">QTH locator පසුතල</string>
|
||||
<string name="prefs_locator_text"> ඔබගේ locator භාවිතා කර පොළහි පිහිටීම සකසන්න</string>
|
||||
|
||||
<string name="prefs_data_title">චන්ද්රිකා දත්ත</string>
|
||||
<string name="prefs_data_entries">චන්ද්රිකා: %s</string>
|
||||
<string name="prefs_data_radios">සම්ප්රෙේෂණය: %s</string>
|
||||
<string name="prefs_data_update">නැවුම් කරන්න</string>
|
||||
<string name="prefs_data_import">ආදානය</string>
|
||||
<string name="prefs_data_clear">පිරිසිදු ක°</string>
|
||||
<string name="prefs_data_clear_success">දත්ත ඉවත් කිරීම සාර්ථකයි</string>
|
||||
<string name="prefs_data_update_success">යාවත්කාලීනය සම්පූර්ණ කිරීම සාර්ථකයි</string>
|
||||
|
||||
<string name="prefs_data_sources_title">Custom data sources</string>
|
||||
<string name="prefs_data_sources_tle_switch">Custom TLE URL</string>
|
||||
<string name="prefs_data_sources_transceivers_switch">Custom transceivers URL</string>
|
||||
|
||||
<string name="prefs_data_output_title">Data output</string>
|
||||
<string name="prefs_net_output">Network</string>
|
||||
<string name="prefs_bt_output">Bluetooth</string>
|
||||
|
||||
<string name="prefs_net_title">ජාල ප්රතිදානය</string>
|
||||
<string name="prefs_net_rotator_switch">Enable rotation output</string>
|
||||
<string name="prefs_net_rotator_ip_hint">IP ලිපිනය</string>
|
||||
<string name="prefs_net_rotator_port_hint">Port</string>
|
||||
<string name="prefs_net_rotator_format_hint">දත්ත අකාරය</string>
|
||||
<string name="prefs_net_frequency_switch">Enable frequency output</string>
|
||||
<string name="prefs_net_frequency_ip_hint">IP ලිපිනය</string>
|
||||
<string name="prefs_net_frequency_port_hint">Port</string>
|
||||
<string name="prefs_net_frequency_format_hint">දත්ත අකාරය</string>
|
||||
|
||||
<string name="prefs_bt_title">Bluetooth දත්ත ප්රතිදානය</string>
|
||||
<string name="prefs_bt_rotator_switch">Enable rotation output</string>
|
||||
<string name="prefs_bt_rotator_device_hint">උපාංග id</string>
|
||||
<string name="prefs_bt_rotator_output_hint">දත්ත අකාරය</string>
|
||||
<string name="prefs_bt_frequency_switch">Enable frequency output</string>
|
||||
<string name="prefs_bt_frequency_device_hint">උපාංග id</string>
|
||||
<string name="prefs_bt_frequency_output_hint">දත්ත අකාරය</string>
|
||||
<string name="prefs_bt_perm_error">Bluetooth අවසර පරික්ෂා ක°</string>
|
||||
|
||||
<string name="prefs_other_title">වෙනත් සැකසුම්</string>
|
||||
<string name="prefs_other_switch_utc">පසුකර වේලාවන් UTC මගින්</string>
|
||||
<string name="prefs_other_switch_update">ස්වයං දත්ත යාවත්කාලීනය</string>
|
||||
<string name="prefs_other_switch_sweep">radar sweep සජීවිකරණය සබල කරන්න</string>
|
||||
<string name="prefs_other_switch_sensors">Radar දර්ශනය කරකැවීමට සංවේදක භාවිතා කරන්න </string>
|
||||
|
||||
<string name="prefs_outro_title">මම ස්තුති කිරීමට කැමති:</string>
|
||||
<string name="prefs_outro_license">මෘදුකාංගය වගකීමක් සමග නොලැබේ</string>
|
||||
|
||||
</resources>
|
||||
@@ -0,0 +1,147 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<resources>
|
||||
<!-- Commmon -->
|
||||
<string name="btn_accept">Прийняти</string>
|
||||
<string name="btn_cancel">Скасувати</string>
|
||||
<string name="empty_list_message">Немає даних для відображення</string>
|
||||
|
||||
<!-- Navigation -->
|
||||
<string name="nav_sat">Супутники</string>
|
||||
<string name="nav_pass">Прольоти</string>
|
||||
<string name="nav_radar">Радар</string>
|
||||
<string name="nav_map">Карта</string>
|
||||
<string name="nav_prefs">Налаштування</string>
|
||||
|
||||
<!-- Satellites screen -->
|
||||
<string name="sat_type_hint">Тип: %s</string>
|
||||
<string name="sat_type_title">Виберіть тип супутника</string>
|
||||
<string name="sat_search_hint">Id - Назва</string>
|
||||
<string name="sat_search_clear">Очистити</string>
|
||||
<string name="sat_clear_all">Очистити всі</string>
|
||||
<string name="sat_select_all">Вибрати всі</string>
|
||||
<string name="sat_empty_list_message">
|
||||
Переконайтеся, що ваш пошуковий запит правильний, а база даних оновлена</string>
|
||||
<string name="sat_warning_title">Увага\!</string>
|
||||
<string name="sat_warning_message">
|
||||
У цьому додатку перелічено понад 9000 супутників.
|
||||
Немає сенсу відстежувати їх усі одночасно.
|
||||
\n\nЗавжди намагайтеся звузити список лише до тих,
|
||||
які вас цікавлять, за допомогою пошуку та селектора типів.</string>
|
||||
|
||||
<!-- Passes screen -->
|
||||
<string name="pass_filter_title">Фільтр прольотів</string>
|
||||
<string name="pass_filter_elev">Мінімальне піднесення</string>
|
||||
<string name="pass_filter_hours">Кількість годин</string>
|
||||
<string name="pass_modes_title">Виберіть тип модуляції</string>
|
||||
<string name="pass_elevation">Піднес.: %.1f°</string>
|
||||
<string name="pass_altitude">Висота: %.0f км</string>
|
||||
<string name="pass_empty_list_message">
|
||||
Переконайтеся, що ваші фільтри правильні, і ви вибрали супутники</string>
|
||||
<string name="pass_error_message">Немає майбутніх прольотів супутників</string>
|
||||
<string name="pass_welcome_title">Ласкаво просимо до Look4Sat\!</string>
|
||||
<string name="pass_welcome_message">
|
||||
Обов’язково встановіть своє місцезнаходження за допомогою GPS, широти/довготи або QTH у налаштуваннях.
|
||||
\n\nБудь ласка, оновлюйте базу даних принаймні раз на тиждень, щоб отримувати точні прогнози.</string>
|
||||
|
||||
<!-- Radar screen -->
|
||||
<string name="radar_back">Назад</string>
|
||||
<string name="radar_notify">Сповістити</string>
|
||||
<string name="radar_az_text">Азимут</string>
|
||||
<string name="radar_el_text">Піднес.</string>
|
||||
<string name="radar_alt_text">Висота</string>
|
||||
<string name="radar_dist_text">Відстань</string>
|
||||
<string name="radar_eclipsed">В тіні</string>
|
||||
<string name="radar_downlink">Низхідний канал</string>
|
||||
<string name="radar_uplink">Вихідний канал</string>
|
||||
<string name="radar_no_data">Цей супутник не має трансиверів</string>
|
||||
<string name="radar_add">Додати</string>
|
||||
<string name="radar_data">Супутникові трансивери:</string>
|
||||
<string name="radar_inverted">Інверсія: %s</string>
|
||||
<string name="radar_mode">Модуляція: %s</string>
|
||||
<string name="radar_string_no">Ні</string>
|
||||
<string name="radar_string_yes">Так</string>
|
||||
<string name="radar_visible">Видимий</string>
|
||||
|
||||
<!-- Map screen -->
|
||||
<string name="map_prev">Попередній</string>
|
||||
<string name="map_next">Наступний</string>
|
||||
<string name="map_azimuth">Азимут: %.1f°</string>
|
||||
<string name="map_elevation">Піднесення: %.1f°</string>
|
||||
<string name="map_altitude">Висота: %.0f км</string>
|
||||
<string name="map_distance">Дистанція: %.0f км</string>
|
||||
<string name="map_latitude">Широта: %.1f°</string>
|
||||
<string name="map_longitude">Довгота: %.1f°</string>
|
||||
<string name="map_phase">Фаза: %.1f°</string>
|
||||
<string name="map_eclipsed">В тіні</string>
|
||||
<string name="map_period">Період: %.0f хв</string>
|
||||
<string name="map_velocity">Швидкість: %.2f км/с</string>
|
||||
<string name="map_visibility">Видимість: %s</string>
|
||||
<string name="map_visible">Видимий</string>
|
||||
|
||||
<!-- Settings screen -->
|
||||
<string name="prefs_donate_title">Підтримати</string>
|
||||
<string name="prefs_privacy_title">Політика конфіденційності</string>
|
||||
|
||||
<string name="prefs_updated_title">Оновлено: %s</string>
|
||||
<string name="prefs_updated_time">d MMM yyyy - HH:mm:ss</string>
|
||||
|
||||
<string name="prefs_loc_title">Місцезнаходження</string>
|
||||
<string name="prefs_loc_gps_title">GPS</string>
|
||||
<string name="prefs_loc_gps_error">Перевірте дозвіл на використання геолокацї</string>
|
||||
<string name="prefs_loc_input_title">Ввести</string>
|
||||
<string name="prefs_loc_input_error">Введено некоректну локацію</string>
|
||||
<string name="prefs_loc_qth_title">QTH</string>
|
||||
<string name="prefs_loc_qth_error">Некоректний локатор QTH</string>
|
||||
<string name="prefs_loc_success">Позицію успішно оновлено</string>
|
||||
<string name="prefs_station_title">Налаштування місцезнаходження станції</string>
|
||||
<string name="prefs_station_lat_text">Широта місцезнаходження станції</string>
|
||||
<string name="prefs_station_lon_text">Довгота місцезнаходження станції</string>
|
||||
<string name="prefs_locator_title">Налаштування QTH локатора</string>
|
||||
<string name="prefs_locator_text">Задати позицію за QTH локатором</string>
|
||||
|
||||
<string name="prefs_data_title">Супутникові дані</string>
|
||||
<string name="prefs_data_entries">Супутників: %s</string>
|
||||
<string name="prefs_data_radios">Трансиверів: %s</string>
|
||||
<string name="prefs_data_update">Оновити</string>
|
||||
<string name="prefs_data_import">Імпорт</string>
|
||||
<string name="prefs_data_clear">Очистити</string>
|
||||
<string name="prefs_data_clear_success">Дані успішно очищено</string>
|
||||
<string name="prefs_data_update_success">Оновлення успішне</string>
|
||||
|
||||
<string name="prefs_data_sources_title">Custom data sources</string>
|
||||
<string name="prefs_data_sources_tle_switch">Custom TLE URL</string>
|
||||
<string name="prefs_data_sources_transceivers_switch">Custom transceivers URL</string>
|
||||
|
||||
<string name="prefs_data_output_title">Data output</string>
|
||||
<string name="prefs_net_output">Network</string>
|
||||
<string name="prefs_bt_output">Bluetooth</string>
|
||||
|
||||
<string name="prefs_net_title">Вивід мережевих даних</string>
|
||||
<string name="prefs_net_rotator_switch">Enable rotation output</string>
|
||||
<string name="prefs_net_rotator_ip_hint">IP адреса</string>
|
||||
<string name="prefs_net_rotator_port_hint">Порт</string>
|
||||
<string name="prefs_net_rotator_format_hint">Формат даних</string>
|
||||
<string name="prefs_net_frequency_switch">Enable frequency output</string>
|
||||
<string name="prefs_net_frequency_ip_hint">IP адреса</string>
|
||||
<string name="prefs_net_frequency_port_hint">Порт</string>
|
||||
<string name="prefs_net_frequency_format_hint">Формат даних</string>
|
||||
|
||||
<string name="prefs_bt_title">Вивід даних Bluetooth</string>
|
||||
<string name="prefs_bt_rotator_switch">Enable rotation output</string>
|
||||
<string name="prefs_bt_rotator_device_hint">Id пристрою</string>
|
||||
<string name="prefs_bt_rotator_output_hint">Формат даних</string>
|
||||
<string name="prefs_bt_frequency_switch">Enable frequency output</string>
|
||||
<string name="prefs_bt_frequency_device_hint">Id пристрою</string>
|
||||
<string name="prefs_bt_frequency_output_hint">Формат даних</string>
|
||||
<string name="prefs_bt_perm_error">Перевірте дозвіл Bluetooth</string>
|
||||
|
||||
<string name="prefs_other_title">Інші налаштування</string>
|
||||
<string name="prefs_other_switch_utc">Показувати час в UTC</string>
|
||||
<string name="prefs_other_switch_update">Автоматичне оновлення даних</string>
|
||||
<string name="prefs_other_switch_sweep">Показувати анімацію радара</string>
|
||||
<string name="prefs_other_switch_sensors">Використовувати сенсори пристрою</string>
|
||||
|
||||
<string name="prefs_outro_title">Я хотів би подякувати:</string>
|
||||
<string name="prefs_outro_license">Ця програма поставляється без жодних гарантій</string>
|
||||
|
||||
</resources>
|
||||
@@ -0,0 +1,139 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<resources>
|
||||
<!-- Commmon -->
|
||||
<string name="btn_accept">确定</string>
|
||||
<string name="btn_cancel">取消</string>
|
||||
<string name="empty_list_message">没有数据可显示</string>
|
||||
|
||||
<!-- Navigation -->
|
||||
<string name="nav_sat">卫星</string>
|
||||
<string name="nav_pass">过境</string>
|
||||
<string name="nav_radar">雷达</string>
|
||||
<string name="nav_map">地图</string>
|
||||
<string name="nav_prefs">设置</string>
|
||||
|
||||
<!-- Satellites screen -->
|
||||
<string name="sat_type_hint">类型:%s</string>
|
||||
<string name="sat_type_title">选择卫星类型</string>
|
||||
<string name="sat_search_hint">Id - 名称</string>
|
||||
<string name="sat_search_clear">清空</string>
|
||||
<string name="sat_clear_all">全部清空</string>
|
||||
<string name="sat_select_all">全选</string>
|
||||
<string name="sat_empty_list_message">请确保您的搜索查询正确且数据库已更新</string>
|
||||
<string name="sat_warning_title">警告\!</string>
|
||||
<string name="sat_warning_message">此应用收录了超过9000颗卫星信息\n同时追踪所有卫星并不现实\n建议始终通过搜索和分类筛选功能仅勾选您感兴趣的卫星</string>
|
||||
|
||||
<!-- Passes screen -->
|
||||
<string name="pass_filter_title">筛选过境</string>
|
||||
<string name="pass_filter_elev">仰角值</string>
|
||||
<string name="pass_filter_hours">小时数</string>
|
||||
<string name="pass_modes_title">选择调制类型</string>
|
||||
<string name="pass_elevation">仰角:%.1f°</string>
|
||||
<string name="pass_altitude">高度:%d km</string>
|
||||
<string name="pass_empty_list_message">请确保您的过滤器设置正确,并且已选择卫星</string>
|
||||
<string name="pass_error_message">没有即将过境的卫星</string>
|
||||
<string name="pass_welcome_title">欢迎来到 Look4Sat\!</string>
|
||||
<string name="pass_welcome_message">请务必在设置中通过GPS、经纬度或QTH定位您的位置\n建议至少每周更新一次数据库,以确保预测结果的准确性</string>
|
||||
|
||||
<!-- Radar screen -->
|
||||
<string name="radar_back">后退</string>
|
||||
<string name="radar_notify">通知</string>
|
||||
<string name="radar_az_text">方位角</string>
|
||||
<string name="radar_el_text">仰角</string>
|
||||
<string name="radar_alt_text">高度</string>
|
||||
<string name="radar_dist_text">距离</string>
|
||||
<string name="radar_eclipsed">地影中</string>
|
||||
<string name="radar_downlink">下行链路</string>
|
||||
<string name="radar_uplink">上行链路</string>
|
||||
<string name="radar_no_data">这颗卫星没有收发器</string>
|
||||
<string name="radar_add">添加</string>
|
||||
<string name="radar_data">卫星收发器:</string>
|
||||
<string name="radar_inverted">倒置:%s</string>
|
||||
<string name="radar_mode">调制:%s</string>
|
||||
<string name="radar_string_no">否</string>
|
||||
<string name="radar_string_yes">是</string>
|
||||
<string name="radar_visible">日照中</string>
|
||||
|
||||
<!-- Map screen -->
|
||||
<string name="map_prev">上一个</string>
|
||||
<string name="map_next">下一个</string>
|
||||
<string name="map_azimuth">方位角:%.1f°</string>
|
||||
<string name="map_elevation">仰角:%.1f°</string>
|
||||
<string name="map_altitude">高度:%.0f km</string>
|
||||
<string name="map_distance">距离:%.0f km</string>
|
||||
<string name="map_latitude">纬度:%.1f°</string>
|
||||
<string name="map_longitude">经度:%.1f°</string>
|
||||
<string name="map_phase">相位:%.1f°</string>
|
||||
<string name="map_eclipsed">地影中</string>
|
||||
<string name="map_period">周期:% .0f min</string>
|
||||
<string name="map_velocity">速度:%.2f km/s</string>
|
||||
<string name="map_visibility">能见度:%s</string>
|
||||
<string name="map_visible">日照中</string>
|
||||
|
||||
<!-- Settings screen -->
|
||||
<string name="prefs_donate_title">支持</string>
|
||||
<string name="prefs_privacy_title">隐私政策</string>
|
||||
|
||||
<string name="prefs_updated_title">更新:%s</string>
|
||||
<string name="prefs_updated_time">yyyy MMM d - HH:mm:ss</string>
|
||||
|
||||
<string name="prefs_loc_title">站位</string>
|
||||
<string name="prefs_loc_gps_title">GPS 定位</string>
|
||||
<string name="prefs_loc_gps_error">检查您的位置权限</string>
|
||||
<string name="prefs_loc_input_title">输入位置</string>
|
||||
<string name="prefs_loc_input_error">输入的位置无效</string>
|
||||
<string name="prefs_loc_qth_title">QTH 网格</string>
|
||||
<string name="prefs_loc_qth_error">无效的 QTH 网格</string>
|
||||
<string name="prefs_loc_success">位置更新成功</string>
|
||||
<string name="prefs_station_title">站位设置</string>
|
||||
<string name="prefs_station_lat_text">输入站位的纬度</string>
|
||||
<string name="prefs_station_lon_text">输入站位的经度</string>
|
||||
<string name="prefs_locator_title">QTH 网格</string>
|
||||
<string name="prefs_locator_text">使用梅登黑德网格设置站位</string>
|
||||
|
||||
<string name="prefs_data_title">卫星数据更新</string>
|
||||
<string name="prefs_data_entries">卫星:%s</string>
|
||||
<string name="prefs_data_radios">收发器:%s</string>
|
||||
<string name="prefs_data_update">在线更新</string>
|
||||
<string name="prefs_data_import">导入文件</string>
|
||||
<string name="prefs_data_clear">清空</string>
|
||||
<string name="prefs_data_clear_success">数据清空成功</string>
|
||||
<string name="prefs_data_update_success">更新成功</string>
|
||||
|
||||
<string name="prefs_data_sources_title">自定义数据源</string>
|
||||
<string name="prefs_data_sources_tle_switch">自定义TLE URL</string>
|
||||
<string name="prefs_data_sources_transceivers_switch">自定义收发器URL</string>
|
||||
|
||||
<string name="prefs_data_output_title">数据输出</string>
|
||||
<string name="prefs_net_output">网络</string>
|
||||
<string name="prefs_bt_output">蓝牙</string>
|
||||
|
||||
<string name="prefs_net_title">网络数据输出</string>
|
||||
<string name="prefs_net_rotator_switch">启用方位俯仰角输出</string>
|
||||
<string name="prefs_net_rotator_ip_hint">IP地址</string>
|
||||
<string name="prefs_net_rotator_port_hint">端口</string>
|
||||
<string name="prefs_net_rotator_format_hint">数据格式</string>
|
||||
<string name="prefs_net_frequency_switch">启用多普勒频率输出</string>
|
||||
<string name="prefs_net_frequency_ip_hint">IP地址</string>
|
||||
<string name="prefs_net_frequency_port_hint">端口</string>
|
||||
<string name="prefs_net_frequency_format_hint">数据格式</string>
|
||||
|
||||
<string name="prefs_bt_title">蓝牙数据输出</string>
|
||||
<string name="prefs_bt_rotator_switch">启用方位俯仰角输出</string>
|
||||
<string name="prefs_bt_rotator_device_hint">蓝牙设备 ID</string>
|
||||
<string name="prefs_bt_rotator_output_hint">数据格式</string>
|
||||
<string name="prefs_bt_frequency_switch">启用多普勒频率输出</string>
|
||||
<string name="prefs_bt_frequency_device_hint">蓝牙设备 ID</string>
|
||||
<string name="prefs_bt_frequency_output_hint">数据格式</string>
|
||||
<string name="prefs_bt_perm_error">请检查蓝牙权限</string>
|
||||
|
||||
<string name="prefs_other_title">其他设置</string>
|
||||
<string name="prefs_other_switch_utc">以 UTC 时间显示</string>
|
||||
<string name="prefs_other_switch_update">启用卫星数据自动更新</string>
|
||||
<string name="prefs_other_switch_sweep">启用雷达扫描动画</string>
|
||||
<string name="prefs_other_switch_sensors">使用传感器旋转雷达视图</string>
|
||||
|
||||
<string name="prefs_outro_title">我要感谢:</string>
|
||||
<string name="prefs_outro_license">该应用程序不提供任何保修.</string>
|
||||
|
||||
</resources>
|
||||
@@ -0,0 +1,183 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<resources>
|
||||
<!-- Commmon -->
|
||||
<string name="app_name" translatable="false">Look4Sat</string>
|
||||
<string name="btn_accept">Accept</string>
|
||||
<string name="btn_cancel">Cancel</string>
|
||||
<string name="empty_list_message">No data to show</string>
|
||||
|
||||
<!-- Navigation -->
|
||||
<string name="nav_sat">Satellites</string>
|
||||
<string name="nav_pass">Passes</string>
|
||||
<string name="nav_radar">Radar</string>
|
||||
<string name="nav_map">Map</string>
|
||||
<string name="nav_prefs">Settings</string>
|
||||
|
||||
<!-- Satellites screen -->
|
||||
<string name="sat_type_hint">Type: %s</string>
|
||||
<string name="sat_type_title">Select satellite type</string>
|
||||
<string name="sat_search_hint">Id - Name</string>
|
||||
<string name="sat_search_clear">Clear</string>
|
||||
<string name="sat_clear_all">Clear all</string>
|
||||
<string name="sat_select_all">Select all</string>
|
||||
<string name="sat_empty_list_message">"Make sure your search query is correct and the DB is updated"</string>
|
||||
<string name="sat_warning_title">Warning\!</string>
|
||||
<string name="sat_warning_message">
|
||||
There are over 9000 satellites listed in this app.
|
||||
It makes no sense to track them all at the same time.
|
||||
\n\nAlways try to narrow down the list to only the ones
|
||||
you\'re interested in via search and types selector.</string>
|
||||
|
||||
<!-- Passes screen -->
|
||||
<string name="pass_filter_title">Filter passes</string>
|
||||
<string name="pass_filter_elev">Minimal elevation</string>
|
||||
<string name="pass_filter_hours">Hours ahead</string>
|
||||
<string name="pass_time_placeholder" translatable="false"> -- : -- : -- </string>
|
||||
<string name="pass_modes_title">Select modulation type</string>
|
||||
<string name="pass_satId" translatable="false">%05d</string>
|
||||
<string name="pass_elevation">Elevation: %.1f°</string>
|
||||
<string name="pass_deep_space" translatable="false">DeepSpace</string>
|
||||
<string name="pass_altitude">Altitude: %d km</string>
|
||||
<string name="pass_aosLos" translatable="false">%03d° - %03d°</string>
|
||||
<string name="pass_empty_list_message">
|
||||
Make sure your filters are correct and you have selected satellites</string>
|
||||
<string name="pass_error_message">There are no upcoming satellite passes to display</string>
|
||||
<string name="pass_welcome_title">Welcome to Look4Sat\!</string>
|
||||
<string name="pass_welcome_message">
|
||||
Make sure to set your position via GPS, Lat/Lon or QTH in Settings.
|
||||
\n\nPlease update the database at least weekly to get accurate predictions.</string>
|
||||
<string name="pass_whatsnew_title" translatable="false">What\'s new in Look4Sat</string>
|
||||
<string name="pass_whatsnew_message" translatable="false">
|
||||
• Londre7 added a remote frequency control feature (Hamlib/SDR++) by sending compatible messages via Network/BT.
|
||||
Enable it in Settings and tap the transceiver (Radar Screen).
|
||||
</string>
|
||||
|
||||
<!-- Radar screen -->
|
||||
<string name="radar_back">Back</string>
|
||||
<string name="radar_notify">Notify</string>
|
||||
<string name="radar_az_text">Azimuth</string>
|
||||
<string name="radar_az_value" translatable="false">%.1f°</string>
|
||||
<string name="radar_el_text">Elevation</string>
|
||||
<string name="radar_el_value" translatable="false">%.1f°</string>
|
||||
<string name="radar_alt_text">Altitude</string>
|
||||
<string name="radar_alt_value" translatable="false">%.0f km</string>
|
||||
<string name="radar_dist_text">Distance</string>
|
||||
<string name="radar_dist_value" translatable="false">%.0f km</string>
|
||||
<string name="radar_eclipsed">Eclipsed</string>
|
||||
<string name="radar_downlink">Downlink</string>
|
||||
<string name="radar_uplink">Uplink</string>
|
||||
<string name="radar_link_low" translatable="false">%.4f</string>
|
||||
<string name="radar_link_lowHigh" translatable="false">%.4f - %.4f</string>
|
||||
<string name="radar_no_link" translatable="false">- - . - -</string>
|
||||
<string name="radar_no_data">This satellite has no transceivers</string>
|
||||
<string name="radar_add">Add</string>
|
||||
<string name="radar_data">Satellite transceivers:</string>
|
||||
<string name="radar_inverted">Inverted: %s</string>
|
||||
<string name="radar_mode">Mode: %s</string>
|
||||
<string name="radar_string_no">No</string>
|
||||
<string name="radar_string_yes">Yes</string>
|
||||
<string name="radar_visible">Visible</string>
|
||||
|
||||
<!-- Map screen -->
|
||||
<string name="map_prev">Prev</string>
|
||||
<string name="map_next">Next</string>
|
||||
<string name="map_copyright" translatable="false">© OpenStreetMap contributors</string>
|
||||
<string name="map_azimuth">Azimuth: %.1f°</string>
|
||||
<string name="map_elevation">Elevation: %.1f°</string>
|
||||
<string name="map_altitude">Altitude: %.0f km</string>
|
||||
<string name="map_distance">Distance: %.0f km</string>
|
||||
<string name="map_latitude">Latitude: %.1f°</string>
|
||||
<string name="map_longitude">Longitude: %.1f°</string>
|
||||
<string name="map_qth" translatable="false">QTH: %s</string>
|
||||
<string name="map_phase">Phase: %.1f°</string>
|
||||
<string name="map_eclipsed">Eclipsed</string>
|
||||
<string name="map_period">Period: %.0f min</string>
|
||||
<string name="map_velocity">Velocity: %.2f km/s</string>
|
||||
<string name="map_visibility">Visibility: %s</string>
|
||||
<string name="map_visible">Visible</string>
|
||||
|
||||
<!-- Settings screen -->
|
||||
<string name="prefs_app_title" translatable="false">Look4Sat v%s</string>
|
||||
<string name="prefs_app_url" translatable="false">https://play.google.com/store/apps/details?id=com.rtbishop.look4sat</string>
|
||||
<string name="prefs_donate_title">Donate</string>
|
||||
<string name="prefs_donate_url" translatable="false">https://ko-fi.com/rt_bishop</string>
|
||||
<string name="prefs_fdroid_title" translatable="false">F-Droid</string>
|
||||
<string name="prefs_fdroid_url" translatable="false">https://f-droid.org/en/packages/com.rtbishop.look4sat/</string>
|
||||
<string name="prefs_github_title" translatable="false">GitHub</string>
|
||||
<string name="prefs_github_url" translatable="false">https://github.com/rt-bishop/Look4Sat/</string>
|
||||
<string name="prefs_license_title" translatable="false">GPLv3</string>
|
||||
<string name="prefs_license_url" translatable="false">https://www.gnu.org/licenses/gpl-3.0.html</string>
|
||||
<string name="prefs_privacy_title">Privacy Policy</string>
|
||||
<string name="prefs_privacy_url" translatable="false">https://sites.google.com/view/look4sat-privacy-policy/home</string>
|
||||
|
||||
<string name="prefs_updated_title">Updated: %s</string>
|
||||
<string name="prefs_updated_time">d MMM yyyy - HH:mm:ss</string>
|
||||
|
||||
<string name="prefs_loc_title">Station position</string>
|
||||
<string name="prefs_loc_gps_title">GPS</string>
|
||||
<string name="prefs_loc_gps_error">Check your location permissions</string>
|
||||
<string name="prefs_loc_input_title">Input</string>
|
||||
<string name="prefs_loc_input_error">Invalid location entered</string>
|
||||
<string name="prefs_loc_qth_title">QTH</string>
|
||||
<string name="prefs_loc_qth_error">Invalid QTH locator</string>
|
||||
<string name="prefs_loc_success">Position updated successfully</string>
|
||||
<string name="prefs_station_title">Station position settings</string>
|
||||
<string name="prefs_station_lat_text">Set your ground station\'s latitude</string>
|
||||
<string name="prefs_station_lon_text">Set your ground station\'s longitude</string>
|
||||
<string name="prefs_locator_title">QTH locator settings</string>
|
||||
<string name="prefs_locator_text">Set station\'s position using locator</string>
|
||||
|
||||
<string name="prefs_data_title">Satellite data</string>
|
||||
<string name="prefs_data_entries">Satellites: %s</string>
|
||||
<string name="prefs_data_radios">Transceivers: %s</string>
|
||||
<string name="prefs_data_update">Update</string>
|
||||
<string name="prefs_data_import">Import</string>
|
||||
<string name="prefs_data_clear">Clear</string>
|
||||
<string name="prefs_data_clear_success">Data was cleared successfully</string>
|
||||
<string name="prefs_data_update_success">Update completed successfully</string>
|
||||
|
||||
<string name="prefs_data_sources_title">Custom data sources</string>
|
||||
<string name="prefs_data_sources_tle_switch">Custom TLE URL</string>
|
||||
<string name="prefs_data_sources_transceivers_switch">Custom transceivers URL</string>
|
||||
<string name="prefs_data_sources_url_title" translatable="false">URL (HTTPS)</string>
|
||||
|
||||
<string name="prefs_data_output_title">Data output</string>
|
||||
<string name="prefs_net_output">Network</string>
|
||||
<string name="prefs_bt_output">Bluetooth</string>
|
||||
|
||||
<string name="prefs_net_title">Network data output</string>
|
||||
<string name="prefs_net_rotator_switch">Enable rotation output</string>
|
||||
<string name="prefs_net_rotator_ip_hint">IP address</string>
|
||||
<string name="prefs_net_rotator_port_hint">Port</string>
|
||||
<string name="prefs_net_rotator_format_hint">Format</string>
|
||||
<string name="prefs_net_frequency_switch">Enable frequency output</string>
|
||||
<string name="prefs_net_frequency_ip_hint">IP address</string>
|
||||
<string name="prefs_net_frequency_port_hint">Port</string>
|
||||
<string name="prefs_net_frequency_format_hint">Format</string>
|
||||
|
||||
<string name="prefs_bt_title">Bluetooth data output</string>
|
||||
<string name="prefs_bt_rotator_switch">Enable rotation output</string>
|
||||
<string name="prefs_bt_rotator_device_hint">Device id</string>
|
||||
<string name="prefs_bt_rotator_output_hint">Data format</string>
|
||||
<string name="prefs_bt_frequency_switch">Enable frequency output</string>
|
||||
<string name="prefs_bt_frequency_device_hint">Device id</string>
|
||||
<string name="prefs_bt_frequency_output_hint">Data format</string>
|
||||
<string name="prefs_bt_perm_error">Check your bluetooth permission</string>
|
||||
|
||||
<string name="prefs_other_title">Other settings</string>
|
||||
<string name="prefs_other_switch_utc">Show pass time in UTC</string>
|
||||
<string name="prefs_other_switch_update">Enable automatic data update</string>
|
||||
<string name="prefs_other_switch_sweep">Enable radar sweep animation</string>
|
||||
<string name="prefs_other_switch_sensors">Use sensors to rotate radar view</string>
|
||||
|
||||
<string name="prefs_outro_title">I would like to say thanks to</string>
|
||||
<string name="prefs_outro_thanks" translatable="false">
|
||||
• Look4Sat users and contributors!
|
||||
\n• David A. B. Johnson (predict4java)
|
||||
\n• Dave Moten (predict4java)
|
||||
\n• Alexandru Csete (Gpredict)
|
||||
\n• Dr T.S. Kelso (Celestrak)
|
||||
\n• Libre Space Foundation (SatNOGS)</string>
|
||||
<string name="prefs_outro_license">The app comes with no warranty</string>
|
||||
|
||||
</resources>
|
||||
@@ -0,0 +1,9 @@
|
||||
<resources>
|
||||
<color name="accent">#FFE082</color>
|
||||
<color name="background">#121212</color>
|
||||
<style name="Theme.Look4Sat.SplashScreen" parent="Theme.SplashScreen">
|
||||
<item name="postSplashScreenTheme">@android:style/Theme.Material.NoActionBar</item>
|
||||
<item name="windowSplashScreenBackground">@color/background</item>
|
||||
<item name="windowSplashScreenAnimatedIcon">@drawable/ic_launcher_fg</item>
|
||||
</style>
|
||||
</resources>
|
||||
Reference in new issue
Block a user