From 2d45fd614e1be684fab37b33cfb0b5741e191c75 Mon Sep 17 00:00:00 2001 From: Arty Bishop Date: Sun, 16 May 2021 19:12:07 +0100 Subject: [PATCH] Added pass direction arrow to radar view #52 --- app/build.gradle | 3 +- .../satMapScreen/SatMapViewModel.kt | 38 ++--- .../satPassInfoScreen/PassInfoView.kt | 153 ++++++++++-------- build.gradle | 6 +- 4 files changed, 105 insertions(+), 95 deletions(-) diff --git a/app/build.gradle b/app/build.gradle index bcc83a28..207ea30d 100644 --- a/app/build.gradle +++ b/app/build.gradle @@ -61,8 +61,7 @@ dependencies { implementation "com.jakewharton.timber:timber:$timber_version" testImplementation "junit:junit:$junit_version" - testImplementation "org.mockito:mockito-core:$mockito_version" + testImplementation "org.mockito:mockito-android:$mockito_version" testImplementation "org.jetbrains.kotlinx:kotlinx-coroutines-test:$coroutines_version" - androidTestImplementation "org.mockito:mockito-android:$mockito_version" debugImplementation "com.squareup.leakcanary:leakcanary-android:$leak_canary_version" } diff --git a/app/src/main/java/com/rtbishop/look4sat/presentation/satMapScreen/SatMapViewModel.kt b/app/src/main/java/com/rtbishop/look4sat/presentation/satMapScreen/SatMapViewModel.kt index ebefabc6..4ab877ad 100644 --- a/app/src/main/java/com/rtbishop/look4sat/presentation/satMapScreen/SatMapViewModel.kt +++ b/app/src/main/java/com/rtbishop/look4sat/presentation/satMapScreen/SatMapViewModel.kt @@ -20,17 +20,16 @@ package com.rtbishop.look4sat.presentation.satMapScreen import androidx.lifecycle.* import com.rtbishop.look4sat.data.PreferencesSource import com.rtbishop.look4sat.data.SatDataRepository -import com.rtbishop.look4sat.injection.DefaultDispatcher import com.rtbishop.look4sat.domain.predict4kotlin.Position import com.rtbishop.look4sat.domain.predict4kotlin.Satellite import com.rtbishop.look4sat.domain.predict4kotlin.StationPosition import com.rtbishop.look4sat.framework.model.SatData +import com.rtbishop.look4sat.injection.DefaultDispatcher import dagger.hilt.android.lifecycle.HiltViewModel import kotlinx.coroutines.* +import org.osmdroid.views.MapView import java.util.* import javax.inject.Inject -import kotlin.math.max -import kotlin.math.min import kotlin.math.pow import kotlin.math.sqrt @@ -47,8 +46,8 @@ class SatMapViewModel @Inject constructor( private lateinit var selectedSat: Satellite val stationPos = liveData { - val osmLat = getOsmLat(gsp.latitude) - val osmLon = getOsmLon(gsp.longitude) + val osmLat = clipLat(gsp.latitude) + val osmLon = clipLon(gsp.longitude) emit(Position(osmLat, osmLon)) } @@ -115,8 +114,8 @@ class SatMapViewModel @Inject constructor( val satPositions = mutableMapOf() list.forEach { satellite -> val satPos = satellite.getPredictor(gsp).getSatPos(date) - val osmLat = getOsmLat(Math.toDegrees(satPos.latitude)) - val osmLon = getOsmLon(Math.toDegrees(satPos.longitude)) + val osmLat = clipLat(Math.toDegrees(satPos.latitude)) + val osmLon = clipLon(Math.toDegrees(satPos.longitude)) satPositions[satellite] = Position(osmLat, osmLon) } _allSatPositions.postValue(satPositions) @@ -129,8 +128,8 @@ class SatMapViewModel @Inject constructor( val currentTrack = mutableListOf() var oldLongitude = 0.0 sat.getPredictor(gsp).getPositions(date, 15, 0, 2.4).forEach { satPos -> - val osmLat = getOsmLat(Math.toDegrees(satPos.latitude)) - val osmLon = getOsmLon(Math.toDegrees(satPos.longitude)) + val osmLat = clipLat(Math.toDegrees(satPos.latitude)) + val osmLon = clipLon(Math.toDegrees(satPos.longitude)) val currentPosition = Position(osmLat, osmLon) if (oldLongitude < -170.0 && currentPosition.longitude > 170.0) { // adding left terminal position @@ -156,8 +155,8 @@ class SatMapViewModel @Inject constructor( private suspend fun setSelectedSatFootprint(sat: Satellite, gsp: StationPosition, date: Date) { withContext(defaultDispatcher) { val satFootprint = sat.getPosition(gsp, date).getRangeCircle().map { rangePos -> - val osmLat = getOsmLat(rangePos.latitude) - val osmLon = getOsmLon(rangePos.longitude) + val osmLat = clipLat(rangePos.latitude) + val osmLon = clipLon(rangePos.longitude) Position(osmLat, osmLon) } _satFootprint.postValue(satFootprint) @@ -167,8 +166,8 @@ class SatMapViewModel @Inject constructor( private suspend fun setSelectedSatData(sat: Satellite, gsp: StationPosition, date: Date) { withContext(defaultDispatcher) { val satPos = sat.getPredictor(gsp).getSatPos(date) - val osmLat = getOsmLat(Math.toDegrees(satPos.latitude)) - val osmLon = getOsmLon(Math.toDegrees(satPos.longitude)) + val osmLat = clipLat(Math.toDegrees(satPos.latitude)) + val osmLon = clipLon(Math.toDegrees(satPos.longitude)) val osmPos = Position(osmLat, osmLon) val qthLoc = preferencesSource.positionToQTH(osmPos.latitude, osmPos.longitude) ?: "-- --" @@ -181,17 +180,12 @@ class SatMapViewModel @Inject constructor( } } - private fun getOsmLat(latitude: Double): Double { - return min(max(latitude, -85.0), 85.0) + private fun clipLat(latitude: Double): Double { + return MapView.getTileSystem().cleanLatitude(latitude) } - private fun getOsmLon(longitude: Double): Double { - val newLongitude = when { - longitude < -180.0 -> longitude + 360.0 - longitude > 180.0 -> longitude - 360.0 - else -> longitude - } - return min(max(newLongitude, -180.0), 180.0) + private fun clipLon(longitude: Double): Double { + return MapView.getTileSystem().cleanLongitude(longitude) } private fun getOrbitalVelocity(altitude: Double): Double { diff --git a/app/src/main/java/com/rtbishop/look4sat/presentation/satPassInfoScreen/PassInfoView.kt b/app/src/main/java/com/rtbishop/look4sat/presentation/satPassInfoScreen/PassInfoView.kt index 8db0cdb8..9219630c 100644 --- a/app/src/main/java/com/rtbishop/look4sat/presentation/satPassInfoScreen/PassInfoView.kt +++ b/app/src/main/java/com/rtbishop/look4sat/presentation/satPassInfoScreen/PassInfoView.kt @@ -18,10 +18,7 @@ package com.rtbishop.look4sat.presentation.satPassInfoScreen import android.content.Context -import android.graphics.Canvas -import android.graphics.Color -import android.graphics.Paint -import android.graphics.Path +import android.graphics.* import android.view.View import androidx.core.content.ContextCompat import com.rtbishop.look4sat.R @@ -33,49 +30,49 @@ import kotlin.math.sin class PassInfoView(context: Context) : View(context) { private lateinit var satPass: SatPass - private val polarWidth = resources.displayMetrics.widthPixels - private val polarCenter = polarWidth / 2f private val scale = resources.displayMetrics.density - private val radius = polarWidth * 0.48f - private val txtSize = scale * 16f - private val satTrack: Path = Path() + private val radarWidth = resources.displayMetrics.widthPixels + private val radarCenter = radarWidth / 2f + private val radarRadius = radarWidth * 0.48f private val piDiv2 = Math.PI / 2.0 + private val arrowPath = Path() + private val satTrack: Path = Path() + private val strokeSize = scale * 2f + private val satSize = scale * 8f + private val txtSize = scale * 16f + private val radarPaint = Paint(Paint.ANTI_ALIAS_FLAG).apply { + color = ContextCompat.getColor(context, R.color.greyLight) + style = Paint.Style.STROKE + strokeWidth = strokeSize + } + private val textPaint = Paint(Paint.ANTI_ALIAS_FLAG).apply { + color = ContextCompat.getColor(context, R.color.themeLight) + textSize = txtSize + } + private val trackPaint = Paint(Paint.ANTI_ALIAS_FLAG).apply { + color = Color.RED + style = Paint.Style.STROKE + strokeWidth = strokeSize + } + private val satPaint = Paint(Paint.ANTI_ALIAS_FLAG).apply { + color = ContextCompat.getColor(context, R.color.themeLight) + style = Paint.Style.FILL + } + private val arrowPaint = Paint(Paint.ANTI_ALIAS_FLAG).apply { + color = ContextCompat.getColor(context, R.color.themeLight) + style = Paint.Style.FILL + strokeWidth = strokeSize + } private var azimuth: Float = 0f private var pitch: Float = 0f private var roll: Float = 0f - private val radarPaint = Paint().apply { - isAntiAlias = true - color = ContextCompat.getColor(context, R.color.greyLight) - style = Paint.Style.STROKE - strokeWidth = scale * 2f - } - private val txtPaint = Paint().apply { - isAntiAlias = true - color = ContextCompat.getColor(context, R.color.themeLight) - textSize = txtSize - } - private val trackPaint = Paint().apply { - isAntiAlias = true - color = Color.RED - style = Paint.Style.STROKE - strokeWidth = scale * 2f - } - private val satPaint = Paint().apply { - isAntiAlias = true - color = ContextCompat.getColor(context, R.color.themeLight) - style = Paint.Style.FILL - } - private val orientPaint = Paint().apply { - isAntiAlias = true - color = Color.RED - style = Paint.Style.STROKE - strokeWidth = scale * 2f - } - fun setPass(satPass: SatPass) { this.satPass = satPass - createPassTrajectory(satPass) + if (!satPass.isDeepSpace) { + createPassTrajectory(satPass) + createPassTrajectoryArrow() + } } fun setOrientation(azimuth: Float, pitch: Float, roll: Float) { @@ -88,63 +85,83 @@ class PassInfoView(context: Context) : View(context) { override fun onDraw(canvas: Canvas) { canvas.drawColor(ContextCompat.getColor(context, R.color.greyDark)) - canvas.translate(polarCenter, polarCenter) + canvas.translate(radarCenter, radarCenter) drawRadarView(canvas) drawRadarText(canvas) - if (!satPass.isDeepSpace) canvas.drawPath(satTrack, trackPaint) + if (!satPass.isDeepSpace) { + canvas.drawPath(satTrack, trackPaint) + canvas.drawPath(satTrack, arrowPaint) + } drawSatellite(canvas, satPass) - drawOrientation(canvas, azimuth, pitch) + drawCrosshair(canvas, azimuth, pitch) } private fun createPassTrajectory(satPass: SatPass) { - val startTime = satPass.aosDate - val endTime = satPass.losDate - while (startTime.before(endTime)) { - val satPos = satPass.predictor.getSatPos(startTime) - val passX = sph2CartX(satPos.azimuth, satPos.elevation, radius.toDouble()) - val passY = sph2CartY(satPos.azimuth, satPos.elevation, radius.toDouble()) - if (startTime.compareTo(satPass.aosDate) == 0) { + val currentTime = satPass.aosDate + while (currentTime.before(satPass.losDate)) { + val satPos = satPass.predictor.getSatPos(currentTime) + val passX = sph2CartX(satPos.azimuth, satPos.elevation, radarRadius.toDouble()) + val passY = sph2CartY(satPos.azimuth, satPos.elevation, radarRadius.toDouble()) + if (currentTime.compareTo(satPass.aosDate) == 0) { satTrack.moveTo(passX, -passY) } else { satTrack.lineTo(passX, -passY) } - startTime.time += 15000 + currentTime.time += 15000 } } + private fun createPassTrajectoryArrow() { + val radius = satSize + val sides = 3 + val angle = 2.0 * Math.PI / sides + arrowPath.moveTo((radius * cos(angle)).toFloat(), (radius * sin(angle)).toFloat()) + for (i in 1 until sides) { + val x = (radius * cos(angle - angle * i)).toFloat() + val y = (radius * sin(angle - angle * i)).toFloat() + arrowPath.lineTo(x, y) + } + arrowPath.close() + val trackLength = PathMeasure(satTrack, false).length + val quarter = trackLength / 4f + val center = trackLength / 2f + val effect = PathDashPathEffect(arrowPath, center, quarter, PathDashPathEffect.Style.ROTATE) + arrowPaint.pathEffect = effect + } + private fun drawRadarView(canvas: Canvas) { - canvas.drawLine(-radius, 0f, radius, 0f, radarPaint) - canvas.drawLine(0f, -radius, 0f, radius, radarPaint) - canvas.drawCircle(0f, 0f, radius, radarPaint) - canvas.drawCircle(0f, 0f, (radius / 3) * 2, radarPaint) - canvas.drawCircle(0f, 0f, radius / 3, radarPaint) + canvas.drawLine(-radarRadius, 0f, radarRadius, 0f, radarPaint) + canvas.drawLine(0f, -radarRadius, 0f, radarRadius, radarPaint) + canvas.drawCircle(0f, 0f, radarRadius, radarPaint) + canvas.drawCircle(0f, 0f, (radarRadius / 3) * 2, radarPaint) + canvas.drawCircle(0f, 0f, radarRadius / 3, radarPaint) } private fun drawRadarText(canvas: Canvas) { - canvas.drawText("N", scale, -radius + txtSize - scale * 2, txtPaint) - canvas.drawText("30°", scale, -((radius / 3) * 2) - scale * 2, txtPaint) - canvas.drawText("60°", scale, -(radius / 3) - scale * 2, txtPaint) - canvas.drawText("90°", scale, -scale * 2, txtPaint) + canvas.drawText("N", scale, -radarRadius + txtSize - strokeSize, textPaint) + canvas.drawText("30°", scale, -((radarRadius / 3) * 2) - strokeSize, textPaint) + canvas.drawText("60°", scale, -(radarRadius / 3) - strokeSize, textPaint) + canvas.drawText("90°", scale, -strokeSize, textPaint) } private fun drawSatellite(canvas: Canvas, satPass: SatPass) { val satPos = satPass.predictor.getSatPos(Date()) if (satPos.elevation > 0) { - val x = sph2CartX(satPos.azimuth, satPos.elevation, radius.toDouble()) - val y = sph2CartY(satPos.azimuth, satPos.elevation, radius.toDouble()) - canvas.drawCircle(x, -y, txtSize / 2.4f, satPaint) + val satX = sph2CartX(satPos.azimuth, satPos.elevation, radarRadius.toDouble()) + val satY = sph2CartY(satPos.azimuth, satPos.elevation, radarRadius.toDouble()) + canvas.drawCircle(satX, -satY, satSize, satPaint) } } - private fun drawOrientation(canvas: Canvas, azimuth: Float, pitch: Float) { + private fun drawCrosshair(canvas: Canvas, azimuth: Float, pitch: Float) { val azimuthRad = Math.toRadians(azimuth.toDouble()) val tmpElevation = Math.toRadians(pitch.toDouble()) val elevationRad = if (tmpElevation > 0.0) 0.0 else tmpElevation - val orientX = sph2CartX(azimuthRad, -elevationRad, radius.toDouble()) - val orientY = sph2CartY(azimuthRad, -elevationRad, radius.toDouble()) - canvas.drawLine(orientX - txtSize, -orientY, orientX + txtSize, -orientY, orientPaint) - canvas.drawLine(orientX, -orientY - txtSize, orientX, -orientY + txtSize, orientPaint) - canvas.drawCircle(orientX, -orientY, txtSize / 2, orientPaint) + val crossX = sph2CartX(azimuthRad, -elevationRad, radarRadius.toDouble()) + val crossY = sph2CartY(azimuthRad, -elevationRad, radarRadius.toDouble()) + canvas.drawLine(crossX - txtSize, -crossY, crossX + txtSize, -crossY, trackPaint) + canvas.drawLine(crossX, -crossY - txtSize, crossX, -crossY + txtSize, trackPaint) + canvas.drawCircle(crossX, -crossY, txtSize / 2, trackPaint) } private fun sph2CartX(azimuth: Double, elevation: Double, r: Double): Float { diff --git a/build.gradle b/build.gradle index ecee18cc..b7bc3d33 100644 --- a/build.gradle +++ b/build.gradle @@ -1,8 +1,8 @@ buildscript { ext { - gradle_version = '4.2.0' + gradle_version = '4.2.1' kotlin_version = '1.5.0' - coroutines_version = '1.5.0-RC-native-mt' + coroutines_version = '1.5.0' material_version = '1.3.0' constraint_layout_version = '2.0.4' lifecycle_version = '2.3.1' @@ -14,7 +14,7 @@ buildscript { osmdroid_version = '6.1.10' timber_version = '4.7.1' junit_version = '4.13.2' - mockito_version = '3.9.0' + mockito_version = '3.10.0' leak_canary_version = '2.7' } repositories {