diff --git a/app/src/main/java/com/rtbishop/look4sat/data/SelectedSat.kt b/app/src/main/java/com/rtbishop/look4sat/data/SelectedSat.kt new file mode 100644 index 00000000..ec7ff156 --- /dev/null +++ b/app/src/main/java/com/rtbishop/look4sat/data/SelectedSat.kt @@ -0,0 +1,18 @@ +package com.rtbishop.look4sat.data + +import com.github.amsacode.predict4java.Position +import org.osmdroid.views.overlay.Overlay + +data class SelectedSat( + val pass: SatPass, + val catNum: Int, + val name: String, + val range: Double, + val altitude: Double, + val velocity: Double, + val qthLoc: String, + val osmPos: Position, + val coverage: Double, + val footprint: Overlay, + val groundTrack: Overlay +) \ No newline at end of file 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 8dccf182..4b782c59 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 @@ -4,7 +4,6 @@ import android.content.SharedPreferences import android.graphics.Color import android.graphics.ColorMatrix import android.graphics.ColorMatrixColorFilter -import android.graphics.Paint import android.os.Bundle import android.view.View import androidx.core.content.ContextCompat @@ -17,8 +16,8 @@ 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.data.SelectedSat import com.rtbishop.look4sat.databinding.FragmentMapBinding -import com.rtbishop.look4sat.utility.Utilities import dagger.hilt.android.AndroidEntryPoint import org.osmdroid.config.Configuration import org.osmdroid.tileprovider.tilesource.ITileSource @@ -36,8 +35,7 @@ class MapFragment : Fragment(R.layout.fragment_map) { @Inject lateinit var preferences: SharedPreferences - private lateinit var trackPaint: Paint - private lateinit var footprintPaint: Paint + private lateinit var binding: FragmentMapBinding private lateinit var nameFormat: String private lateinit var qthFormat: String private lateinit var altFormat: String @@ -45,46 +43,18 @@ class MapFragment : Fragment(R.layout.fragment_map) { private lateinit var velFormat: String private lateinit var latLonFormat: String private lateinit var coverageFormat: String - private lateinit var selectedSat: SatPass - private val dateNow = Date() 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) setupComponents(view) - setupMapView() setupObservers() } private fun setupComponents(view: View) { Configuration.getInstance().load(requireContext().applicationContext, preferences) - binding = FragmentMapBinding.bind(view) - trackPaint = Paint().apply { - strokeWidth = 2f - style = Paint.Style.STROKE - color = Color.RED - strokeCap = Paint.Cap.ROUND - strokeJoin = Paint.Join.ROUND - isAntiAlias = true - } - footprintPaint = Paint().apply { - style = Paint.Style.FILL_AND_STROKE - color = ContextCompat.getColor(requireContext(), R.color.satFootprint) - isAntiAlias = true - } - nameFormat = getString(R.string.pat_osm_idName) - qthFormat = getString(R.string.map_qth) - 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) - coverageFormat = getString(R.string.map_coverage) - } - - private fun setupMapView() { - binding?.apply { + binding = FragmentMapBinding.bind(view).apply { mapView.apply { setMultiTouchControls(true) setTileSource(getTileSource()) @@ -100,6 +70,13 @@ class MapFragment : Fragment(R.layout.fragment_map) { overlays.addAll(Array(4) { FolderOverlay() }) } } + nameFormat = getString(R.string.pat_osm_idName) + qthFormat = getString(R.string.map_qth) + 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) + coverageFormat = getString(R.string.map_coverage) } private fun setupObservers() { @@ -107,23 +84,16 @@ class MapFragment : Fragment(R.layout.fragment_map) { 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) } + binding.fabPrev.setOnClickListener { viewModel.changeSelection(true) } + binding.fabNext.setOnClickListener { viewModel.changeSelection(false) } } }) - viewModel.getSelectedPass().observe(viewLifecycleOwner, { satPass -> - selectedSat = satPass - setSelectedSatDetails(satPass) - scrollToSat(satPass) - }) - viewModel.getSatMarkers().observe(viewLifecycleOwner, { - setMarkers(it) - setSelectedSatDetails(selectedSat) - }) + viewModel.getSelectedSat().observe(viewLifecycleOwner, { setSelectedSatDetails(it) }) + viewModel.getSatMarkers().observe(viewLifecycleOwner, { setMarkers(it) }) } private fun setupPosOverlay(gsp: GroundStationPosition) { - binding?.apply { + binding.apply { Marker(mapView).apply { setInfoWindow(null) setAnchor(Marker.ANCHOR_CENTER, Marker.ANCHOR_BOTTOM) @@ -135,33 +105,23 @@ class MapFragment : Fragment(R.layout.fragment_map) { } } - private fun setSelectedSatDetails(satPass: SatPass) { - val satPos = satPass.predictor.getSatPos(dateNow) - val osmPos = viewModel.getOsmPosition(satPos.latitude, satPos.longitude, true) - val qthLoc = Utilities.locToQTH(osmPos.lat, osmPos.lon) - binding?.apply { - idName.text = String.format(nameFormat, satPass.tle.catnum, satPass.tle.name) - qthLocator.text = String.format(qthFormat, qthLoc) - 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) - mapCoverage.text = String.format(coverageFormat, satPos.rangeCircleRadiusKm * 2) - mapView.overlays[1] = getSatTrack(satPass) - mapView.overlays[2] = getSatFootprint(satPass) + private fun setSelectedSatDetails(sat: SelectedSat) { + binding.apply { + idName.text = String.format(nameFormat, sat.catNum, sat.name) + qthLocator.text = String.format(qthFormat, sat.qthLoc) + altitude.text = String.format(altFormat, sat.altitude) + distance.text = String.format(distFormat, sat.range) + velocity.text = String.format(velFormat, sat.velocity) + latLon.text = String.format(latLonFormat, sat.osmPos.lat, sat.osmPos.lon) + mapCoverage.text = String.format(coverageFormat, sat.coverage) + mapView.overlays[1] = sat.groundTrack + mapView.overlays[2] = sat.footprint mapView.invalidate() } } - 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 { + binding.apply { val markers = FolderOverlay() map.entries.forEach { Marker(mapView).apply { @@ -169,7 +129,10 @@ class MapFragment : Fragment(R.layout.fragment_map) { setAnchor(Marker.ANCHOR_CENTER, Marker.ANCHOR_CENTER) icon = ContextCompat.getDrawable(requireContext(), R.drawable.ic_map_sat) position = GeoPoint(it.value.lat, it.value.lon) - setOnMarkerClickListener { _, _ -> viewModel.setSelectedSat(it.key) } + setOnMarkerClickListener { _, _ -> + viewModel.setSelectedSat(it.key) + return@setOnMarkerClickListener true + } markers.add(this) } } @@ -178,48 +141,6 @@ class MapFragment : Fragment(R.layout.fragment_map) { } } - private fun getSatTrack(pass: SatPass): Overlay { - val period = (24 * 60 / pass.tle.meanmo).toInt() - val positions = pass.predictor.getPositions(dateNow, 20, 0, period * 3) - val trackOverlay = FolderOverlay() - val trackPoints = mutableListOf() - var oldLon = 0.0 - positions.forEach { - 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) - Polyline().apply { - outlinePaint.set(trackPaint) - setPoints(currentPoints) - trackOverlay.add(this) - } - trackPoints.clear() - } - oldLon = osmPos.lon - trackPoints.add(GeoPoint(osmPos.lat, osmPos.lon)) - } - Polyline().apply { - outlinePaint.set(trackPaint) - setPoints(trackPoints) - trackOverlay.add(this) - } - return trackOverlay - } - - private fun getSatFootprint(pass: SatPass): Overlay { - val rangePoints = mutableListOf() - pass.predictor.getSatPos(dateNow).rangeCircle.withIndex().forEach { - val osmPos = viewModel.getOsmPosition(it.value.lat, it.value.lon, false) - rangePoints.add(GeoPoint(osmPos.lat, osmPos.lon)) - } - return Polygon().apply { - fillPaint.set(footprintPaint) - outlinePaint.set(footprintPaint) - points = rangePoints - } - } - private fun getColorFilter(): ColorMatrixColorFilter { val targetColor = Color.RED val newR = Color.red(targetColor) / 255f @@ -254,9 +175,4 @@ class MapFragment : Fragment(R.layout.fragment_map) { ) 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 index 244dc528..8562b79f 100644 --- a/app/src/main/java/com/rtbishop/look4sat/ui/mainScreen/MapViewModel.kt +++ b/app/src/main/java/com/rtbishop/look4sat/ui/mainScreen/MapViewModel.kt @@ -1,5 +1,7 @@ package com.rtbishop.look4sat.ui.mainScreen +import android.graphics.Color +import android.graphics.Paint import androidx.hilt.lifecycle.ViewModelInject import androidx.lifecycle.LiveData import androidx.lifecycle.MutableLiveData @@ -8,10 +10,17 @@ 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.data.SelectedSat import com.rtbishop.look4sat.utility.PrefsManager +import com.rtbishop.look4sat.utility.Utilities import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.delay import kotlinx.coroutines.launch +import org.osmdroid.util.GeoPoint +import org.osmdroid.views.overlay.FolderOverlay +import org.osmdroid.views.overlay.Overlay +import org.osmdroid.views.overlay.Polygon +import org.osmdroid.views.overlay.Polyline import java.util.* import kotlin.math.pow import kotlin.math.sqrt @@ -19,60 +28,133 @@ import kotlin.math.sqrt class MapViewModel @ViewModelInject constructor(prefsManager: PrefsManager) : ViewModel() { private val dateNow = Date() + private val trackPaint = Paint().apply { + strokeWidth = 2f + style = Paint.Style.STROKE + color = Color.RED + strokeCap = Paint.Cap.ROUND + strokeJoin = Paint.Join.ROUND + isAntiAlias = true + } + private val footprintPaint = Paint().apply { + style = Paint.Style.FILL_AND_STROKE + color = Color.parseColor("#26FFE082") + isAntiAlias = true + } private var filteredPasses = listOf() private val _gsp = MutableLiveData(prefsManager.getStationPosition()) fun getGSP(): LiveData = _gsp - private val _selectedPass = MutableLiveData() - fun getSelectedPass(): LiveData = _selectedPass + private val _selectedSat = MutableLiveData() + fun getSelectedSat(): LiveData = _selectedSat private val _satMarkers = MutableLiveData>() fun getSatMarkers(): LiveData> = _satMarkers fun setPasses(passesList: List) { filteredPasses = passesList.distinctBy { it.tle } - _selectedPass.value = filteredPasses[0] - calculateSatPos(filteredPasses) + setSelectedSat(filteredPasses[0]) + calculatePositions(filteredPasses) } - private fun calculateSatPos(passes: List) { - viewModelScope.launch(Dispatchers.Default) { + fun changeSelection(decrement: Boolean) { + val index = filteredPasses.indexOf(_selectedSat.value?.pass) + if (decrement) { + if (index > 0) setSelectedSat(filteredPasses[index - 1]) + else setSelectedSat(filteredPasses[filteredPasses.size - 1]) + } else { + if (index < filteredPasses.size - 1) setSelectedSat(filteredPasses[index + 1]) + else setSelectedSat(filteredPasses[0]) + } + } + + fun setSelectedSat(pass: SatPass?) { + pass?.let { + viewModelScope.launch(Dispatchers.Default) { + val satPos = pass.predictor.getSatPos(dateNow) + val osmPos = getOsmPosition(satPos.latitude, satPos.longitude, true) + val qthLoc = Utilities.locToQTH(osmPos.lat, osmPos.lon) + val velocity = getSatVelocity(satPos.altitude) + val coverage = satPos.rangeCircleRadiusKm * 2 + val footprint = getSatFootprint(pass) + val track = getSatTrack(pass) + _selectedSat.postValue( + SelectedSat( + pass, pass.tle.catnum, pass.tle.name, satPos.range, + satPos.altitude, velocity, qthLoc, osmPos, coverage, footprint, track + ) + ) + } + } + } + + private fun calculatePositions(passes: List) { + val passesMap = mutableMapOf() + viewModelScope.launch { while (true) { - val passesMap = mutableMapOf() dateNow.time = System.currentTimeMillis() passes.forEach { val satPos = it.predictor.getSatPos(dateNow) passesMap[it] = getOsmPosition(satPos.latitude, satPos.longitude, true) } + setSelectedSat(_selectedSat.value?.pass) _satMarkers.postValue(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] + private fun getSatFootprint(pass: SatPass): Overlay { + val rangePoints = mutableListOf() + pass.predictor.getSatPos(dateNow).rangeCircle.withIndex().forEach { + val osmPos = getOsmPosition(it.value.lat, it.value.lon, false) + rangePoints.add(GeoPoint(osmPos.lat, osmPos.lon)) + } + return Polygon().apply { + fillPaint.set(footprintPaint) + outlinePaint.set(footprintPaint) + points = rangePoints } } - fun getSatVelocity(satAlt: Double): Double { + private fun getSatTrack(pass: SatPass): Overlay { + val period = (24 * 60 / pass.tle.meanmo).toInt() + val positions = pass.predictor.getPositions(dateNow, 20, 0, period * 3) + val trackOverlay = FolderOverlay() + val trackPoints = mutableListOf() + var oldLon = 0.0 + positions.forEach { + val osmPos = 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) + Polyline().apply { + outlinePaint.set(trackPaint) + setPoints(currentPoints) + trackOverlay.add(this) + } + trackPoints.clear() + } + oldLon = osmPos.lon + trackPoints.add(GeoPoint(osmPos.lat, osmPos.lon)) + } + Polyline().apply { + outlinePaint.set(trackPaint) + setPoints(trackPoints) + trackOverlay.add(this) + } + return trackOverlay + } + + private fun getSatVelocity(altitude: 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) + val radius = 6.37 * 10.0.pow(6) + altitude * 10.0.pow(3) return sqrt(earthG * earthM / radius) / 1000 } - fun getOsmPosition(lat: Double, lon: Double, inRadians: Boolean): Position { + private fun getOsmPosition(lat: Double, lon: Double, inRadians: Boolean): Position { return if (inRadians) { var osmLat = Math.toDegrees(lat) var osmLon = Math.toDegrees(lon) diff --git a/app/src/main/res/values/colors.xml b/app/src/main/res/values/colors.xml index 6a90ba89..7e8852a2 100644 --- a/app/src/main/res/values/colors.xml +++ b/app/src/main/res/values/colors.xml @@ -25,7 +25,5 @@ #D0D0D0 #808080 #1e1e1e - - #26FFE082 #66000000