From 80b62b46580735334c8acd2410b7b1831bc8cbbf Mon Sep 17 00:00:00 2001 From: Arty Bishop Date: Thu, 30 Apr 2020 11:09:04 +0100 Subject: [PATCH] Added PolarView rotation using sensors data --- .../look4sat/ui/fragments/MapViewFragment.kt | 23 +- .../ui/fragments/PolarViewFragment.kt | 291 ++++++------------ .../rtbishop/look4sat/ui/views/PolarView.kt | 106 +++++++ .../rtbishop/look4sat/utility/GeneralUtils.kt | 28 ++ app/src/main/res/values-ru/strings.xml | 5 - app/src/main/res/values/strings.xml | 17 +- 6 files changed, 249 insertions(+), 221 deletions(-) create mode 100644 app/src/main/java/com/rtbishop/look4sat/ui/views/PolarView.kt create mode 100644 app/src/main/java/com/rtbishop/look4sat/utility/GeneralUtils.kt diff --git a/app/src/main/java/com/rtbishop/look4sat/ui/fragments/MapViewFragment.kt b/app/src/main/java/com/rtbishop/look4sat/ui/fragments/MapViewFragment.kt index e3de7d2d..6ea30c30 100644 --- a/app/src/main/java/com/rtbishop/look4sat/ui/fragments/MapViewFragment.kt +++ b/app/src/main/java/com/rtbishop/look4sat/ui/fragments/MapViewFragment.kt @@ -41,6 +41,7 @@ import com.rtbishop.look4sat.data.SatPass import com.rtbishop.look4sat.databinding.FragmentMapViewBinding import com.rtbishop.look4sat.ui.MainActivity import com.rtbishop.look4sat.ui.SharedViewModel +import com.rtbishop.look4sat.utility.GeneralUtils import com.rtbishop.look4sat.utility.PassPredictor import kotlinx.coroutines.delay import kotlinx.coroutines.launch @@ -182,7 +183,7 @@ class MapViewFragment : Fragment(R.layout.fragment_map_view) { val degLon = width / 360f val degLat = height / 180f drawHomeLoc(canvas, degLon, degLat) - val currentTime = getDateFor(System.currentTimeMillis()) + val currentTime = GeneralUtils.getDateFor(System.currentTimeMillis()) val orbitalPeriod = (24 * 60 / selectedSat.meanmo).toInt() val positions = predictor.getPositions(currentTime, 60, 0, orbitalPeriod * 3) setTextViewsToSelectedSatPos(positions[0]) @@ -215,8 +216,8 @@ class MapViewFragment : Fragment(R.layout.fragment_map_view) { } private fun setTextViewsToSelectedSatPos(position: SatPos) { - var lon = rad2Deg(position.longitude).toFloat() - val lat = rad2Deg(position.latitude).toFloat() + var lon = GeneralUtils.rad2Deg(position.longitude).toFloat() + val lat = GeneralUtils.rad2Deg(position.latitude).toFloat() val rng = position.range if (lon > 180f) lon -= 360f @@ -238,8 +239,8 @@ class MapViewFragment : Fragment(R.layout.fragment_map_view) { var lastLon = 181f list.withIndex().forEach { (index, satPos) -> - lon = rad2Deg(satPos.longitude).toFloat() - lat = rad2Deg(satPos.latitude).toFloat() * -1 + lon = GeneralUtils.rad2Deg(satPos.longitude).toFloat() + lat = GeneralUtils.rad2Deg(satPos.latitude).toFloat() * -1 if (lon > 180f) lon -= 360f @@ -305,8 +306,8 @@ class MapViewFragment : Fragment(R.layout.fragment_map_view) { position: SatPos, name: String ) { - var lon = rad2Deg(position.longitude).toFloat() - val lat = rad2Deg(position.latitude).toFloat() * -1 + var lon = GeneralUtils.rad2Deg(position.longitude).toFloat() + val lat = GeneralUtils.rad2Deg(position.latitude).toFloat() * -1 if (lon > 180f) lon -= 360f @@ -318,13 +319,5 @@ class MapViewFragment : Fragment(R.layout.fragment_map_view) { canvas.drawText(name, cx - rect.width() / 2, cy - txtPaint.textSize, outlinePaint) canvas.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 - } } } diff --git a/app/src/main/java/com/rtbishop/look4sat/ui/fragments/PolarViewFragment.kt b/app/src/main/java/com/rtbishop/look4sat/ui/fragments/PolarViewFragment.kt index 45918ff1..400b1efb 100644 --- a/app/src/main/java/com/rtbishop/look4sat/ui/fragments/PolarViewFragment.kt +++ b/app/src/main/java/com/rtbishop/look4sat/ui/fragments/PolarViewFragment.kt @@ -20,18 +20,18 @@ package com.rtbishop.look4sat.ui.fragments import android.content.Context -import android.graphics.Canvas -import android.graphics.Paint -import android.graphics.Path +import android.hardware.Sensor +import android.hardware.SensorEvent +import android.hardware.SensorEventListener +import android.hardware.SensorManager import android.os.Bundle import android.view.View -import androidx.core.content.ContextCompat import androidx.fragment.app.Fragment +import androidx.lifecycle.Observer import androidx.lifecycle.ViewModelProvider import androidx.lifecycle.lifecycleScope import androidx.navigation.fragment.navArgs import androidx.recyclerview.widget.LinearLayoutManager -import com.github.amsacode.predict4java.SatPos import com.rtbishop.look4sat.Look4SatApp import com.rtbishop.look4sat.R import com.rtbishop.look4sat.dagger.ViewModelFactory @@ -41,225 +41,136 @@ import com.rtbishop.look4sat.databinding.FragmentPolarViewBinding import com.rtbishop.look4sat.ui.MainActivity import com.rtbishop.look4sat.ui.SharedViewModel import com.rtbishop.look4sat.ui.adapters.TransmitterAdapter +import com.rtbishop.look4sat.ui.views.PolarView +import com.rtbishop.look4sat.utility.GeneralUtils +import kotlinx.coroutines.Job import kotlinx.coroutines.delay import kotlinx.coroutines.launch import java.util.* import javax.inject.Inject -import kotlin.math.cos -import kotlin.math.sin -class PolarViewFragment : Fragment(R.layout.fragment_polar_view) { +class PolarViewFragment : Fragment(R.layout.fragment_polar_view), SensorEventListener { @Inject lateinit var modelFactory: ViewModelFactory - private val args: PolarViewFragmentArgs by navArgs() + private lateinit var mainActivity: MainActivity private lateinit var viewModel: SharedViewModel + private lateinit var binding: FragmentPolarViewBinding private lateinit var satPass: SatPass private lateinit var polarView: PolarView - private lateinit var mainActivity: MainActivity - private var transmitterAdapter: TransmitterAdapter = TransmitterAdapter() + private lateinit var sensorManager: SensorManager + + private val args: PolarViewFragmentArgs by navArgs() + private val transmitterAdapter = TransmitterAdapter() + private var lastAccData = FloatArray(3) + private var lastMagData = FloatArray(3) + private var refreshJob: Job? = null override fun onViewCreated(view: View, savedInstanceState: Bundle?) { super.onViewCreated(view, savedInstanceState) - val binding = FragmentPolarViewBinding.bind(view) mainActivity = activity as MainActivity + binding = FragmentPolarViewBinding.bind(view) (mainActivity.application as Look4SatApp).appComponent.inject(this) viewModel = ViewModelProvider(mainActivity, modelFactory).get(SharedViewModel::class.java) - val refreshRate = viewModel.getRefreshRate() - + sensorManager = mainActivity.getSystemService(Context.SENSOR_SERVICE) as SensorManager binding.recPolar.apply { layoutManager = LinearLayoutManager(mainActivity) adapter = transmitterAdapter } - - observePasses(refreshRate, binding) + observePasses() } - private fun observePasses( - refreshRate: Long, - binding: FragmentPolarViewBinding - ) { - viewModel.getPassList() - .observe(viewLifecycleOwner, androidx.lifecycle.Observer { result -> - when (result) { - is Result.Success -> { - satPass = result.data[args.satPassIndex] - mainActivity.supportActionBar?.title = satPass.tle.name - polarView = PolarView(mainActivity, refreshRate, binding) - binding.framePolar.addView(polarView) - observeTransmitters(binding) - refreshView() - } + override fun onResume() { + super.onResume() + sensorManager.getDefaultSensor(Sensor.TYPE_ACCELEROMETER).also { accData -> + sensorManager.registerListener(this, accData, SensorManager.SENSOR_DELAY_GAME) + } + sensorManager.getDefaultSensor(Sensor.TYPE_MAGNETIC_FIELD).also { magData -> + sensorManager.registerListener(this, magData, SensorManager.SENSOR_DELAY_GAME) + } + } + + override fun onPause() { + super.onPause() + sensorManager.unregisterListener(this) + } + + override fun onAccuracyChanged(sensor: Sensor?, accuracy: Int) {} + + override fun onSensorChanged(event: SensorEvent?) { + event?.let { + if (it.sensor.type == Sensor.TYPE_ACCELEROMETER) { + filterSensorData(event.values, lastAccData) + } else if (it.sensor.type == Sensor.TYPE_MAGNETIC_FIELD) { + filterSensorData(event.values, lastMagData) + } + + val rotationMatrix = FloatArray(9) + if (SensorManager.getRotationMatrix(rotationMatrix, null, lastAccData, lastMagData)) { + val orientation = FloatArray(3) + SensorManager.getOrientation(rotationMatrix, orientation) + val degree = orientation[0] * 57.2957795f + polarView.rotation = -degree + } + } + } + + private fun filterSensorData(input: FloatArray, output: FloatArray) { + val filterStep = 0.05f + input.indices.forEach { output[it] = output[it] + filterStep * (input[it] - output[it]) } + } + + private fun observePasses() { + viewModel.getPassList().observe(viewLifecycleOwner, Observer { result -> + when (result) { + is Result.Success -> { + val refreshRate = viewModel.getRefreshRate() + satPass = result.data[args.satPassIndex] + mainActivity.supportActionBar?.title = satPass.tle.name + polarView = PolarView(mainActivity, satPass) + binding.framePolar.addView(polarView) + observeTransmitters() + refreshText(refreshRate) } - }) + } + }) } - private fun observeTransmitters(binding: FragmentPolarViewBinding) { - viewModel.getTransmittersForSat(satPass.tle.catnum) - .observe(viewLifecycleOwner, androidx.lifecycle.Observer { - if (it.isNotEmpty()) { - transmitterAdapter.setList(it) - transmitterAdapter.notifyDataSetChanged() - binding.recPolar.visibility = View.VISIBLE - binding.tvPolarNoTrans.visibility = View.INVISIBLE - } else { - binding.recPolar.visibility = View.INVISIBLE - binding.tvPolarNoTrans.visibility = View.VISIBLE - } - }) + private fun observeTransmitters() { + viewModel.getTransmittersForSat(satPass.tle.catnum).observe(viewLifecycleOwner, Observer { + if (it.isNotEmpty()) { + transmitterAdapter.setList(it) + transmitterAdapter.notifyDataSetChanged() + binding.recPolar.visibility = View.VISIBLE + binding.tvPolarNoTrans.visibility = View.INVISIBLE + } else { + binding.recPolar.visibility = View.INVISIBLE + binding.tvPolarNoTrans.visibility = View.VISIBLE + } + }) } - private fun refreshView() { - val rate = viewModel.getRefreshRate() - lifecycleScope.launch { + private fun refreshText(rate: Long) { + refreshJob?.cancel() + refreshJob = null + refreshJob = lifecycleScope.launch { while (true) { + setPassText() polarView.invalidate() delay(rate) } } } - inner class PolarView( - context: Context, - private val updateFreq: Long, - private val binding: FragmentPolarViewBinding - ) : - 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 radarPaint = Paint().apply { - isAntiAlias = true - color = ContextCompat.getColor(mainActivity, R.color.themeLight) - style = Paint.Style.STROKE - strokeWidth = scale - } - private val txtPaint = Paint().apply { - isAntiAlias = true - color = ContextCompat.getColor(mainActivity, R.color.themeAccent) - textSize = txtSize - } - private val trackPaint = Paint().apply { - isAntiAlias = true - color = ContextCompat.getColor(mainActivity, R.color.satTrack) - style = Paint.Style.STROKE - strokeWidth = scale - } - private val satPaint = Paint().apply { - isAntiAlias = true - color = ContextCompat.getColor(mainActivity, R.color.themeAccent) - 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) - if (!satPass.tle.isDeepspace) drawPassTrajectory(canvas) - drawSatellite(canvas) - } - - private fun setPassText() { - satPos = satPass.predictor.getSatPos(Date()) - binding.tvPolarAz.text = - String.format(context.getString(R.string.pat_azimuth), rad2Deg(satPos.azimuth)) - binding.tvPolarEl.text = - String.format(context.getString(R.string.pat_elevation), rad2Deg(satPos.elevation)) - binding.tvPolarRng.text = - String.format(context.getString(R.string.pat_range), satPos.range) - binding.tvPolarAlt.text = - String.format(context.getString(R.string.pat_altitude), 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( - context.getString(R.string.polar_north), - center - txtSize / 3, - center - radius - scale * 2, - txtPaint - ) - cvs.drawText( - context.getString(R.string.polar_east), - center + radius + scale * 2, - center + txtSize / 3, - txtPaint - ) - cvs.drawText( - context.getString(R.string.polar_south), - center - txtSize / 3, - center + radius + txtSize, - txtPaint - ) - cvs.drawText( - context.getString(R.string.polar_west), - center - radius - txtSize, - center + txtSize / 3, - txtPaint - ) - cvs.drawText("90°", center + scale, center - scale * 2, txtPaint) - cvs.drawText("60°", center + scale, center - (radius / 3) - scale * 2, txtPaint) - cvs.drawText("30°", 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 += updateFreq - } - 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 - } + private fun setPassText() { + val satPos = satPass.predictor.getSatPos(Date()) + val polarAz = mainActivity.getString(R.string.pat_azimuth) + val polarEl = mainActivity.getString(R.string.pat_elevation) + val polarRng = mainActivity.getString(R.string.pat_range) + val polarAlt = mainActivity.getString(R.string.pat_altitude) + binding.tvPolarAz.text = String.format(polarAz, GeneralUtils.rad2Deg(satPos.azimuth)) + binding.tvPolarEl.text = String.format(polarEl, GeneralUtils.rad2Deg(satPos.elevation)) + binding.tvPolarRng.text = String.format(polarRng, satPos.range) + binding.tvPolarAlt.text = String.format(polarAlt, satPos.altitude) } } diff --git a/app/src/main/java/com/rtbishop/look4sat/ui/views/PolarView.kt b/app/src/main/java/com/rtbishop/look4sat/ui/views/PolarView.kt new file mode 100644 index 00000000..205a05c0 --- /dev/null +++ b/app/src/main/java/com/rtbishop/look4sat/ui/views/PolarView.kt @@ -0,0 +1,106 @@ +package com.rtbishop.look4sat.ui.views + +import android.content.Context +import android.graphics.Canvas +import android.graphics.Matrix +import android.graphics.Paint +import android.graphics.Path +import android.view.View +import androidx.core.content.ContextCompat +import com.rtbishop.look4sat.R +import com.rtbishop.look4sat.data.SatPass +import com.rtbishop.look4sat.utility.GeneralUtils + +class PolarView(context: Context) : View(context) { + + constructor(context: Context, satPass: SatPass) : this(context) { + pass = satPass + } + + private lateinit var pass: SatPass + private val polarWidth = resources.displayMetrics.widthPixels + private val polarCenter = polarWidth / 2f + private val scale = resources.displayMetrics.density + private val radius = polarWidth * 0.49f + private val txtSize = scale * 15 + private val path: Path = Path() + private val mMatrix = Matrix() + var azimuth = 0f + + private val radarPaint = Paint().apply { + isAntiAlias = true + color = ContextCompat.getColor(context, R.color.themeLight) + style = Paint.Style.STROKE + strokeWidth = scale + } + private val txtPaint = Paint().apply { + isAntiAlias = true + color = ContextCompat.getColor(context, R.color.themeAccent) + textSize = txtSize + } + private val trackPaint = Paint().apply { + isAntiAlias = true + color = ContextCompat.getColor(context, R.color.satTrack) + style = Paint.Style.STROKE + strokeWidth = scale + } + private val satPaint = Paint().apply { + isAntiAlias = true + color = ContextCompat.getColor(context, R.color.themeAccent) + style = Paint.Style.FILL + } + + override fun onDraw(canvas: Canvas) { +// Log.w("myTag", "$measuredWidth, $measuredHeight") + canvas.rotate(-azimuth, polarCenter, polarCenter) + canvas.translate(polarCenter, polarCenter) + drawRadarView(canvas) + drawRadarText(canvas) + if (!pass.tle.isDeepspace) { + drawPassTrajectory(canvas, pass) + } + drawSatellite(canvas, pass) + } + + private fun drawRadarView(cvs: Canvas) { + cvs.drawLine(-radius, 0f, radius, 0f, radarPaint) + cvs.drawLine(0f, -radius, 0f, radius, radarPaint) + cvs.drawCircle(0f, 0f, radius, radarPaint) + cvs.drawCircle(0f, 0f, (radius / 3) * 2, radarPaint) + cvs.drawCircle(0f, 0f, radius / 3, radarPaint) + } + + private fun drawRadarText(cvs: Canvas) { + cvs.drawText("N", scale, -radius + txtSize - scale * 2, txtPaint) + cvs.drawText("30°", scale, -((radius / 3) * 2) - scale * 2, txtPaint) + cvs.drawText("60°", scale, -(radius / 3) - scale * 2, txtPaint) + cvs.drawText("90°", scale, -scale * 2, txtPaint) + } + + private fun drawPassTrajectory(cvs: Canvas, satPass: SatPass) { + val startTime = satPass.pass.startTime + val endTime = satPass.pass.endTime + while (startTime.before(endTime)) { + val satPos = satPass.predictor.getSatPos(startTime) + val x = GeneralUtils.sph2CartX(satPos.azimuth, satPos.elevation, radius.toDouble()) + val y = GeneralUtils.sph2CartY(satPos.azimuth, satPos.elevation, radius.toDouble()) + if (startTime.compareTo(satPass.pass.startTime) == 0) { + path.moveTo(x, -y) + } else { + path.lineTo(x, -y) + } + startTime.time += 15000 + } + cvs.drawPath(path, trackPaint) + } + + private fun drawSatellite(cvs: Canvas, satPass: SatPass) { + val date = GeneralUtils.getDateFor(System.currentTimeMillis()) + val satPos = satPass.predictor.getSatPos(date) + if (satPos.elevation > 0) { + val x = GeneralUtils.sph2CartX(satPos.azimuth, satPos.elevation, radius.toDouble()) + val y = GeneralUtils.sph2CartY(satPos.azimuth, satPos.elevation, radius.toDouble()) + cvs.drawCircle(x, -y, txtSize / 3, satPaint) + } + } +} \ No newline at end of file diff --git a/app/src/main/java/com/rtbishop/look4sat/utility/GeneralUtils.kt b/app/src/main/java/com/rtbishop/look4sat/utility/GeneralUtils.kt new file mode 100644 index 00000000..7da294da --- /dev/null +++ b/app/src/main/java/com/rtbishop/look4sat/utility/GeneralUtils.kt @@ -0,0 +1,28 @@ +package com.rtbishop.look4sat.utility + +import java.util.* +import kotlin.math.cos +import kotlin.math.sin + +object GeneralUtils { + + private const val piDiv2 = Math.PI / 2.0 + + fun getDateFor(value: Long): Date { + return Date(value) + } + + fun rad2Deg(value: Double): Double { + return value * 180 / Math.PI + } + + fun sph2CartX(azimuth: Double, elevation: Double, r: Double): Float { + val radius = r * (piDiv2 - elevation) / piDiv2 + return (radius * cos(piDiv2 - azimuth)).toFloat() + } + + fun sph2CartY(azimuth: Double, elevation: Double, r: Double): Float { + val radius = r * (piDiv2 - elevation) / piDiv2 + return (radius * sin(piDiv2 - azimuth)).toFloat() + } +} \ No newline at end of file diff --git a/app/src/main/res/values-ru/strings.xml b/app/src/main/res/values-ru/strings.xml index 4e573830..3e313898 100644 --- a/app/src/main/res/values-ru/strings.xml +++ b/app/src/main/res/values-ru/strings.xml @@ -77,11 +77,6 @@ Элевация: %.1f° %2d° — КПС - С - З - Ю - В - Азимут: %.1f° Элевация: %.1f° Широта: %.1f° diff --git a/app/src/main/res/values/strings.xml b/app/src/main/res/values/strings.xml index bc8f1b54..f2f0952e 100644 --- a/app/src/main/res/values/strings.xml +++ b/app/src/main/res/values/strings.xml @@ -91,18 +91,13 @@ %2d° — LOS dd MMM @ HH:mm:ss - N - W - S - E - %.4f - Azimuth: %.1f° - Elevation: %.1f° - Latitude: %.1f° - Longitude: %.1f° - Distance: %.0f km - Altitude: %.0f km + Az: %.1f° + El: %.1f° + Lat: %.1f° + Lon: %.1f° + Dst: %.0f km + Alt: %.0f km %02d:%02d:%02d %s