Minor refactoring. Changes to the app name and the package.

This commit is contained in:
Arty Bishop committed 2019-12-27 19:56:58 +00:00
1 parent 1b741bc572
commit 0b93780d7c
28 files changed
+116 -178

No files matched your search

@@ -0,0 +1,31 @@
/*
* LookingSat. Amateur radio & weather satellite tracker and passes calculator.
* Copyright (C) 2019 Arty Bishop (bishop.arty@gmail.com)
*
* This program is free software; you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation; either version 2 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, write to the Free Software Foundation, Inc.,
* 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA.
*/
package com.rtbishop.look4sat
import android.app.Application
import com.rtbishop.look4sat.di.AppComponent
import com.rtbishop.look4sat.di.DaggerAppComponent
class Look4SatApp : Application() {
val appComponent: AppComponent by lazy {
DaggerAppComponent.factory().create(applicationContext)
}
}
@@ -0,0 +1,222 @@
/*
* LookingSat. Amateur radio & weather satellite tracker and passes calculator.
* Copyright (C) 2019 Arty Bishop (bishop.arty@gmail.com)
*
* This program is free software; you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation; either version 2 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, write to the Free Software Foundation, Inc.,
* 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA.
*/
package com.rtbishop.look4sat
import android.app.Application
import android.content.Context
import android.content.SharedPreferences
import android.location.Location
import androidx.core.content.edit
import androidx.lifecycle.AndroidViewModel
import androidx.lifecycle.LiveData
import androidx.lifecycle.MutableLiveData
import androidx.lifecycle.viewModelScope
import com.github.amsacode.predict4java.GroundStationPosition
import com.github.amsacode.predict4java.PassPredictor
import com.github.amsacode.predict4java.SatNotFoundException
import com.github.amsacode.predict4java.TLE
import com.google.android.gms.location.FusedLocationProviderClient
import com.rtbishop.look4sat.repo.Repository
import com.rtbishop.look4sat.repo.SatPass
import com.rtbishop.look4sat.repo.Transmitter
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.launch
import kotlinx.coroutines.withContext
import java.io.FileNotFoundException
import java.io.IOException
import java.io.ObjectInputStream
import java.io.ObjectOutputStream
import java.util.*
import javax.inject.Inject
class MainViewModel(application: Application) : AndroidViewModel(application) {
private val defValueLoc = application.getString(R.string.def_gsp_loc)
private val defValueHours = application.getString(R.string.def_hours_ahead)
private val defValueMinEl = application.getString(R.string.def_min_el)
private val defValueUpdateFreq = application.getString(R.string.def_update_freq)
private val keyLat = application.getString(R.string.key_lat)
private val keyLon = application.getString(R.string.key_lon)
private val keyAlt = application.getString(R.string.key_alt)
private val keyHours = application.getString(R.string.key_hours_ahead)
private val keyMinEl = application.getString(R.string.key_min_el)
private val keyUpdateFreq = application.getString(R.string.key_update_freq)
private val tleMainListFileName = application.getString(R.string.tle_main_list_file_name)
private val tleSelectionFileName = application.getString(R.string.tle_selection_file_name)
private val app = application
@Inject
lateinit var locationClient: FusedLocationProviderClient
@Inject
lateinit var preferences: SharedPreferences
@Inject
lateinit var repository: Repository
init {
(app as Look4SatApp).appComponent.inject(this)
}
private var passPrefs = SatPassPrefs(
preferences.getString(keyHours, defValueHours)!!.toInt(),
preferences.getString(keyMinEl, defValueMinEl)!!.toDouble()
)
private val _debugMessage = MutableLiveData<String>()
val debugMessage: LiveData<String> = _debugMessage
private val _gsp = MutableLiveData<GroundStationPosition>(
GroundStationPosition(
preferences.getString(keyLat, defValueLoc)!!.toDouble(),
preferences.getString(keyLon, defValueLoc)!!.toDouble(),
preferences.getString(keyAlt, defValueLoc)!!.toDouble()
)
)
val gsp: LiveData<GroundStationPosition> = _gsp
var satPassList = emptyList<SatPass>()
var tleMainList = loadTwoLineElementFile()
var selectionList = loadSelectionList()
val updateFreq: Long
get() = preferences.getString(keyUpdateFreq, defValueUpdateFreq)!!.toLong()
fun updateGsp() {
val lat = preferences.getString(keyLat, defValueLoc)!!.toDouble()
val lon = preferences.getString(keyLon, defValueLoc)!!.toDouble()
val alt = preferences.getString(keyAlt, defValueLoc)!!.toDouble()
_gsp.postValue(GroundStationPosition(lat, lon, alt))
}
fun getCurrentLocation() {
locationClient.lastLocation.addOnSuccessListener { location: Location? ->
val lat = location?.latitude ?: 0.0
val lon = location?.longitude ?: 0.0
val alt = location?.altitude ?: 0.0
preferences.edit {
putString(keyLat, lat.toString())
putString(keyLon, lon.toString())
putString(keyAlt, alt.toString())
apply()
}
_gsp.postValue(GroundStationPosition(lat, lon, alt))
_debugMessage.postValue("Location was updated")
}
}
fun updateAndSaveTleFile() {
viewModelScope.launch(Dispatchers.IO) {
try {
val inputStream = repository.fetchTleStream()
val tleList = TLE.importSat(inputStream).apply { sortBy { it.name } }
val fileOutStream = app.openFileOutput(tleMainListFileName, Context.MODE_PRIVATE)
ObjectOutputStream(fileOutStream).apply {
writeObject(tleList)
flush()
close()
}
tleMainList = tleList
selectionList = mutableListOf()
_debugMessage.postValue("TLE file was updated")
} catch (exception: IOException) {
_debugMessage.postValue("Couldn't update TLE file")
}
}
}
fun updateTransmittersDatabase() {
viewModelScope.launch(Dispatchers.IO) {
try {
repository.updateTransmittersDatabase()
_debugMessage.postValue("Transmitters were updated")
} catch (exception: IOException) {
_debugMessage.postValue("Couldn't update transmitters")
}
}
}
fun updateAndSaveSelectionList(list: MutableList<Int>) {
selectionList = list
try {
val fileOutputStream = app.openFileOutput(tleSelectionFileName, Context.MODE_PRIVATE)
ObjectOutputStream(fileOutputStream).apply {
writeObject(list)
flush()
close()
}
} catch (exception: IOException) {
_debugMessage.postValue(exception.toString())
}
}
suspend fun getPasses() {
val hoursAhead = preferences.getString(keyHours, defValueHours)!!.toInt()
val minEl = preferences.getString(keyMinEl, defValueMinEl)!!.toDouble()
val passList = mutableListOf<SatPass>()
withContext(Dispatchers.Default) {
selectionList.forEach { indexOfSelection ->
val tle = tleMainList[indexOfSelection]
try {
val predictor = PassPredictor(tle, gsp.value)
val passes = predictor.getPasses(Date(), hoursAhead, true)
passes.forEach { passList.add(SatPass(tle, predictor, it)) }
} catch (exception: IllegalArgumentException) {
_debugMessage.postValue("There was a problem with ${tle.name}")
} catch (exception: SatNotFoundException) {
_debugMessage.postValue("${tle.name} shall not pass")
}
}
passList.retainAll { it.pass.maxEl >= minEl }
passList.sortBy { it.pass.startTime }
}
satPassList = passList
}
suspend fun getTransmittersForSat(id: Int): List<Transmitter> {
return repository.getTransmittersForSat(id)
}
@Suppress("UNCHECKED_CAST")
private fun loadTwoLineElementFile(): List<TLE> {
try {
val fileInputStream = app.openFileInput(tleMainListFileName)
val tleList = ObjectInputStream(fileInputStream).readObject()
return tleList as List<TLE>
} catch (exception: FileNotFoundException) {
_debugMessage.postValue("TLE file wasn't found")
} catch (exception: IOException) {
_debugMessage.postValue(exception.toString())
}
return emptyList()
}
@Suppress("UNCHECKED_CAST")
private fun loadSelectionList(): MutableList<Int> {
try {
val fileInputStream = app.openFileInput(tleSelectionFileName)
val selectionList = ObjectInputStream(fileInputStream).readObject()
return selectionList as MutableList<Int>
} catch (exception: FileNotFoundException) {
_debugMessage.postValue("Selection file wasn't found")
} catch (exception: IOException) {
_debugMessage.postValue(exception.toString())
}
return mutableListOf()
}
}
@@ -0,0 +1,45 @@
/*
* LookingSat. Amateur radio & weather satellite tracker and passes calculator.
* Copyright (C) 2019 Arty Bishop (bishop.arty@gmail.com)
*
* This program is free software; you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation; either version 2 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, write to the Free Software Foundation, Inc.,
* 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA.
*/
package com.rtbishop.look4sat.di
import android.content.Context
import android.content.SharedPreferences
import androidx.preference.PreferenceManager
import com.google.android.gms.location.FusedLocationProviderClient
import com.google.android.gms.location.LocationServices
import dagger.Module
import dagger.Provides
import javax.inject.Singleton
@Module
class AndroidModule {
@Singleton
@Provides
fun provideSharedPreferences(context: Context): SharedPreferences {
return PreferenceManager.getDefaultSharedPreferences(context)
}
@Singleton
@Provides
fun provideLocationClient(context: Context): FusedLocationProviderClient {
return LocationServices.getFusedLocationProviderClient(context)
}
}
@@ -0,0 +1,38 @@
/*
* LookingSat. Amateur radio & weather satellite tracker and passes calculator.
* Copyright (C) 2019 Arty Bishop (bishop.arty@gmail.com)
*
* This program is free software; you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation; either version 2 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, write to the Free Software Foundation, Inc.,
* 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA.
*/
package com.rtbishop.look4sat.di
import android.content.Context
import com.rtbishop.look4sat.MainViewModel
import dagger.BindsInstance
import dagger.Component
import javax.inject.Singleton
@Singleton
@Component(modules = [AndroidModule::class, NetworkModule::class, StorageModule::class])
interface AppComponent {
@Component.Factory
interface Factory {
fun create(@BindsInstance context: Context): AppComponent
}
fun inject(viewModel: MainViewModel)
}
@@ -0,0 +1,48 @@
/*
* LookingSat. Amateur radio & weather satellite tracker and passes calculator.
* Copyright (C) 2019 Arty Bishop (bishop.arty@gmail.com)
*
* This program is free software; you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation; either version 2 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, write to the Free Software Foundation, Inc.,
* 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA.
*/
package com.rtbishop.look4sat.di
import com.rtbishop.look4sat.network.TransmittersApi
import dagger.Module
import dagger.Provides
import okhttp3.OkHttpClient
import retrofit2.Retrofit
import retrofit2.converter.gson.GsonConverterFactory
import javax.inject.Singleton
@Module
class NetworkModule {
@Singleton
@Provides
fun provideTransmittersApi(): TransmittersApi {
return Retrofit.Builder()
.baseUrl("https://db.satnogs.org/api/")
.addConverterFactory(GsonConverterFactory.create())
.build()
.create(TransmittersApi::class.java)
}
@Singleton
@Provides
fun provideOkHttpClient(): OkHttpClient {
return OkHttpClient()
}
}
@@ -0,0 +1,45 @@
/*
* LookingSat. Amateur radio & weather satellite tracker and passes calculator.
* Copyright (C) 2019 Arty Bishop (bishop.arty@gmail.com)
*
* This program is free software; you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation; either version 2 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, write to the Free Software Foundation, Inc.,
* 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA.
*/
package com.rtbishop.look4sat.di
import android.content.Context
import androidx.room.Room
import com.rtbishop.look4sat.storage.TransmittersDao
import com.rtbishop.look4sat.storage.TransmittersDb
import dagger.Module
import dagger.Provides
import javax.inject.Singleton
@Module
class StorageModule {
@Singleton
@Provides
fun provideTransmittersDb(context: Context): TransmittersDb {
return Room.databaseBuilder(context, TransmittersDb::class.java, "transmitters")
.build()
}
@Singleton
@Provides
fun provideTransmittersDao(db: TransmittersDb): TransmittersDao {
return db.transmittersDao()
}
}
@@ -0,0 +1,50 @@
/*
* LookingSat. Amateur radio & weather satellite tracker and passes calculator.
* Copyright (C) 2019 Arty Bishop (bishop.arty@gmail.com)
*
* This program is free software; you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation; either version 2 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, write to the Free Software Foundation, Inc.,
* 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA.
*/
package com.rtbishop.look4sat.network
import com.rtbishop.look4sat.repo.Transmitter
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.withContext
import okhttp3.OkHttpClient
import okhttp3.Request
import java.io.InputStream
import java.io.SequenceInputStream
import java.util.*
import javax.inject.Inject
class RemoteDataSource @Inject constructor(
private val api: TransmittersApi,
private val client: OkHttpClient
) {
fun fetchTleStream(): InputStream {
val requestAmateur =
Request.Builder().url("https://celestrak.com/NORAD/elements/amateur.txt").build()
val requestWeather =
Request.Builder().url("https://celestrak.com/NORAD/elements/weather.txt").build()
val streamAmateur = client.newCall(requestAmateur).execute().body()?.byteStream()
val streamWeather = client.newCall(requestWeather).execute().body()?.byteStream()
return SequenceInputStream(streamAmateur, streamWeather)
}
suspend fun fetchTransmittersList(): List<Transmitter> {
return api.fetchTransmittersList()
}
}
@@ -0,0 +1,29 @@
/*
* LookingSat. Amateur radio & weather satellite tracker and passes calculator.
* Copyright (C) 2019 Arty Bishop (bishop.arty@gmail.com)
*
* This program is free software; you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation; either version 2 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, write to the Free Software Foundation, Inc.,
* 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA.
*/
package com.rtbishop.look4sat.network
import com.rtbishop.look4sat.repo.Transmitter
import retrofit2.http.GET
interface TransmittersApi {
@GET("transmitters/")
suspend fun fetchTransmittersList(): List<Transmitter>
}
@@ -0,0 +1,42 @@
/*
* LookingSat. Amateur radio & weather satellite tracker and passes calculator.
* Copyright (C) 2019 Arty Bishop (bishop.arty@gmail.com)
*
* This program is free software; you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation; either version 2 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, write to the Free Software Foundation, Inc.,
* 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA.
*/
package com.rtbishop.look4sat.repo
import com.rtbishop.look4sat.network.RemoteDataSource
import com.rtbishop.look4sat.storage.LocalDataSource
import java.io.InputStream
import javax.inject.Inject
class Repository @Inject constructor(
private val localSource: LocalDataSource,
private val remoteSource: RemoteDataSource
) {
fun fetchTleStream(): InputStream {
return remoteSource.fetchTleStream()
}
suspend fun updateTransmittersDatabase() {
localSource.insertTransmitters(remoteSource.fetchTransmittersList())
}
suspend fun getTransmittersForSat(id: Int): List<Transmitter> {
return localSource.getTransmittersForSat(id)
}
}
@@ -0,0 +1,34 @@
/*
* LookingSat. Amateur radio & weather satellite tracker and passes calculator.
* Copyright (C) 2019 Arty Bishop (bishop.arty@gmail.com)
*
* This program is free software; you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation; either version 2 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, write to the Free Software Foundation, Inc.,
* 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA.
*/
package com.rtbishop.look4sat.repo
import android.os.Parcelable
import com.github.amsacode.predict4java.PassPredictor
import com.github.amsacode.predict4java.SatPassTime
import com.github.amsacode.predict4java.TLE
import kotlinx.android.parcel.Parcelize
import kotlinx.android.parcel.RawValue
@Parcelize
data class SatPass(
val tle: TLE,
val predictor: @RawValue PassPredictor,
val pass: @RawValue SatPassTime
) : Parcelable
@@ -0,0 +1,38 @@
/*
* LookingSat. Amateur radio & weather satellite tracker and passes calculator.
* Copyright (C) 2019 Arty Bishop (bishop.arty@gmail.com)
*
* This program is free software; you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation; either version 2 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, write to the Free Software Foundation, Inc.,
* 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA.
*/
package com.rtbishop.look4sat.repo
import androidx.room.Entity
import androidx.room.PrimaryKey
import com.google.gson.annotations.SerializedName
@Entity(tableName = "transmitters")
data class Transmitter(
@PrimaryKey @SerializedName("uuid") val uuid: String,
@SerializedName("description") val description: String,
@SerializedName("alive") val isAlive: Boolean,
@SerializedName("uplink_low") val uplinkLow: Long?,
@SerializedName("uplink_high") val uplinkHigh: Long?,
@SerializedName("downlink_low") val downlinkLow: Long?,
@SerializedName("downlink_high") val downlinkHigh: Long?,
@SerializedName("mode") val mode: String?,
@SerializedName("invert") val isInverted: Boolean,
@SerializedName("norad_cat_id") val noradCatId: Int
)
@@ -0,0 +1,34 @@
/*
* LookingSat. Amateur radio & weather satellite tracker and passes calculator.
* Copyright (C) 2019 Arty Bishop (bishop.arty@gmail.com)
*
* This program is free software; you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation; either version 2 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, write to the Free Software Foundation, Inc.,
* 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA.
*/
package com.rtbishop.look4sat.storage
import com.rtbishop.look4sat.repo.Transmitter
import javax.inject.Inject
class LocalDataSource @Inject constructor(private val transmittersDao: TransmittersDao) {
suspend fun insertTransmitters(transmitters: List<Transmitter>) {
transmittersDao.insertTransmitters(transmitters)
}
suspend fun getTransmittersForSat(id: Int): List<Transmitter> {
return transmittersDao.getTransmittersForSat(id)
}
}
@@ -0,0 +1,36 @@
/*
* LookingSat. Amateur radio & weather satellite tracker and passes calculator.
* Copyright (C) 2019 Arty Bishop (bishop.arty@gmail.com)
*
* This program is free software; you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation; either version 2 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, write to the Free Software Foundation, Inc.,
* 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA.
*/
package com.rtbishop.look4sat.storage
import androidx.room.Dao
import androidx.room.Insert
import androidx.room.OnConflictStrategy
import androidx.room.Query
import com.rtbishop.look4sat.repo.Transmitter
@Dao
interface TransmittersDao {
@Insert(onConflict = OnConflictStrategy.REPLACE)
suspend fun insertTransmitters(transmitters: List<Transmitter>)
@Query("SELECT * FROM transmitters WHERE isAlive = 1 and noradCatId = :id")
suspend fun getTransmittersForSat(id: Int): List<Transmitter>
}
@@ -0,0 +1,30 @@
/*
* LookingSat. Amateur radio & weather satellite tracker and passes calculator.
* Copyright (C) 2019 Arty Bishop (bishop.arty@gmail.com)
*
* This program is free software; you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation; either version 2 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, write to the Free Software Foundation, Inc.,
* 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA.
*/
package com.rtbishop.look4sat.storage
import androidx.room.Database
import androidx.room.RoomDatabase
import com.rtbishop.look4sat.repo.Transmitter
@Database(entities = [Transmitter::class], version = 1, exportSchema = false)
abstract class TransmittersDb : RoomDatabase() {
abstract fun transmittersDao(): TransmittersDao
}
@@ -0,0 +1,38 @@
/*
* LookingSat. Amateur radio & weather satellite tracker and passes calculator.
* Copyright (C) 2019 Arty Bishop (bishop.arty@gmail.com)
*
* This program is free software; you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation; either version 2 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, write to the Free Software Foundation, Inc.,
* 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA.
*/
package com.rtbishop.look4sat.ui
import android.os.Bundle
import android.view.LayoutInflater
import android.view.View
import android.view.ViewGroup
import androidx.fragment.app.Fragment
import com.rtbishop.look4sat.R
class CreditsFragment : Fragment() {
override fun onCreateView(
inflater: LayoutInflater,
container: ViewGroup?,
savedInstanceState: Bundle?
): View? {
return inflater.inflate(R.layout.fragment_credits, container, false)
}
}
@@ -0,0 +1,170 @@
/*
* LookingSat. Amateur radio & weather satellite tracker and passes calculator.
* Copyright (C) 2019 Arty Bishop (bishop.arty@gmail.com)
*
* This program is free software; you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation; either version 2 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, write to the Free Software Foundation, Inc.,
* 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA.
*/
package com.rtbishop.look4sat.ui
import android.Manifest
import android.content.Intent
import android.content.pm.ActivityInfo
import android.content.pm.PackageManager
import android.net.Uri
import android.os.Bundle
import android.view.View
import android.widget.ImageButton
import android.widget.TextView
import android.widget.Toast
import androidx.appcompat.app.AppCompatActivity
import androidx.appcompat.widget.Toolbar
import androidx.constraintlayout.widget.ConstraintLayout
import androidx.core.app.ActivityCompat
import androidx.core.content.ContextCompat
import androidx.drawerlayout.widget.DrawerLayout
import androidx.lifecycle.Observer
import androidx.lifecycle.ViewModelProvider
import androidx.navigation.findNavController
import androidx.navigation.ui.AppBarConfiguration
import androidx.navigation.ui.navigateUp
import androidx.navigation.ui.setupActionBarWithNavController
import androidx.navigation.ui.setupWithNavController
import com.google.android.material.navigation.NavigationView
import com.rtbishop.look4sat.MainViewModel
import com.rtbishop.look4sat.R
class MainActivity : AppCompatActivity() {
private val permLocationCode = 101
private val permLocation = Manifest.permission.ACCESS_FINE_LOCATION
private val permGranted = PackageManager.PERMISSION_GRANTED
private val githubUrl = "https://github.com/rt-bishop/LookingSat"
private lateinit var viewModel: MainViewModel
private lateinit var timerLayout: ConstraintLayout
private lateinit var drawerLayout: DrawerLayout
private lateinit var drawerLat: TextView
private lateinit var drawerLon: TextView
private lateinit var drawerBtnLoc: ImageButton
private lateinit var drawerBtnTle: ImageButton
private lateinit var drawerBtnTrans: ImageButton
private lateinit var drawerBtnGithub: ImageButton
private lateinit var drawerBtnExit: ImageButton
private lateinit var appBarConfig: AppBarConfiguration
override fun onCreate(savedInstanceState: Bundle?) {
setTheme(R.style.AppTheme)
super.onCreate(savedInstanceState)
setContentView(R.layout.activity_main)
viewModel = ViewModelProvider(this).get(MainViewModel::class.java)
setupComponents()
setupObservers()
setupDrawer()
}
private fun setupComponents() {
val navController = findNavController(R.id.nav_host)
val navView: NavigationView = findViewById(R.id.nav_view)
val header = navView.getHeaderView(0)
val toolbar: Toolbar = findViewById(R.id.toolbar)
setSupportActionBar(toolbar)
timerLayout = findViewById(R.id.toolbar_layout_timer)
drawerLayout = findViewById(R.id.drawer_layout)
drawerLat = header.findViewById(R.id.drawer_lat_value)
drawerLon = header.findViewById(R.id.drawer_lon_value)
drawerBtnLoc = header.findViewById(R.id.drawer_btn_loc)
drawerBtnTle = header.findViewById(R.id.drawer_btn_tle)
drawerBtnTrans = header.findViewById(R.id.drawer_btn_trans)
drawerBtnGithub = header.findViewById(R.id.drawer_btn_github)
drawerBtnExit = header.findViewById(R.id.drawer_btn_exit)
appBarConfig = AppBarConfiguration(
setOf(R.id.nav_sky, R.id.nav_map, R.id.nav_credits, R.id.nav_about),
drawerLayout
)
setupActionBarWithNavController(navController, appBarConfig)
navView.setupWithNavController(navController)
navController.addOnDestinationChangedListener { _, destination, _ ->
when (destination.id) {
R.id.nav_sky -> {
this.requestedOrientation = ActivityInfo.SCREEN_ORIENTATION_PORTRAIT
toolbar.visibility = View.VISIBLE
timerLayout.visibility = View.VISIBLE
}
R.id.nav_map -> {
this.requestedOrientation = ActivityInfo.SCREEN_ORIENTATION_LANDSCAPE
toolbar.visibility = View.GONE
}
else -> {
this.requestedOrientation = ActivityInfo.SCREEN_ORIENTATION_PORTRAIT
toolbar.visibility = View.VISIBLE
timerLayout.visibility = View.GONE
}
}
}
}
private fun setupObservers() {
viewModel.debugMessage.observe(this, Observer { debug_message ->
Toast.makeText(this, debug_message, Toast.LENGTH_SHORT).show()
})
viewModel.gsp.observe(this, Observer { gsp ->
drawerLat.text = String.format(getString(R.string.pattern_lat), gsp.latitude)
drawerLon.text = String.format(getString(R.string.pattern_lon), gsp.longitude)
})
}
private fun setupDrawer() {
drawerBtnLoc.setOnClickListener { requestLocationUpdate() }
drawerBtnTle.setOnClickListener { viewModel.updateAndSaveTleFile() }
drawerBtnTrans.setOnClickListener { viewModel.updateTransmittersDatabase() }
drawerBtnGithub.setOnClickListener {
startActivity(Intent(Intent.ACTION_VIEW, Uri.parse(githubUrl)))
}
drawerBtnExit.setOnClickListener { finish() }
}
private fun requestLocationUpdate() {
if (ContextCompat.checkSelfPermission(this, permLocation) != permGranted) {
if (ActivityCompat.shouldShowRequestPermissionRationale(this, permLocation)) {
Toast.makeText(this, getString(R.string.no_permissions), Toast.LENGTH_LONG).show()
} else {
ActivityCompat.requestPermissions(this, arrayOf(permLocation), permLocationCode)
}
} else {
viewModel.getCurrentLocation()
}
}
override fun onRequestPermissionsResult(reqCode: Int, perms: Array<out String>, res: IntArray) {
when (reqCode) {
permLocationCode -> {
if (res.isNotEmpty() && res[0] == permGranted) {
viewModel.getCurrentLocation()
}
}
}
}
override fun onSupportNavigateUp(): Boolean {
val navController = findNavController(R.id.nav_host)
return navController.navigateUp(appBarConfig) || super.onSupportNavigateUp()
}
}
@@ -0,0 +1,233 @@
/*
* LookingSat. Amateur radio & weather satellite tracker and passes calculator.
* Copyright (C) 2019 Arty Bishop (bishop.arty@gmail.com)
*
* This program is free software; you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation; either version 2 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, write to the Free Software Foundation, Inc.,
* 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA.
*/
package com.rtbishop.look4sat.ui
import android.content.Context
import android.graphics.Canvas
import android.graphics.Paint
import android.graphics.Path
import android.os.Bundle
import android.view.LayoutInflater
import android.view.View
import android.view.ViewGroup
import android.widget.FrameLayout
import android.widget.TextView
import androidx.fragment.app.Fragment
import androidx.lifecycle.ViewModelProvider
import androidx.lifecycle.lifecycleScope
import androidx.recyclerview.widget.LinearLayoutManager
import androidx.recyclerview.widget.RecyclerView
import com.github.amsacode.predict4java.SatPos
import com.rtbishop.look4sat.MainViewModel
import com.rtbishop.look4sat.R
import com.rtbishop.look4sat.repo.SatPass
import kotlinx.coroutines.launch
import java.util.*
import java.util.concurrent.Executors
import java.util.concurrent.TimeUnit
import kotlin.math.cos
import kotlin.math.sin
class RadarFragment : Fragment() {
private val service = Executors.newSingleThreadScheduledExecutor()
private lateinit var viewModel: MainViewModel
private lateinit var satPass: SatPass
private lateinit var radarView: RadarView
private lateinit var radarSkyFrame: FrameLayout
private lateinit var transRecycler: RecyclerView
private lateinit var transAdapter: TransAdapter
private lateinit var radarAzimuth: TextView
private lateinit var radarElevation: TextView
private lateinit var radarRange: TextView
private lateinit var radarAltitude: TextView
private lateinit var transNoFound: TextView
private lateinit var mainActivity: MainActivity
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
mainActivity = activity as MainActivity
viewModel = ViewModelProvider(mainActivity).get(MainViewModel::class.java)
}
override fun onCreateView(
inflater: LayoutInflater,
container: ViewGroup?,
savedInstanceState: Bundle?
): View? {
return inflater.inflate(R.layout.fragment_radar, container, false)
}
override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
super.onViewCreated(view, savedInstanceState)
val delay = viewModel.updateFreq
satPass = arguments?.get("satPass") as SatPass
mainActivity.supportActionBar?.title = satPass.tle.name
radarSkyFrame = view.findViewById(R.id.radar_sky_frame)
transRecycler = view.findViewById(R.id.radar_recycler)
radarAzimuth = view.findViewById(R.id.radar_azimuth)
radarElevation = view.findViewById(R.id.radar_elevation)
radarRange = view.findViewById(R.id.radar_range)
radarAltitude = view.findViewById(R.id.radar_altitude)
transNoFound = view.findViewById(R.id.radar_no_trans)
radarView = RadarView(mainActivity, delay)
radarSkyFrame.addView(radarView)
service.scheduleAtFixedRate({ radarView.invalidate() }, delay, delay, TimeUnit.MILLISECONDS)
setupTransRecycler()
}
private fun setupTransRecycler() {
transAdapter = TransAdapter()
transRecycler.apply {
layoutManager = LinearLayoutManager(mainActivity)
adapter = transAdapter
}
lifecycleScope.launch {
val transList = viewModel.getTransmittersForSat(satPass.tle.catnum)
if (transList.isNotEmpty()) {
transAdapter.setList(transList)
transAdapter.notifyDataSetChanged()
} else {
transRecycler.visibility = View.INVISIBLE
transNoFound.visibility = View.VISIBLE
}
}
}
inner class RadarView(context: Context, updateFreq: Long) : View(context) {
private val radarSize = resources.displayMetrics.widthPixels
private val scale = resources.displayMetrics.density
private val startTime = satPass.pass.startTime
private val endTime = satPass.pass.endTime
private val radius = radarSize * 0.45f
private val piDiv2 = Math.PI / 2.0
private val txtSize = scale * 15
private val center = 0f
private val delay = updateFreq
private val radarPaint = Paint().apply {
isAntiAlias = true
color = resources.getColor(R.color.lightOnDark, mainActivity.theme)
style = Paint.Style.STROKE
strokeWidth = scale
}
private val txtPaint = Paint().apply {
isAntiAlias = true
color = resources.getColor(R.color.themeAccent, mainActivity.theme)
textSize = txtSize
}
private val trackPaint = Paint().apply {
isAntiAlias = true
color = resources.getColor(R.color.satTrack, mainActivity.theme)
style = Paint.Style.STROKE
strokeWidth = scale
}
private val satPaint = Paint().apply {
isAntiAlias = true
color = resources.getColor(R.color.themeAccent, mainActivity.theme)
style = Paint.Style.FILL
}
private val path: Path = Path()
private lateinit var satPos: SatPos
private var satPassX = 0f
private var satPassY = 0f
override fun onDraw(canvas: Canvas) {
canvas.translate(radarSize / 2f, radarSize / 2f)
setPassText()
drawRadarView(canvas)
drawRadarText(canvas)
drawPassTrajectory(canvas)
drawSatellite(canvas)
}
private fun setPassText() {
satPos = satPass.predictor.getSatPos(Date())
radarAzimuth.text = String.format("Azimuth: %.1f°", rad2Deg(satPos.azimuth))
radarElevation.text = String.format("Elevation: %.1f°", rad2Deg(satPos.elevation))
radarRange.text = String.format("Range: %.0f km", satPos.range)
radarAltitude.text = String.format("Altitude: %.0f km", satPos.altitude)
}
private fun drawRadarView(cvs: Canvas) {
cvs.drawLine(center - radius, center, center + radius, center, radarPaint)
cvs.drawLine(center, center - radius, center, center + radius, radarPaint)
cvs.drawCircle(center, center, radius, radarPaint)
cvs.drawCircle(center, center, (radius / 3) * 2, radarPaint)
cvs.drawCircle(center, center, radius / 3, radarPaint)
}
private fun drawRadarText(cvs: Canvas) {
cvs.drawText("N", center - txtSize / 3, center - radius - scale * 2, txtPaint)
cvs.drawText("E", center + radius + scale * 2, center + txtSize / 3, txtPaint)
cvs.drawText("S", center - txtSize / 3, center + radius + txtSize, txtPaint)
cvs.drawText("W", center - radius - txtSize, center + txtSize / 3, txtPaint)
cvs.drawText("0°", center + scale, center - scale * 2, txtPaint)
cvs.drawText("30°", center + scale, center - (radius / 3) - scale * 2, txtPaint)
cvs.drawText("60°", center + scale, center - ((radius / 3) * 2) - scale * 2, txtPaint)
}
private fun drawPassTrajectory(cvs: Canvas) {
while (startTime.before(endTime)) {
satPos = satPass.predictor.getSatPos(startTime)
satPassX = center + sph2CartX(satPos.azimuth, satPos.elevation, radius.toDouble())
satPassY = center - sph2CartY(satPos.azimuth, satPos.elevation, radius.toDouble())
if (startTime.compareTo(satPass.pass.startTime) == 0) {
path.moveTo(satPassX, satPassY)
} else {
path.lineTo(satPassX, satPassY)
}
startTime.time += delay
}
cvs.drawPath(path, trackPaint)
}
private fun drawSatellite(cvs: Canvas) {
satPos = satPass.predictor.getSatPos(Date())
if (satPos.elevation > 0) {
cvs.drawCircle(
center + sph2CartX(satPos.azimuth, satPos.elevation, radius.toDouble()),
center - sph2CartY(satPos.azimuth, satPos.elevation, radius.toDouble()),
txtSize / 3, satPaint
)
}
}
private fun sph2CartX(azimuth: Double, elevation: Double, r: Double): Float {
val radius = r * (piDiv2 - elevation) / piDiv2
return (radius * cos(piDiv2 - azimuth)).toFloat()
}
private fun sph2CartY(azimuth: Double, elevation: Double, r: Double): Float {
val radius = r * (piDiv2 - elevation) / piDiv2
return (radius * sin(piDiv2 - azimuth)).toFloat()
}
private fun rad2Deg(value: Double): Double {
return value * 180 / Math.PI
}
}
}
@@ -0,0 +1,80 @@
/*
* LookingSat. Amateur radio & weather satellite tracker and passes calculator.
* Copyright (C) 2019 Arty Bishop (bishop.arty@gmail.com)
*
* This program is free software; you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation; either version 2 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, write to the Free Software Foundation, Inc.,
* 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA.
*/
package com.rtbishop.look4sat.ui
import android.view.LayoutInflater
import android.view.View
import android.view.ViewGroup
import android.widget.TextView
import androidx.core.os.bundleOf
import androidx.navigation.findNavController
import androidx.recyclerview.widget.RecyclerView
import com.rtbishop.look4sat.R
import com.rtbishop.look4sat.repo.SatPass
import java.text.SimpleDateFormat
import java.util.*
class SatPassAdapter : RecyclerView.Adapter<SatPassAdapter.PassHolder>() {
inner class PassHolder(itemView: View) : RecyclerView.ViewHolder(itemView)
private var satPassList = emptyList<SatPass>()
fun setList(list: List<SatPass>) {
satPassList = list
}
override fun getItemCount(): Int {
return satPassList.size
}
override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): PassHolder {
val itemView = LayoutInflater
.from(parent.context)
.inflate(R.layout.sat_pass_card, parent, false)
return PassHolder(itemView)
}
override fun onBindViewHolder(holder: PassHolder, position: Int) {
val satPass = satPassList[position]
val satName = holder.itemView.findViewById<TextView>(R.id.card_sat_name)
val maxEl = holder.itemView.findViewById<TextView>(R.id.card_max_el)
val passVector = holder.itemView.findViewById<TextView>(R.id.card_pass_vector)
val passStart = holder.itemView.findViewById<TextView>(R.id.card_pass_start)
val passEnd = holder.itemView.findViewById<TextView>(R.id.card_pass_end)
val aosTime =
SimpleDateFormat("HH:mm:ss", Locale.getDefault()).format(satPass.pass.startTime)
val losTime =
SimpleDateFormat("HH:mm:ss", Locale.getDefault()).format(satPass.pass.endTime)
satName.text = satPass.tle.name
maxEl.text = String.format("%.1f°", satPass.pass.maxEl)
passVector.text =
String.format("Az: %2d° -> %2d°", satPass.pass.aosAzimuth, satPass.pass.losAzimuth)
passStart.text = String.format("AOS - %s", aosTime)
passEnd.text = String.format("LOS - %s", losTime)
holder.itemView.setOnClickListener {
val bundle = bundleOf("satPass" to satPass)
holder.itemView.findNavController().navigate(R.id.action_nav_sky_to_nav_radar, bundle)
}
}
}
@@ -0,0 +1,66 @@
/*
* LookingSat. Amateur radio & weather satellite tracker and passes calculator.
* Copyright (C) 2019 Arty Bishop (bishop.arty@gmail.com)
*
* This program is free software; you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation; either version 2 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, write to the Free Software Foundation, Inc.,
* 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA.
*/
package com.rtbishop.look4sat.ui
import android.content.SharedPreferences
import android.os.Bundle
import androidx.lifecycle.ViewModelProvider
import androidx.preference.EditTextPreference
import androidx.preference.PreferenceFragmentCompat
import com.rtbishop.look4sat.MainViewModel
import com.rtbishop.look4sat.R
class SettingsFragment : PreferenceFragmentCompat(),
SharedPreferences.OnSharedPreferenceChangeListener {
private lateinit var viewModel: MainViewModel
override fun onCreatePreferences(savedInstanceState: Bundle?, rootKey: String?) {
setPreferencesFromResource(R.xml.preference, rootKey)
viewModel = ViewModelProvider(activity as MainActivity).get(MainViewModel::class.java)
val provider = EditTextPreference.SimpleSummaryProvider.getInstance()
val latPref = findPreference<EditTextPreference>(context!!.getText(R.string.key_lat))
val lonPref = findPreference<EditTextPreference>(context!!.getText(R.string.key_lon))
val altPref = findPreference<EditTextPreference>(context!!.getText(R.string.key_alt))
latPref?.summaryProvider = provider
lonPref?.summaryProvider = provider
altPref?.summaryProvider = provider
}
override fun onSharedPreferenceChanged(sharedPreferences: SharedPreferences?, key: String?) {
if (key == context!!.getText(R.string.key_lat) ||
key == context!!.getText(R.string.key_lon) ||
key == context!!.getText(R.string.key_alt)
) viewModel.updateGsp()
}
override fun onResume() {
super.onResume()
preferenceManager.sharedPreferences.registerOnSharedPreferenceChangeListener(this)
}
override fun onPause() {
super.onPause()
preferenceManager.sharedPreferences.unregisterOnSharedPreferenceChangeListener(this)
}
}
@@ -0,0 +1,177 @@
/*
* LookingSat. Amateur radio & weather satellite tracker and passes calculator.
* Copyright (C) 2019 Arty Bishop (bishop.arty@gmail.com)
*
* This program is free software; you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation; either version 2 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, write to the Free Software Foundation, Inc.,
* 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA.
*/
package com.rtbishop.look4sat.ui
import android.os.Bundle
import android.os.CountDownTimer
import android.view.LayoutInflater
import android.view.View
import android.view.ViewGroup
import android.widget.ImageButton
import android.widget.ProgressBar
import android.widget.TextView
import android.widget.Toast
import androidx.appcompat.app.AlertDialog
import androidx.fragment.app.Fragment
import androidx.lifecycle.ViewModelProvider
import androidx.lifecycle.lifecycleScope
import androidx.recyclerview.widget.LinearLayoutManager
import androidx.recyclerview.widget.RecyclerView
import com.github.amsacode.predict4java.TLE
import com.google.android.material.floatingactionbutton.FloatingActionButton
import com.rtbishop.look4sat.MainViewModel
import com.rtbishop.look4sat.R
import com.rtbishop.look4sat.repo.SatPass
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.launch
import java.util.concurrent.TimeUnit
class SkyFragment : Fragment() {
private lateinit var viewModel: MainViewModel
private lateinit var recViewCurrent: RecyclerView
private lateinit var recViewFuture: RecyclerView
private lateinit var recAdapterCurrent: RecyclerView.Adapter<*>
private lateinit var recAdapterFuture: SatPassAdapter
private lateinit var timeToAos: TextView
private lateinit var btnRefresh: ImageButton
private lateinit var progressBar: ProgressBar
private lateinit var fab: FloatingActionButton
private lateinit var aosTimer: CountDownTimer
private lateinit var satPassList: List<SatPass>
private var isTimerSet: Boolean = false
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
viewModel = ViewModelProvider(activity as MainActivity).get(MainViewModel::class.java)
recAdapterFuture = SatPassAdapter()
}
override fun onCreateView(
inflater: LayoutInflater, container: ViewGroup?, savedInstanceState: Bundle?
): View? {
return inflater.inflate(R.layout.fragment_sky, container, false)
}
override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
super.onViewCreated(view, savedInstanceState)
setupViews(view)
}
private fun setupViews(view: View) {
timeToAos = (activity as MainActivity).findViewById(R.id.toolbar_time_to_aos)
btnRefresh = (activity as MainActivity).findViewById(R.id.toolbar_btn_refresh)
progressBar = view.findViewById(R.id.sky_progressbar)
recViewFuture = view.findViewById(R.id.sky_recycler_future)
fab = view.findViewById(R.id.sky_fab)
satPassList = viewModel.satPassList
recViewFuture.apply {
layoutManager = LinearLayoutManager(activity as MainActivity)
adapter = recAdapterFuture
}
recAdapterFuture.setList(satPassList)
recAdapterFuture.notifyDataSetChanged()
if (satPassList.isEmpty()) {
resetTimer()
}
btnRefresh.setOnClickListener { calculatePasses() }
fab.setOnClickListener {
showSelectSatDialog(viewModel.tleMainList, viewModel.selectionList)
}
}
private fun showSelectSatDialog(tleMainList: List<TLE>, selectionList: MutableList<Int>) {
val tleNameArray = arrayOfNulls<String>(tleMainList.size).apply {
tleMainList.withIndex().forEach { (position, tle) -> this[position] = tle.name }
}
val tleCheckedArray = BooleanArray(tleMainList.size).apply {
selectionList.forEach { this[it] = true }
}
val builder = AlertDialog.Builder(activity as MainActivity)
builder.setTitle(getString(R.string.dialog_select_sat))
.setMultiChoiceItems(tleNameArray, tleCheckedArray) { _, which, isChecked ->
if (isChecked) selectionList.add(which)
else if (selectionList.contains(which)) selectionList.remove(which)
}
.setPositiveButton(getString(R.string.btn_ok)) { _, _ ->
viewModel.updateAndSaveSelectionList(selectionList)
calculatePasses()
}
.setNegativeButton(getString(R.string.btn_cancel)) { dialog, _ ->
dialog.cancel()
}
.setNeutralButton(getString(R.string.btn_clear)) { _, _ ->
selectionList.clear()
viewModel.updateAndSaveSelectionList(selectionList)
calculatePasses()
}
.create()
.show()
}
private fun calculatePasses() {
lifecycleScope.launch(Dispatchers.Main) {
recViewFuture.visibility = View.INVISIBLE
progressBar.visibility = View.VISIBLE
progressBar.isIndeterminate = true
viewModel.getPasses()
satPassList = viewModel.satPassList
recAdapterFuture.setList(satPassList)
recAdapterFuture.notifyDataSetChanged()
progressBar.isIndeterminate = false
progressBar.visibility = View.INVISIBLE
recViewFuture.visibility = View.VISIBLE
if (satPassList.isNotEmpty()) setTimer(satPassList.first().pass.startTime.time)
else resetTimer()
}
}
private fun setTimer(passTime: Long) {
if (isTimerSet) resetTimer()
val totalMillis = passTime.minus(System.currentTimeMillis())
aosTimer = object : CountDownTimer(totalMillis, 1000) {
override fun onFinish() {
Toast.makeText(activity, "Time is up!", Toast.LENGTH_SHORT).show()
this.cancel()
}
override fun onTick(millisUntilFinished: Long) {
timeToAos.text = String.format(
getString(R.string.pattern_aos),
TimeUnit.MILLISECONDS.toHours(millisUntilFinished) % 60,
TimeUnit.MILLISECONDS.toMinutes(millisUntilFinished) % 60,
TimeUnit.MILLISECONDS.toSeconds(millisUntilFinished) % 60
)
}
}
aosTimer.start()
isTimerSet = true
}
private fun resetTimer() {
if (isTimerSet) {
aosTimer.cancel()
isTimerSet = false
}
timeToAos.text = String.format(getString(R.string.pattern_aos), 0, 0, 0)
}
}
@@ -0,0 +1,111 @@
/*
* LookingSat. Amateur radio & weather satellite tracker and passes calculator.
* Copyright (C) 2019 Arty Bishop (bishop.arty@gmail.com)
*
* This program is free software; you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation; either version 2 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, write to the Free Software Foundation, Inc.,
* 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA.
*/
package com.rtbishop.look4sat.ui
import android.view.LayoutInflater
import android.view.View
import android.view.ViewGroup
import android.widget.TextView
import androidx.recyclerview.widget.RecyclerView
import com.rtbishop.look4sat.R
import com.rtbishop.look4sat.repo.Transmitter
class TransAdapter : RecyclerView.Adapter<TransAdapter.TransHolder>() {
inner class TransHolder(itemView: View) : RecyclerView.ViewHolder(itemView)
private var transList = emptyList<Transmitter>()
fun setList(list: List<Transmitter>) {
transList = list
}
override fun getItemCount(): Int {
return transList.size
}
override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): TransHolder {
val itemView = LayoutInflater
.from(parent.context)
.inflate(R.layout.card_transmitter, parent, false)
return TransHolder(itemView)
}
override fun onBindViewHolder(holder: TransHolder, position: Int) {
val context = holder.itemView.context
val freqDivider = 1000000f
val transmitter = transList[position]
val description = holder.itemView.findViewById<TextView>(R.id.trans_description)
val downlink = holder.itemView.findViewById<TextView>(R.id.trans_downlink)
val uplink = holder.itemView.findViewById<TextView>(R.id.trans_uplink)
val mode = holder.itemView.findViewById<TextView>(R.id.trans_mode)
val inverted = holder.itemView.findViewById<TextView>(R.id.trans_inverted)
description.text = transmitter.description
if (transmitter.downlinkLow != null && transmitter.downlinkHigh == null) {
downlink.text = String.format(
context.getString(R.string.pattern_trans_downlink_low),
transmitter.downlinkLow / freqDivider
)
} else if (transmitter.downlinkLow != null && transmitter.downlinkHigh != null) {
downlink.text = String.format(
context.getString(R.string.pattern_trans_downlink_lowHigh),
transmitter.downlinkLow / freqDivider,
transmitter.downlinkHigh / freqDivider
)
} else {
downlink.text = context.getString(R.string.no_downlink)
}
if (transmitter.uplinkLow != null && transmitter.uplinkHigh == null) {
uplink.text = String.format(
context.getString(R.string.pattern_trans_uplink_low),
transmitter.uplinkLow / freqDivider
)
} else if (transmitter.uplinkLow != null && transmitter.uplinkHigh != null) {
uplink.text = String.format(
context.getString(R.string.pattern_trans_uplink_lowHigh),
transmitter.uplinkLow / freqDivider,
transmitter.uplinkHigh / freqDivider
)
} else {
uplink.text = context.getString(R.string.no_uplink)
}
if (transmitter.mode != null) {
mode.text =
String.format(context.getString(R.string.pattern_trans_mode), transmitter.mode)
} else {
mode.text = context.getString(R.string.no_mode)
}
if (transmitter.isInverted) {
inverted.text = String.format(
context.getString(R.string.pattern_trans_inverted),
context.getString(R.string.btn_yes)
)
} else {
inverted.text = String.format(
context.getString(R.string.pattern_trans_inverted),
context.getString(R.string.btn_no)
)
}
}
}
@@ -0,0 +1,263 @@
/*
* LookingSat. Amateur radio & weather satellite tracker and passes calculator.
* Copyright (C) 2019 Arty Bishop (bishop.arty@gmail.com)
*
* This program is free software; you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation; either version 2 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, write to the Free Software Foundation, Inc.,
* 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA.
*/
package com.rtbishop.look4sat.ui
import android.content.Context
import android.graphics.*
import android.os.Bundle
import android.view.LayoutInflater
import android.view.View
import android.view.ViewGroup
import android.widget.FrameLayout
import android.widget.Toast
import androidx.appcompat.app.AlertDialog
import androidx.fragment.app.Fragment
import androidx.lifecycle.ViewModelProvider
import com.github.amsacode.predict4java.PassPredictor
import com.github.amsacode.predict4java.Position
import com.github.amsacode.predict4java.SatPos
import com.github.amsacode.predict4java.TLE
import com.google.android.material.floatingactionbutton.FloatingActionButton
import com.rtbishop.look4sat.MainViewModel
import com.rtbishop.look4sat.R
import java.util.*
import java.util.concurrent.Executors
import java.util.concurrent.TimeUnit
import kotlin.math.abs
class WorldMapFragment : Fragment() {
private val service = Executors.newSingleThreadScheduledExecutor()
private lateinit var viewModel: MainViewModel
private lateinit var trackView: TrackView
private lateinit var mapFrame: FrameLayout
private lateinit var fab: FloatingActionButton
private lateinit var predictor: PassPredictor
private lateinit var mainActivity: MainActivity
private lateinit var selectedSat: TLE
private lateinit var tleMainList: List<TLE>
private lateinit var selectionList: MutableList<Int>
private var checkedItem = 0
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
mainActivity = activity as MainActivity
viewModel = ViewModelProvider(mainActivity).get(MainViewModel::class.java)
}
override fun onCreateView(
inflater: LayoutInflater,
container: ViewGroup?,
savedInstanceState: Bundle?
): View? {
return inflater.inflate(R.layout.fragment_worldmap, container, false)
}
override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
super.onViewCreated(view, savedInstanceState)
val delay = viewModel.updateFreq
tleMainList = viewModel.tleMainList
selectionList = viewModel.selectionList
mapFrame = view.findViewById(R.id.worldmap_frame)
fab = view.findViewById(R.id.worldmap_fab)
if (tleMainList.isNotEmpty() && selectionList.isNotEmpty()) {
fab.setOnClickListener { showSelectSatDialog(tleMainList, selectionList) }
selectedSat = tleMainList[selectionList[0]]
predictor = PassPredictor(selectedSat, viewModel.gsp.value)
trackView = TrackView(mainActivity)
mapFrame.addView(trackView)
service.scheduleAtFixedRate(
{ trackView.invalidate() },
delay,
delay,
TimeUnit.MILLISECONDS
)
} else {
fab.setOnClickListener {
Toast.makeText(
mainActivity,
getString(R.string.no_selected_sat),
Toast.LENGTH_SHORT
)
.show()
}
}
}
private fun showSelectSatDialog(tleMainList: List<TLE>, selectionList: MutableList<Int>) {
val tleNameArray = arrayOfNulls<String>(selectionList.size).apply {
selectionList.withIndex().forEach { (index, selection) ->
this[index] = tleMainList[selection].name
}
}
val builder = AlertDialog.Builder(mainActivity)
builder.setTitle(getString(R.string.dialog_show_track))
.setSingleChoiceItems(tleNameArray, checkedItem) { dialog, which ->
checkedItem = which
selectedSat = tleMainList[selectionList[which]]
predictor = PassPredictor(selectedSat, viewModel.gsp.value)
trackView.invalidate()
dialog.dismiss()
}
.create()
.show()
}
inner class TrackView(context: Context) : View(context) {
private val gsp = viewModel.gsp.value!!
private val scale = resources.displayMetrics.density
private val groundTrackPaint = Paint().apply {
isAntiAlias = true
color = resources.getColor(R.color.satTrack, mainActivity.theme)
style = Paint.Style.STROKE
strokeWidth = scale
}
private val footprintPaint = Paint().apply {
isAntiAlias = true
color = resources.getColor(R.color.satFootprint, mainActivity.theme)
style = Paint.Style.STROKE
strokeWidth = scale
}
private val txtPaint = Paint().apply {
isAntiAlias = true
color = resources.getColor(R.color.satFootprint, mainActivity.theme)
style = Paint.Style.FILL
strokeWidth = scale
textSize = 16f
setShadowLayer(2f, 2f, 2f, Color.BLACK)
}
private val outlinePaint = Paint().apply {
isAntiAlias = true
color = Color.BLACK
style = Paint.Style.FILL_AND_STROKE
strokeWidth = scale
textSize = 16f
}
private val rect = Rect()
override fun onDraw(canvas: Canvas) {
canvas.translate(width / 2f, height / 2f)
val degLon = width / 360f
val degLat = height / 180f
drawHomeLoc(canvas, degLon, degLat)
val currentTime = getDateFor(System.currentTimeMillis())
val orbitalPeriod = (24 * 60 / selectedSat.meanmo).toInt()
val satPosList = predictor.getPositions(currentTime, 60, 0, orbitalPeriod * 3)
drawGroundTrack(canvas, degLon, degLat, satPosList)
selectionList.forEach {
drawSat(it, currentTime, canvas, degLon, degLat)
}
}
private fun drawHomeLoc(cvs: Canvas, degLon: Float, degLat: Float) {
val lon = gsp.longitude.toFloat()
val lat = gsp.latitude.toFloat() * -1
val cx = lon * degLon
val cy = lat * degLat
cvs.drawCircle(cx, cy, scale * 2, txtPaint)
cvs.drawText(
context.getString(R.string.map_gsp),
cx - txtPaint.textSize,
cy - txtPaint.textSize,
txtPaint
)
}
private fun drawGroundTrack(cvs: Canvas, degLon: Float, degLat: Float, list: List<SatPos>) {
val path = Path()
var lon: Float
var lat: Float
var lastLon = 181f
list.withIndex().forEach { (index, satPos) ->
lon = rad2Deg(satPos.longitude).toFloat()
lat = rad2Deg(satPos.latitude).toFloat() * -1
if (lon > 180f) lon -= 360f
lon *= degLon
lat *= degLat
if (index == 0 || abs(lon - lastLon) > 180) path.moveTo(lon, lat)
else path.lineTo(lon, lat)
lastLon = lon
}
cvs.drawPath(path, groundTrackPaint)
}
private fun drawSat(index: Int, date: Date, canvas: Canvas, degLon: Float, degLat: Float) {
val tle = tleMainList[index]
val predictor = PassPredictor(tle, gsp)
val satPosNow = predictor.getSatPos(date)
val footprintPosList = satPosNow.rangeCircle
drawFootprint(canvas, degLon, degLat, footprintPosList)
drawName(canvas, degLon, degLat, satPosNow, tle.name)
}
private fun drawFootprint(cvs: Canvas, degLon: Float, degLat: Float, list: List<Position>) {
val path = Path()
var lon: Float
var lat: Float
var lastLon = 181f
list.withIndex().forEach { (index, position) ->
lon = position.lon.toFloat()
lat = position.lat.toFloat() * -1
if (lon > 180f) lon -= 360f
lon *= degLon
lat *= degLat
if (index == 0 || abs(lon - lastLon) > 180) path.moveTo(lon, lat)
else path.lineTo(lon, lat)
lastLon = lon
}
cvs.drawPath(path, footprintPaint)
}
private fun drawName(cvs: Canvas, degLon: Float, degLat: Float, pos: SatPos, name: String) {
var lon = rad2Deg(pos.longitude).toFloat()
val lat = rad2Deg(pos.latitude).toFloat() * -1
if (lon > 180f) lon -= 360f
val cx = lon * degLon
val cy = lat * degLat
cvs.drawCircle(cx, cy, scale * 2, txtPaint)
txtPaint.getTextBounds(name, 0, name.length, rect)
cvs.drawText(name, cx - rect.width() / 2, cy - txtPaint.textSize, outlinePaint)
cvs.drawText(name, cx - rect.width() / 2, cy - txtPaint.textSize, txtPaint)
}
private fun getDateFor(value: Long): Date {
return Date(value)
}
private fun rad2Deg(value: Double): Double {
return value * 180 / Math.PI
}
}
}