Added PassesViewModel.kt

This commit is contained in:
Arty Bishop committed 2021-03-20 16:44:40 +00:00
1 parent d32e5a5ce3
commit 4edf316b5e
21 files changed
+106 -99

No files matched your search

+1 -1
View File
@@ -23,4 +23,4 @@
</activity> </activity>
</application> </application>
</manifest> </manifest>
@@ -29,4 +29,4 @@ class Look4SatApp : Application() {
Timber.plant(Timber.DebugTree()) Timber.plant(Timber.DebugTree())
} }
} }
} }
@@ -41,8 +41,10 @@ class PassesRepo @Inject constructor(
private var selectedSatellites = emptyList<Satellite>() private var selectedSatellites = emptyList<Satellite>()
suspend fun calculatePasses(satellites: List<Satellite>) { suspend fun calculatePasses(satellites: List<Satellite>) {
if (satellites !== selectedSatellites) { withContext(defaultDispatcher) {
withContext(defaultDispatcher) { val newCatNums = satellites.map { it.tle.catnum }
val oldCatNums = selectedSatellites.map { it.tle.catnum }
if (newCatNums != oldCatNums) {
_passes.value = Result.InProgress _passes.value = Result.InProgress
selectedSatellites = satellites selectedSatellites = satellites
val refDate = Date(System.currentTimeMillis()) val refDate = Date(System.currentTimeMillis())
@@ -75,4 +77,4 @@ class PassesRepo @Inject constructor(
passes.sortBy { it.pass.startTime } passes.sortBy { it.pass.startTime }
return passes return passes
} }
} }
@@ -43,4 +43,4 @@ class MainActivity : AppCompatActivity() {
if (prefsRepo.isFirstLaunch()) navHost.navController.navigate(R.id.nav_dialog_splash) if (prefsRepo.isFirstLaunch()) navHost.navController.navigate(R.id.nav_dialog_splash)
} }
} }
} }
@@ -17,26 +17,24 @@
*/ */
package com.rtbishop.look4sat.ui package com.rtbishop.look4sat.ui
import androidx.lifecycle.* import androidx.lifecycle.LiveData
import com.github.amsacode.predict4java.Satellite import androidx.lifecycle.ViewModel
import androidx.lifecycle.asLiveData
import androidx.lifecycle.liveData
import com.rtbishop.look4sat.data.model.Result import com.rtbishop.look4sat.data.model.Result
import com.rtbishop.look4sat.data.model.SatPass import com.rtbishop.look4sat.data.model.SatPass
import com.rtbishop.look4sat.data.repository.PrefsRepo import com.rtbishop.look4sat.data.repository.PrefsRepo
import com.rtbishop.look4sat.data.repository.SatelliteRepo import com.rtbishop.look4sat.data.repository.SatelliteRepo
import com.rtbishop.look4sat.utility.getPredictor
import dagger.hilt.android.lifecycle.HiltViewModel import dagger.hilt.android.lifecycle.HiltViewModel
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.FlowPreview import kotlinx.coroutines.FlowPreview
import kotlinx.coroutines.delay import kotlinx.coroutines.delay
import kotlinx.coroutines.flow.MutableStateFlow import kotlinx.coroutines.flow.MutableStateFlow
import kotlinx.coroutines.launch
import java.util.*
import javax.inject.Inject import javax.inject.Inject
@FlowPreview @FlowPreview
@HiltViewModel @HiltViewModel
class SharedViewModel @Inject constructor( class SharedViewModel @Inject constructor(
private val prefsRepo: PrefsRepo, prefsRepo: PrefsRepo,
private val satelliteRepo: SatelliteRepo, private val satelliteRepo: SatelliteRepo,
) : ViewModel() { ) : ViewModel() {
@@ -47,7 +45,6 @@ class SharedViewModel @Inject constructor(
if (prefsRepo.isFirstLaunch()) { if (prefsRepo.isFirstLaunch()) {
prefsRepo.setFirstLaunchDone() prefsRepo.setFirstLaunchDone()
} }
calculatePasses()
} }
fun getAppTimer() = liveData { fun getAppTimer() = liveData {
@@ -58,36 +55,4 @@ class SharedViewModel @Inject constructor(
} }
fun getTransmittersForSat(satId: Int) = satelliteRepo.getSatTransmitters(satId).asLiveData() fun getTransmittersForSat(satId: Int) = satelliteRepo.getSatTransmitters(satId).asLiveData()
fun calculatePasses(dateNow: Date = Date(System.currentTimeMillis())) {
_passes.value = Result.InProgress
viewModelScope.launch(Dispatchers.Default) {
val passes = mutableListOf<SatPass>()
satelliteRepo.getSelectedSatellites().forEach { satellite ->
passes.addAll(getPasses(satellite, dateNow))
}
val filteredPasses = sortList(passes, dateNow)
_passes.value = Result.Success(filteredPasses)
}
}
private fun getPasses(satellite: Satellite, dateNow: Date): MutableList<SatPass> {
val predictor = satellite.getPredictor(prefsRepo.getStationPosition())
val passes = predictor.getPasses(dateNow, prefsRepo.getHoursAhead(), true)
val passList = passes.map { SatPass(satellite.tle, predictor, it) }
return passList as MutableList<SatPass>
}
private fun sortList(passes: MutableList<SatPass>, dateNow: Date): MutableList<SatPass> {
val hoursAhead = prefsRepo.getHoursAhead()
val dateFuture = Calendar.getInstance().apply {
this.time = dateNow
this.add(Calendar.HOUR, hoursAhead)
}.time
passes.removeAll { it.pass.startTime.after(dateFuture) }
passes.removeAll { it.pass.endTime.before(dateNow) }
passes.removeAll { it.pass.maxEl < prefsRepo.getMinElevation() }
passes.sortBy { it.pass.startTime }
return passes
}
} }
@@ -145,4 +145,4 @@ class EntriesFragment : Fragment(R.layout.fragment_entries), EntriesAdapter.Entr
binding = null binding = null
super.onDestroyView() super.onDestroyView()
} }
} }
@@ -79,4 +79,4 @@ class EntriesViewModel @Inject constructor(
satelliteRepo.updateEntriesSelection(items, isSelected) satelliteRepo.updateEntriesSelection(items, isSelected)
} }
} }
} }
@@ -204,4 +204,4 @@ class MapFragment : Fragment(R.layout.fragment_map) {
) )
return XYTileSource("wikimedia", 0, 6, 256, ".png", sources, copyright, policy) return XYTileSource("wikimedia", 0, 6, 256, ".png", sources, copyright, policy)
} }
} }
@@ -196,4 +196,4 @@ class MapViewModel @Inject constructor(
Position(osmLat, osmLon) Position(osmLat, osmLon)
} }
} }
} }
@@ -20,32 +20,26 @@ package com.rtbishop.look4sat.ui.passesScreen
import android.os.Bundle import android.os.Bundle
import android.view.View import android.view.View
import androidx.fragment.app.Fragment import androidx.fragment.app.Fragment
import androidx.fragment.app.activityViewModels import androidx.fragment.app.viewModels
import androidx.recyclerview.widget.LinearLayoutManager import androidx.recyclerview.widget.LinearLayoutManager
import androidx.recyclerview.widget.SimpleItemAnimator import androidx.recyclerview.widget.SimpleItemAnimator
import com.rtbishop.look4sat.R import com.rtbishop.look4sat.R
import com.rtbishop.look4sat.data.model.Result import com.rtbishop.look4sat.data.model.Result
import com.rtbishop.look4sat.data.model.SatPass import com.rtbishop.look4sat.data.model.SatPass
import com.rtbishop.look4sat.databinding.FragmentPassesBinding import com.rtbishop.look4sat.databinding.FragmentPassesBinding
import com.rtbishop.look4sat.ui.SharedViewModel
import com.rtbishop.look4sat.data.repository.PrefsRepo
import com.rtbishop.look4sat.utility.RecyclerDivider import com.rtbishop.look4sat.utility.RecyclerDivider
import com.rtbishop.look4sat.utility.formatForTimer import com.rtbishop.look4sat.utility.formatForTimer
import dagger.hilt.android.AndroidEntryPoint import dagger.hilt.android.AndroidEntryPoint
import kotlinx.coroutines.FlowPreview import kotlinx.coroutines.FlowPreview
import java.util.* import java.util.*
import javax.inject.Inject
@FlowPreview @FlowPreview
@AndroidEntryPoint @AndroidEntryPoint
class PassesFragment : Fragment(R.layout.fragment_passes) { class PassesFragment : Fragment(R.layout.fragment_passes) {
@Inject
lateinit var prefsRepo: PrefsRepo
private var binding: FragmentPassesBinding? = null private var binding: FragmentPassesBinding? = null
private var passesAdapter: PassesAdapter? = null private var passesAdapter: PassesAdapter? = null
private val viewModel: SharedViewModel by activityViewModels() private val viewModel: PassesViewModel by viewModels()
private var passes = mutableListOf<SatPass>() private var passes = mutableListOf<SatPass>()
override fun onViewCreated(view: View, savedInstanceState: Bundle?) { override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
@@ -90,7 +84,7 @@ class PassesFragment : Fragment(R.layout.fragment_passes) {
} }
private fun setupComponents() { private fun setupComponents() {
passesAdapter = PassesAdapter(requireContext(), prefsRepo.shouldUseUTC()) passesAdapter = PassesAdapter(requireContext(), viewModel.shouldUseUTC())
binding?.apply { binding?.apply {
passesRecycler.apply { passesRecycler.apply {
setHasFixedSize(true) setHasFixedSize(true)
@@ -100,7 +94,6 @@ class PassesFragment : Fragment(R.layout.fragment_passes) {
(itemAnimator as SimpleItemAnimator).supportsChangeAnimations = false (itemAnimator as SimpleItemAnimator).supportsChangeAnimations = false
addItemDecoration(RecyclerDivider(R.drawable.rec_divider_dark)) addItemDecoration(RecyclerDivider(R.drawable.rec_divider_dark))
} }
passesFab.setOnClickListener { viewModel.calculatePasses() }
} }
} }
@@ -0,0 +1,60 @@
/*
* Look4Sat. Amateur radio satellite tracker and pass predictor.
* Copyright (C) 2019-2021 Arty Bishop (bishop.arty@gmail.com)
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with this program. If not, see <https://www.gnu.org/licenses/>.
*/
package com.rtbishop.look4sat.ui.passesScreen
import androidx.lifecycle.ViewModel
import androidx.lifecycle.asLiveData
import androidx.lifecycle.liveData
import androidx.lifecycle.viewModelScope
import com.rtbishop.look4sat.data.repository.PassesRepo
import com.rtbishop.look4sat.data.repository.PrefsRepo
import com.rtbishop.look4sat.data.repository.SatelliteRepo
import dagger.hilt.android.lifecycle.HiltViewModel
import kotlinx.coroutines.delay
import kotlinx.coroutines.flow.collect
import kotlinx.coroutines.launch
import javax.inject.Inject
@HiltViewModel
class PassesViewModel @Inject constructor(
private val prefsRepo: PrefsRepo,
private val satelliteRepo: SatelliteRepo,
private val passesRepo: PassesRepo
) : ViewModel() {
val passes = passesRepo.passes.asLiveData()
init {
viewModelScope.launch {
satelliteRepo.getSelectedSatellitesFlow().collect { selectedSatellites ->
passesRepo.calculatePasses(selectedSatellites)
}
}
}
fun getAppTimer() = liveData {
while (true) {
emit(System.currentTimeMillis())
delay(1000)
}
}
fun shouldUseUTC(): Boolean {
return prefsRepo.shouldUseUTC()
}
}
@@ -127,4 +127,4 @@ class PolarView(context: Context) : View(context) {
val radius = r * (piDiv2 - elevation) / piDiv2 val radius = r * (piDiv2 - elevation) / piDiv2
return (radius * sin(piDiv2 - azimuth)).toFloat() return (radius * sin(piDiv2 - azimuth)).toFloat()
} }
} }
@@ -71,4 +71,4 @@ class SourcesAdapter : RecyclerView.Adapter<SourcesAdapter.TleSourceHolder>() {
} }
} }
} }
} }
@@ -63,4 +63,4 @@ class SourcesDialog : AppCompatDialogFragment() {
tleSourcesBtnNeg.setOnClickListener { dismiss() } tleSourcesBtnNeg.setOnClickListener { dismiss() }
} }
} }
} }
@@ -42,4 +42,4 @@ class SplashDialog : AppCompatDialogFragment() {
splashButton.setOnClickListener { dismiss() } splashButton.setOnClickListener { dismiss() }
} }
} }
} }
@@ -26,8 +26,7 @@ import java.util.*
class PassPredictor(private val satellite: Satellite, private val qth: GroundStationPosition) { class PassPredictor(private val satellite: Satellite, private val qth: GroundStationPosition) {
private val oneQuarterOrbitMin = (24.0 * 60.0 / satellite.tle.meanmo / 4.0).toInt() private val oneQuarterOrbitMin = (24.0 * 60.0 / satellite.tle.meanmo / 4.0).toInt()
private val speedOfLight = 2.99792458E8 private val speedOfLight = 2.99792458E8
private val polePassed = "none"
fun getDownlinkFreq(freq: Long, date: Date): Long { fun getDownlinkFreq(freq: Long, date: Date): Long {
val rangeRate = getSatPos(date).rangeRate val rangeRate = getSatPos(date).rangeRate
return (freq.toDouble() * (speedOfLight - rangeRate * 1000.0) / speedOfLight).toLong() return (freq.toDouble() * (speedOfLight - rangeRate * 1000.0) / speedOfLight).toLong()
@@ -47,7 +46,6 @@ class PassPredictor(private val satellite: Satellite, private val qth: GroundSta
val endDate = Date(refDate.time + minAfter * 60L * 1000L) val endDate = Date(refDate.time + minAfter * 60L * 1000L)
val startDate = Date(refDate.time - minBefore * 60L * 1000L) val startDate = Date(refDate.time - minBefore * 60L * 1000L)
var currentDate = startDate var currentDate = startDate
while (currentDate.before(endDate)) { while (currentDate.before(endDate)) {
positions.add(getSatPos(currentDate)) positions.add(getSatPos(currentDate))
currentDate = Date(currentDate.time + stepSeconds * 1000) currentDate = Date(currentDate.time + stepSeconds * 1000)
@@ -62,7 +60,6 @@ class PassPredictor(private val satellite: Satellite, private val qth: GroundSta
var shouldWindBack = windBack var shouldWindBack = windBack
var lastAosDate: Date var lastAosDate: Date
var count = 0 var count = 0
if (satellite.willBeSeen(qth)) { if (satellite.willBeSeen(qth)) {
if (satellite.tle.isDeepspace) { if (satellite.tle.isDeepspace) {
passes.add(nextDeepSpacePass(refDate)) passes.add(nextDeepSpacePass(refDate))
@@ -87,7 +84,7 @@ class PassPredictor(private val satellite: Satellite, private val qth: GroundSta
val aosAzimuth = (satPos.azimuth / (2.0 * Math.PI) * 360.0).toInt() val aosAzimuth = (satPos.azimuth / (2.0 * Math.PI) * 360.0).toInt()
val losAzimuth = (satPos.azimuth / (2.0 * Math.PI) * 360.0).toInt() val losAzimuth = (satPos.azimuth / (2.0 * Math.PI) * 360.0).toInt()
val maxEl = satPos.elevation / (2.0 * Math.PI) * 360.0 val maxEl = satPos.elevation / (2.0 * Math.PI) * 360.0
return SatPassTime(startDate, endDate, polePassed, aosAzimuth, losAzimuth, maxEl) return SatPassTime(startDate, endDate, "pole", aosAzimuth, losAzimuth, maxEl)
} }
private fun nextNearEarthPass(refDate: Date, windBack: Boolean = false): SatPassTime { private fun nextNearEarthPass(refDate: Date, windBack: Boolean = false): SatPassTime {
@@ -101,27 +98,26 @@ class PassPredictor(private val satellite: Satellite, private val qth: GroundSta
// wind back time 1/4 of an orbit // wind back time 1/4 of an orbit
if (windBack) calendar.add(Calendar.MINUTE, -oneQuarterOrbitMin) if (windBack) calendar.add(Calendar.MINUTE, -oneQuarterOrbitMin)
var satPos = getSatPos(calendar.time) var satPos = getSatPos(calendar.time)
// test for the elevation being above the horizon
if (satPos.elevation > 0.0) { if (satPos.elevation > 0.0) {
// move time forward in 30 second intervals until the sat goes below the horizon // move forward in 30 second intervals until the sat goes below the horizon
do { do {
calendar.add(Calendar.SECOND, 30) calendar.add(Calendar.SECOND, 30)
satPos = getSatPos(calendar.time) satPos = getSatPos(calendar.time)
} while (satPos.elevation > 0.0) } while (satPos.elevation > 0.0)
// move time forward 3/4 orbit // move forward 3/4 of an orbit
calendar.add(Calendar.MINUTE, oneQuarterOrbitMin * 3) calendar.add(Calendar.MINUTE, oneQuarterOrbitMin * 3)
} }
// find the next time it comes above the horizon // find the next time sat comes above the horizon
do { do {
calendar.add(Calendar.SECOND, 60) calendar.add(Calendar.SECOND, 60)
satPos = getSatPos(calendar.time) satPos = getSatPos(calendar.time)
elevation = satPos.elevation elevation = satPos.elevation
if (elevation > maxElevation) maxElevation = elevation if (elevation > maxElevation) maxElevation = elevation
} while (satPos.elevation < 0.0) } while (satPos.elevation < 0.0)
// refine it to 3 seconds // refine to 3 seconds
calendar.add(Calendar.SECOND, -60) calendar.add(Calendar.SECOND, -60)
do { do {
calendar.add(Calendar.SECOND, 3) calendar.add(Calendar.SECOND, 3)
@@ -129,19 +125,19 @@ class PassPredictor(private val satellite: Satellite, private val qth: GroundSta
elevation = satPos.elevation elevation = satPos.elevation
if (elevation > maxElevation) maxElevation = elevation if (elevation > maxElevation) maxElevation = elevation
} while (satPos.elevation < 0.0) } while (satPos.elevation < 0.0)
val startDate = satPos.time val startDate = satPos.time
val aosAzimuth = (satPos.azimuth / (2.0 * Math.PI) * 360.0).toInt() val aosAzimuth = (satPos.azimuth / (2.0 * Math.PI) * 360.0).toInt()
// now find when it goes below // find when sat goes below
do { do {
calendar.add(Calendar.SECOND, 30) calendar.add(Calendar.SECOND, 30)
satPos = getSatPos(calendar.time) satPos = getSatPos(calendar.time)
elevation = satPos.elevation elevation = satPos.elevation
if (elevation > maxElevation) maxElevation = elevation if (elevation > maxElevation) maxElevation = elevation
} while (satPos.elevation > 0.0) } while (satPos.elevation > 0.0)
// refine it to 3 seconds // refine to 3 seconds
calendar.add(Calendar.SECOND, -30) calendar.add(Calendar.SECOND, -30)
do { do {
calendar.add(Calendar.SECOND, 3) calendar.add(Calendar.SECOND, 3)
@@ -149,10 +145,10 @@ class PassPredictor(private val satellite: Satellite, private val qth: GroundSta
elevation = satPos.elevation elevation = satPos.elevation
if (elevation > maxElevation) maxElevation = elevation if (elevation > maxElevation) maxElevation = elevation
} while (satPos.elevation > 0.0) } while (satPos.elevation > 0.0)
val endDate = satPos.time val endDate = satPos.time
val losAzimuth = (satPos.azimuth / (2.0 * Math.PI) * 360.0).toInt() val losAzimuth = (satPos.azimuth / (2.0 * Math.PI) * 360.0).toInt()
val maxEl = maxElevation / (2.0 * Math.PI) * 360.0 val maxEl = maxElevation / (2.0 * Math.PI) * 360.0
return SatPassTime(startDate, endDate, polePassed, aosAzimuth, losAzimuth, maxEl) return SatPassTime(startDate, endDate, "pole", aosAzimuth, losAzimuth, maxEl)
} }
} }
@@ -60,4 +60,4 @@ class QthConverter @Inject constructor() {
private fun isValidLocation(lat: Double, lon: Double): Boolean { private fun isValidLocation(lat: Double, lon: Double): Boolean {
return (lat > -90.0 && lat < 90.0) && (lon > -180.0 && lon < 360.0) return (lat > -90.0 && lat < 90.0) && (lon > -180.0 && lon < 360.0)
} }
} }
@@ -23,7 +23,7 @@ import androidx.recyclerview.widget.RecyclerView
class RecyclerDivider(private val resId: Int) : RecyclerView.ItemDecoration() { class RecyclerDivider(private val resId: Int) : RecyclerView.ItemDecoration() {
override fun onDrawOver(c: Canvas, parent: RecyclerView) { override fun onDrawOver(canvas: Canvas, parent: RecyclerView) {
ContextCompat.getDrawable(parent.context, resId)?.let { drawable -> ContextCompat.getDrawable(parent.context, resId)?.let { drawable ->
val leftPos = parent.paddingLeft val leftPos = parent.paddingLeft
val rightPos = parent.width - parent.paddingRight val rightPos = parent.width - parent.paddingRight
@@ -35,10 +35,10 @@ class RecyclerDivider(private val resId: Int) : RecyclerView.ItemDecoration() {
val topPos = view.bottom + params.bottomMargin val topPos = view.bottom + params.bottomMargin
val bottomPos = topPos + drawable.intrinsicHeight val bottomPos = topPos + drawable.intrinsicHeight
drawable.setBounds(leftPos, topPos, rightPos, bottomPos) drawable.setBounds(leftPos, topPos, rightPos, bottomPos)
drawable.draw(c) drawable.draw(canvas)
} }
} }
} }
} }
} }
} }
@@ -65,13 +65,4 @@
</androidx.constraintlayout.widget.ConstraintLayout> </androidx.constraintlayout.widget.ConstraintLayout>
<com.google.android.material.floatingactionbutton.FloatingActionButton
android:id="@+id/passes_fab"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_gravity="bottom|center"
android:layout_marginBottom="@dimen/fab_padding"
android:contentDescription="@string/placeholder"
android:src="@drawable/ic_refresh" />
</androidx.coordinatorlayout.widget.CoordinatorLayout> </androidx.coordinatorlayout.widget.CoordinatorLayout>
@@ -39,4 +39,4 @@ class CoroutineTestRule(private val dispatcher: TestCoroutineDispatcher = TestCo
Dispatchers.resetMain() Dispatchers.resetMain()
dispatcher.cleanupTestCoroutines() dispatcher.cleanupTestCoroutines()
} }
} }
@@ -57,4 +57,4 @@ class QthConverterTest {
assert(converter.locationToQTH(91.0542, -170.1142) == null) assert(converter.locationToQTH(91.0542, -170.1142) == null)
assert(converter.locationToQTH(89.0542, -240.1142) == null) assert(converter.locationToQTH(89.0542, -240.1142) == null)
} }
} }