Added MapViewModel.kt, cleaned up MapFragment.kt

This commit is contained in:
Arty Bishop committed 2020-11-26 19:27:46 +00:00
1 parent 19a0649cb3
commit b850c7a41d
3 files changed
+200 -146

No files matched your search

@@ -1,28 +1,24 @@
package com.rtbishop.look4sat.ui.mainScreen
import android.content.SharedPreferences
import android.graphics.Color
import android.graphics.ColorMatrix
import android.graphics.ColorMatrixColorFilter
import android.graphics.Paint
import android.graphics.drawable.Drawable
import android.os.Bundle
import android.view.View
import androidx.core.content.ContextCompat
import androidx.fragment.app.Fragment
import androidx.fragment.app.activityViewModels
import androidx.lifecycle.lifecycleScope
import androidx.fragment.app.viewModels
import com.github.amsacode.predict4java.GroundStationPosition
import com.github.amsacode.predict4java.Position
import com.rtbishop.look4sat.R
import com.rtbishop.look4sat.SharedViewModel
import com.rtbishop.look4sat.data.Result
import com.rtbishop.look4sat.data.SatPass
import com.rtbishop.look4sat.databinding.FragmentMapBinding
import com.rtbishop.look4sat.utility.PrefsManager
import dagger.hilt.android.AndroidEntryPoint
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.delay
import kotlinx.coroutines.launch
import kotlinx.coroutines.withContext
import org.osmdroid.config.Configuration
import org.osmdroid.tileprovider.tilesource.ITileSource
import org.osmdroid.tileprovider.tilesource.TileSourcePolicy
@@ -33,53 +29,35 @@ import org.osmdroid.views.CustomZoomButtonsController
import org.osmdroid.views.overlay.*
import java.util.*
import javax.inject.Inject
import kotlin.math.pow
import kotlin.math.sqrt
@AndroidEntryPoint
class MapFragment : Fragment(R.layout.fragment_map) {
@Inject
lateinit var prefsManager: PrefsManager
private lateinit var binding: FragmentMapBinding
lateinit var preferences: SharedPreferences
private lateinit var trackPaint: Paint
private lateinit var footprintPaint: Paint
private lateinit var selectedPass: SatPass
private lateinit var allPasses: List<SatPass>
private lateinit var nameFormat: String
private lateinit var altFormat: String
private lateinit var distFormat: String
private lateinit var velFormat: String
private lateinit var latLonFormat: String
private lateinit var selectedSat: SatPass
private val dateNow = Date()
private val viewModel: SharedViewModel by activityViewModels()
private var iconPos: Drawable? = null
private var iconSat: Drawable? = null
private val viewModel: MapViewModel by viewModels()
private val sharedViewModel: SharedViewModel by activityViewModels()
private var binding: FragmentMapBinding? = null
override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
super.onViewCreated(view, savedInstanceState)
binding = FragmentMapBinding.bind(view)
setupComponents(view)
setupMapView()
setupPosOverlay()
setupObservers()
}
private fun setupMapView() {
Configuration.getInstance()
.load(requireContext().applicationContext, prefsManager.preferences)
val filter = getColorFilter(ContextCompat.getColor(requireContext(), R.color.satTrack))
binding.mapView.apply {
setMultiTouchControls(true)
setTileSource(getWikimediaTileSource())
minZoomLevel = 2.5
maxZoomLevel = 6.0
controller.setZoom(3.0)
zoomController.setVisibility(CustomZoomButtonsController.Visibility.NEVER)
overlayManager.tilesOverlay.loadingBackgroundColor = Color.TRANSPARENT
overlayManager.tilesOverlay.loadingLineColor = Color.TRANSPARENT
overlayManager.tilesOverlay.setColorFilter(filter)
setScrollableAreaLimitDouble(BoundingBox(85.05, 180.0, -85.05, -180.0))
// add overlays: 0 - GSP, 1 - SatTrack, 2 - SatFootprint, 3 - SatIcons
val layers = listOf(FolderOverlay(), FolderOverlay(), FolderOverlay(), FolderOverlay())
overlays.addAll(layers)
}
private fun setupComponents(view: View) {
Configuration.getInstance().load(requireContext().applicationContext, preferences)
binding = FragmentMapBinding.bind(view)
trackPaint = Paint().apply {
strokeWidth = 2f
style = Paint.Style.STROKE
@@ -93,119 +71,105 @@ class MapFragment : Fragment(R.layout.fragment_map) {
color = ContextCompat.getColor(requireContext(), R.color.satFootprint)
isAntiAlias = true
}
iconPos = ContextCompat.getDrawable(requireContext(), R.drawable.ic_map_pos)
iconSat = ContextCompat.getDrawable(requireContext(), R.drawable.ic_map_sat)
nameFormat = getString(R.string.pat_osm_idName)
altFormat = getString(R.string.pat_altitude)
distFormat = getString(R.string.pat_distance)
velFormat = getString(R.string.pat_osm_vel)
latLonFormat = getString(R.string.pat_osm_latLon)
}
private fun getWikimediaTileSource(): ITileSource {
val copyright = resources.getString(R.string.map_copyright)
val sources = arrayOf("https://maps.wikimedia.org/osm-intl/")
val policy = TileSourcePolicy(
1, TileSourcePolicy.FLAG_NO_BULK and TileSourcePolicy.FLAG_NO_PREVENTIVE and
TileSourcePolicy.FLAG_USER_AGENT_MEANINGFUL and TileSourcePolicy.FLAG_USER_AGENT_NORMALIZED
)
return XYTileSource("wikimedia", 2, 6, 256, ".png", sources, copyright, policy)
}
private fun setupPosOverlay() {
val gsp = prefsManager.getStationPosition()
Marker(binding.mapView).apply {
setInfoWindow(null)
setAnchor(Marker.ANCHOR_CENTER, Marker.ANCHOR_BOTTOM)
icon = iconPos
position = GeoPoint(gsp.latitude, gsp.longitude)
binding.mapView.overlays[0] = this
binding.mapView.invalidate()
private fun setupMapView() {
binding?.apply {
mapView.apply {
setMultiTouchControls(true)
setTileSource(getTileSource())
minZoomLevel = 2.5
maxZoomLevel = 6.0
controller.setZoom(3.0)
zoomController.setVisibility(CustomZoomButtonsController.Visibility.NEVER)
overlayManager.tilesOverlay.loadingBackgroundColor = Color.TRANSPARENT
overlayManager.tilesOverlay.loadingLineColor = Color.TRANSPARENT
overlayManager.tilesOverlay.setColorFilter(getColorFilter())
setScrollableAreaLimitDouble(BoundingBox(85.05, 180.0, -85.05, -180.0))
// add overlays: 0 - GSP, 1 - SatTrack, 2 - SatFootprint, 3 - SatIcons
overlays.addAll(Array(4) { FolderOverlay() })
}
}
}
private fun setupObservers() {
viewModel.getPasses().observe(viewLifecycleOwner, { satPasses ->
if (satPasses is Result.Success) {
if (satPasses.data.isNotEmpty()) {
allPasses = satPasses.data.distinctBy { it.tle }
selectedPass = allPasses[0]
binding.fabPrev.setOnClickListener { changeSelection(true) }
binding.fabNext.setOnClickListener { changeSelection(false) }
setupSatOverlay(allPasses)
}
viewModel.getGSP().observe(viewLifecycleOwner, { setupPosOverlay(it) })
sharedViewModel.getPasses().observe(viewLifecycleOwner, {
if (it is Result.Success && it.data.isNotEmpty()) {
viewModel.setPasses(it.data)
binding?.fabPrev?.setOnClickListener { viewModel.changeSelection(true) }
binding?.fabNext?.setOnClickListener { viewModel.changeSelection(false) }
}
})
viewModel.getSelectedPass().observe(viewLifecycleOwner, { satPass ->
satPass?.let {
selectedSat = satPass
setSelectedSatDetails(satPass)
scrollToSat(satPass)
}
})
viewModel.getSatMarkers().observe(viewLifecycleOwner, {
setMarkers(it)
setSelectedSatDetails(selectedSat)
})
}
private fun setupSatOverlay(passList: List<SatPass>) {
lifecycleScope.launch {
while (true) {
dateNow.time = System.currentTimeMillis()
setSatDetails(selectedPass)
binding.mapView.overlays[3] = getSatMarkers(passList)
binding.mapView.invalidate()
delay(2000)
private fun setupPosOverlay(gsp: GroundStationPosition) {
binding?.apply {
Marker(mapView).apply {
setInfoWindow(null)
setAnchor(Marker.ANCHOR_CENTER, Marker.ANCHOR_BOTTOM)
icon = ContextCompat.getDrawable(requireContext(), R.drawable.ic_map_pos)
position = GeoPoint(gsp.latitude, gsp.longitude)
mapView.overlays[0] = this
mapView.invalidate()
}
}
}
private fun changeSelection(decrement: Boolean) {
val index = allPasses.indexOf(selectedPass)
selectedPass = if (decrement) {
if (index > 0) allPasses[index - 1] else allPasses[allPasses.size - 1]
} else {
if (index < allPasses.size - 1) allPasses[index + 1] else allPasses[0]
private fun setSelectedSatDetails(satPass: SatPass) {
val satPos = satPass.predictor.getSatPos(dateNow)
val osmPos = viewModel.getOsmPosition(satPos.latitude, satPos.longitude, true)
binding?.apply {
idName.text = String.format(nameFormat, satPass.tle.catnum, satPass.tle.name)
altitude.text = String.format(altFormat, satPos.altitude)
distance.text = String.format(distFormat, satPos.range)
velocity.text = String.format(velFormat, viewModel.getSatVelocity(satPos.altitude))
latLon.text = String.format(latLonFormat, osmPos.lat, osmPos.lon)
mapView.overlays[1] = getSatTrack(satPass)
mapView.overlays[2] = getSatFootprint(satPass)
mapView.invalidate()
}
val satPos = selectedPass.predictor.getSatPos(dateNow)
val osmPos = getOsmPosition(satPos.latitude, satPos.longitude, true)
setSatDetails(selectedPass)
binding.mapView.controller.animateTo(GeoPoint(osmPos.lat, osmPos.lon))
}
private suspend fun getSatMarkers(passList: List<SatPass>): Overlay =
withContext(Dispatchers.Default) {
val items = FolderOverlay()
passList.forEach { satPass ->
val satPos = satPass.predictor.getSatPos(dateNow)
val osmPos = getOsmPosition(satPos.latitude, satPos.longitude, true)
Marker(binding.mapView).apply {
private fun scrollToSat(satPass: SatPass) {
val satPos = satPass.predictor.getSatPos(dateNow)
val osmPos = viewModel.getOsmPosition(satPos.latitude, satPos.longitude, true)
binding?.mapView?.controller?.animateTo(GeoPoint(osmPos.lat, osmPos.lon))
}
private fun setMarkers(map: Map<SatPass, Position>) {
dateNow.time = System.currentTimeMillis()
binding?.apply {
val markers = FolderOverlay()
map.entries.forEach {
Marker(mapView).apply {
setInfoWindow(null)
setAnchor(Marker.ANCHOR_CENTER, Marker.ANCHOR_CENTER)
icon = iconSat
position = GeoPoint(osmPos.lat, osmPos.lon)
setOnMarkerClickListener { _, _ ->
selectedPass = satPass
setSatDetails(satPass)
true
}
items.add(this)
icon = ContextCompat.getDrawable(requireContext(), R.drawable.ic_map_sat)
position = GeoPoint(it.value.lat, it.value.lon)
setOnMarkerClickListener { _, _ -> viewModel.setSelectedSat(it.key) }
markers.add(this)
}
}
return@withContext items
mapView.overlays[3] = markers
mapView.invalidate()
}
private fun setSatDetails(satPass: SatPass) {
setSatInfo(satPass)
binding.mapView.overlays[1] = getSatTrack(satPass)
binding.mapView.overlays[2] = getSatFootprint(satPass)
}
private fun setSatInfo(satPass: SatPass) {
val satPos = satPass.predictor.getSatPos(dateNow)
val osmPos = getOsmPosition(satPos.latitude, satPos.longitude, true)
val catNum = satPass.tle.catnum
val name = satPass.tle.name
val satVelocity = getSatVelocity(satPos.altitude)
binding.apply {
idName.text = String.format(getString(R.string.pat_osm_idName), catNum, name)
altitude.text = String.format(getString(R.string.pat_altitude), satPos.altitude)
distance.text = String.format(getString(R.string.pat_distance), satPos.range)
velocity.text = String.format(getString(R.string.pat_osm_vel), satVelocity)
latLon.text = String.format(getString(R.string.pat_osm_latLon), osmPos.lat, osmPos.lon)
}
}
private fun getSatVelocity(satAlt: Double): 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) + satAlt * 10.0.pow(3)
return sqrt(earthG * earthM / radius) / 1000
}
private fun getSatTrack(pass: SatPass): Overlay {
@@ -216,7 +180,7 @@ class MapFragment : Fragment(R.layout.fragment_map) {
var oldLon = 0.0
positions.forEach {
val osmPos = getOsmPosition(it.latitude, it.longitude, true)
val osmPos = viewModel.getOsmPosition(it.latitude, it.longitude, true)
if (oldLon < -170.0 && osmPos.lon > 170.0 || oldLon > 170.0 && osmPos.lon < -170.0) {
val currentPoints = mutableListOf<GeoPoint>()
currentPoints.addAll(trackPoints)
@@ -245,7 +209,7 @@ class MapFragment : Fragment(R.layout.fragment_map) {
var zeroPoint = GeoPoint(0.0, 0.0)
rangeCircle.withIndex().forEach {
val osmPos = getOsmPosition(it.value.lat, it.value.lon, false)
val osmPos = viewModel.getOsmPosition(it.value.lat, it.value.lon, false)
if (it.index == 0) zeroPoint = GeoPoint(osmPos.lat, osmPos.lon)
rangePoints.add(GeoPoint(osmPos.lat, osmPos.lon))
}
@@ -258,7 +222,8 @@ class MapFragment : Fragment(R.layout.fragment_map) {
}
}
private fun getColorFilter(targetColor: Int): ColorMatrixColorFilter {
private fun getColorFilter(): ColorMatrixColorFilter {
val targetColor = ContextCompat.getColor(requireContext(), R.color.satTrack)
val newR = Color.red(targetColor) / 255f
val newG = Color.green(targetColor) / 255f
val newB = Color.blue(targetColor) / 255f
@@ -282,17 +247,18 @@ class MapFragment : Fragment(R.layout.fragment_map) {
return ColorMatrixColorFilter(tintedMatrix)
}
private fun getOsmPosition(lat: Double, lon: Double, inRadians: Boolean): Position {
return if (inRadians) {
var osmLat = Math.toDegrees(lat)
var osmLon = Math.toDegrees(lon)
if (osmLat > 85.05) osmLat = 85.05 else if (osmLat < -85.05) osmLat = -85.05
if (osmLon > 180f) osmLon -= 360f
Position(osmLat, osmLon)
} else {
val osmLat = if (lat > 85.05) 85.05 else if (lat < -85.05) -85.05 else lat
val osmLon = if (lon > 180.0) lon - 360.0 else lon
Position(osmLat, osmLon)
}
private fun getTileSource(): ITileSource {
val copyright = resources.getString(R.string.map_copyright)
val sources = arrayOf("https://maps.wikimedia.org/osm-intl/")
val policy = TileSourcePolicy(
1, TileSourcePolicy.FLAG_NO_BULK and TileSourcePolicy.FLAG_NO_PREVENTIVE and
TileSourcePolicy.FLAG_USER_AGENT_MEANINGFUL and TileSourcePolicy.FLAG_USER_AGENT_NORMALIZED
)
return XYTileSource("wikimedia", 2, 6, 256, ".png", sources, copyright, policy)
}
override fun onDestroyView() {
binding = null
super.onDestroyView()
}
}
@@ -0,0 +1,88 @@
package com.rtbishop.look4sat.ui.mainScreen
import androidx.hilt.lifecycle.ViewModelInject
import androidx.lifecycle.LiveData
import androidx.lifecycle.MutableLiveData
import androidx.lifecycle.ViewModel
import androidx.lifecycle.viewModelScope
import com.github.amsacode.predict4java.GroundStationPosition
import com.github.amsacode.predict4java.Position
import com.rtbishop.look4sat.data.SatPass
import com.rtbishop.look4sat.utility.PrefsManager
import kotlinx.coroutines.delay
import kotlinx.coroutines.launch
import java.util.*
import kotlin.math.pow
import kotlin.math.sqrt
class MapViewModel @ViewModelInject constructor(prefsManager: PrefsManager) : ViewModel() {
private val dateNow = Date()
private var filteredPasses = listOf<SatPass>()
private val _gsp = MutableLiveData(prefsManager.getStationPosition())
fun getGSP(): LiveData<GroundStationPosition> = _gsp
private val _selectedPass = MutableLiveData<SatPass?>()
fun getSelectedPass(): LiveData<SatPass?> = _selectedPass
private val _satMarkers = MutableLiveData<Map<SatPass, Position>>()
fun getSatMarkers(): LiveData<Map<SatPass, Position>> = _satMarkers
fun setPasses(passesList: List<SatPass>) {
filteredPasses = passesList.distinctBy { it.tle }
_selectedPass.value = filteredPasses[0]
calculateSatPos(filteredPasses)
}
private fun calculateSatPos(passes: List<SatPass>) {
viewModelScope.launch {
while (true) {
val passesMap = mutableMapOf<SatPass, Position>()
dateNow.time = System.currentTimeMillis()
passes.forEach {
val satPos = it.predictor.getSatPos(dateNow)
val osmPos = getOsmPosition(satPos.latitude, satPos.longitude, true)
passesMap[it] = osmPos
}
_satMarkers.value = passesMap
delay(2000)
}
}
}
fun setSelectedSat(satPass: SatPass): Boolean {
_selectedPass.value = satPass
return true
}
fun changeSelection(decrement: Boolean) {
val index = filteredPasses.indexOf(_selectedPass.value)
_selectedPass.value = if (decrement) {
if (index > 0) filteredPasses[index - 1] else filteredPasses[filteredPasses.size - 1]
} else {
if (index < filteredPasses.size - 1) filteredPasses[index + 1] else filteredPasses[0]
}
}
fun getSatVelocity(satAlt: Double): 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) + satAlt * 10.0.pow(3)
return sqrt(earthG * earthM / radius) / 1000
}
fun getOsmPosition(lat: Double, lon: Double, inRadians: Boolean): Position {
return if (inRadians) {
var osmLat = Math.toDegrees(lat)
var osmLon = Math.toDegrees(lon)
if (osmLat > 85.05) osmLat = 85.05 else if (osmLat < -85.05) osmLat = -85.05
if (osmLon > 180f) osmLon -= 360f
Position(osmLat, osmLon)
} else {
val osmLat = if (lat > 85.05) 85.05 else if (lat < -85.05) -85.05 else lat
val osmLon = if (lon > 180.0) lon - 360.0 else lon
Position(osmLat, osmLon)
}
}
}
@@ -26,7 +26,7 @@ import com.github.amsacode.predict4java.GroundStationPosition
import com.rtbishop.look4sat.data.PassPrefs
import javax.inject.Inject
class PrefsManager @Inject constructor(val preferences: SharedPreferences) {
class PrefsManager @Inject constructor(private val preferences: SharedPreferences) {
companion object {
const val keyLatitude = "latitude"
const val keyLongitude = "longitude"