From b850c7a41d8875cdbd0724366428f41637ef5f21 Mon Sep 17 00:00:00 2001 From: Arty Bishop Date: Thu, 26 Nov 2020 19:27:46 +0000 Subject: [PATCH] Added MapViewModel.kt, cleaned up MapFragment.kt --- .../look4sat/ui/mainScreen/MapFragment.kt | 256 ++++++++---------- .../look4sat/ui/mainScreen/MapViewModel.kt | 88 ++++++ .../rtbishop/look4sat/utility/PrefsManager.kt | 2 +- 3 files changed, 200 insertions(+), 146 deletions(-) create mode 100644 app/src/main/java/com/rtbishop/look4sat/ui/mainScreen/MapViewModel.kt diff --git a/app/src/main/java/com/rtbishop/look4sat/ui/mainScreen/MapFragment.kt b/app/src/main/java/com/rtbishop/look4sat/ui/mainScreen/MapFragment.kt index 70f608ba..bad93a9b 100644 --- a/app/src/main/java/com/rtbishop/look4sat/ui/mainScreen/MapFragment.kt +++ b/app/src/main/java/com/rtbishop/look4sat/ui/mainScreen/MapFragment.kt @@ -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 + 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) { - 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): 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) { + 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() 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() } } \ No newline at end of file diff --git a/app/src/main/java/com/rtbishop/look4sat/ui/mainScreen/MapViewModel.kt b/app/src/main/java/com/rtbishop/look4sat/ui/mainScreen/MapViewModel.kt new file mode 100644 index 00000000..d2ae426b --- /dev/null +++ b/app/src/main/java/com/rtbishop/look4sat/ui/mainScreen/MapViewModel.kt @@ -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() + + private val _gsp = MutableLiveData(prefsManager.getStationPosition()) + fun getGSP(): LiveData = _gsp + + private val _selectedPass = MutableLiveData() + fun getSelectedPass(): LiveData = _selectedPass + + private val _satMarkers = MutableLiveData>() + fun getSatMarkers(): LiveData> = _satMarkers + + fun setPasses(passesList: List) { + filteredPasses = passesList.distinctBy { it.tle } + _selectedPass.value = filteredPasses[0] + calculateSatPos(filteredPasses) + } + + private fun calculateSatPos(passes: List) { + viewModelScope.launch { + while (true) { + val passesMap = mutableMapOf() + 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) + } + } +} \ No newline at end of file diff --git a/app/src/main/java/com/rtbishop/look4sat/utility/PrefsManager.kt b/app/src/main/java/com/rtbishop/look4sat/utility/PrefsManager.kt index d8b57f3e..dd4e99d6 100644 --- a/app/src/main/java/com/rtbishop/look4sat/utility/PrefsManager.kt +++ b/app/src/main/java/com/rtbishop/look4sat/utility/PrefsManager.kt @@ -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"