From 38ad7c46a8568e15eea6fe5c527870f743ee9c8b Mon Sep 17 00:00:00 2001 From: Arty Bishop Date: Mon, 6 Mar 2023 18:30:28 +0000 Subject: [PATCH] Reimplemented RadarView in Jetpack Compose --- .../look4sat/ExampleInstrumentedTest.kt | 2 +- .../presentation/radarScreen/RadarScreen.kt | 16 - .../presentation/radarScreen/RadarView.kt | 379 ++++++------------ .../radarScreen/RadarViewCompose.kt | 155 ------- 4 files changed, 129 insertions(+), 423 deletions(-) delete mode 100644 app/src/main/java/com/rtbishop/look4sat/presentation/radarScreen/RadarViewCompose.kt diff --git a/app/src/androidTest/java/com/rtbishop/look4sat/ExampleInstrumentedTest.kt b/app/src/androidTest/java/com/rtbishop/look4sat/ExampleInstrumentedTest.kt index f99ae446..27b6a311 100644 --- a/app/src/androidTest/java/com/rtbishop/look4sat/ExampleInstrumentedTest.kt +++ b/app/src/androidTest/java/com/rtbishop/look4sat/ExampleInstrumentedTest.kt @@ -12,6 +12,6 @@ class ExampleInstrumentedTest { @Test fun useAppContext() { val appContext = InstrumentationRegistry.getInstrumentation().targetContext - assertEquals("com.rtbishop.bbctestapp", appContext.packageName) + assertEquals("com.rtbishop.look4sat", appContext.packageName) } } diff --git a/app/src/main/java/com/rtbishop/look4sat/presentation/radarScreen/RadarScreen.kt b/app/src/main/java/com/rtbishop/look4sat/presentation/radarScreen/RadarScreen.kt index cbc26b19..b8422118 100644 --- a/app/src/main/java/com/rtbishop/look4sat/presentation/radarScreen/RadarScreen.kt +++ b/app/src/main/java/com/rtbishop/look4sat/presentation/radarScreen/RadarScreen.kt @@ -9,19 +9,16 @@ import androidx.compose.runtime.Composable import androidx.compose.runtime.collectAsState import androidx.compose.ui.Alignment import androidx.compose.ui.Modifier -import androidx.compose.ui.platform.LocalContext import androidx.compose.ui.res.painterResource import androidx.compose.ui.res.stringResource import androidx.compose.ui.text.font.FontWeight import androidx.compose.ui.text.style.TextAlign import androidx.compose.ui.unit.dp import androidx.compose.ui.unit.sp -import androidx.compose.ui.viewinterop.AndroidView import androidx.hilt.navigation.compose.hiltViewModel import androidx.navigation.NavController import com.rtbishop.look4sat.R import com.rtbishop.look4sat.domain.model.SatRadio -import timber.log.Timber @Composable fun RadarScreen(navController: NavController) { @@ -65,19 +62,6 @@ fun RadarScreen(navController: NavController) { ElevatedCard(modifier = Modifier .fillMaxSize() .weight(1f)) { -// val context = LocalContext.current -// AndroidView({ RadarView(context) }, modifier = Modifier.fillMaxSize()) { radarView -> -// Timber.d("Radar view recomposition") -// radarView.setShowAim(true) -// radarView.setScanning(true) -// radarData.value?.let { -// radarView.setPosition(it.satPos) -// radarView.setPositions(it.satTrack) -// } -// orientation.value?.let { -// radarView.setOrientation(it.first, it.second, it.first) -// } -// } radarData.value?.let { data -> orientation.value?.let { triple -> RadarViewCompose( diff --git a/app/src/main/java/com/rtbishop/look4sat/presentation/radarScreen/RadarView.kt b/app/src/main/java/com/rtbishop/look4sat/presentation/radarScreen/RadarView.kt index a78a0e84..767ec160 100644 --- a/app/src/main/java/com/rtbishop/look4sat/presentation/radarScreen/RadarView.kt +++ b/app/src/main/java/com/rtbishop/look4sat/presentation/radarScreen/RadarView.kt @@ -1,265 +1,142 @@ -/* - * Look4Sat. Amateur radio satellite tracker and pass predictor. - * Copyright (C) 2019-2022 Arty Bishop (bishop.arty@gmail.com) - * - * This program is free software: you can redistribute it and/or modify - * it under the terms of the GNU General Public License as published by - * the Free Software Foundation, either version 3 of the License, or - * (at your option) any later version. - * - * This program is distributed in the hope that it will be useful, - * but WITHOUT ANY WARRANTY; without even the implied warranty of - * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the - * GNU General Public License for more details. - * - * You should have received a copy of the GNU General Public License - * along with this program. If not, see . - */ +@file:OptIn(ExperimentalTextApi::class) + package com.rtbishop.look4sat.presentation.radarScreen -import android.content.Context -import android.graphics.* -import android.view.View -import androidx.core.content.ContextCompat -import com.rtbishop.look4sat.R +import androidx.compose.foundation.Canvas +import androidx.compose.foundation.layout.fillMaxSize +import androidx.compose.runtime.Composable +import androidx.compose.runtime.mutableStateOf +import androidx.compose.runtime.remember +import androidx.compose.ui.Modifier +import androidx.compose.ui.geometry.Offset +import androidx.compose.ui.graphics.* +import androidx.compose.ui.graphics.drawscope.* +import androidx.compose.ui.text.* +import androidx.compose.ui.unit.sp import com.rtbishop.look4sat.domain.predict.PI_2 import com.rtbishop.look4sat.domain.predict.SatPos import com.rtbishop.look4sat.domain.predict.TWO_PI import com.rtbishop.look4sat.utility.toRadians import kotlin.math.cos -import kotlin.math.min import kotlin.math.sin -class RadarView(context: Context) : View(context) { - - private val defaultColor = ContextCompat.getColor(context, R.color.accent) - private val scale = resources.displayMetrics.density - private val strokeSize = scale * 2f - private var position: SatPos? = null - private var positions: List = emptyList() - - private var radarColor = ContextCompat.getColor(context, R.color.textMain) - private var radarCircleNum = 3 - private var radarPaint = Paint(Paint.ANTI_ALIAS_FLAG).apply { - color = radarColor - style = Paint.Style.STROKE - strokeWidth = strokeSize - } - - private var shouldShowSweep = true - private var sweepColor = defaultColor - private var sweepDegrees = 0f - private var sweepSpeed = 15.0f - private var sweepPaint = Paint(Paint.ANTI_ALIAS_FLAG).apply { - color = sweepColor - } - - private var shouldShowBeacons = true - private var beaconColor = defaultColor - private var beaconSize = scale * 7f - private var beaconPaint = Paint(Paint.ANTI_ALIAS_FLAG).apply { - color = beaconColor - style = Paint.Style.FILL - } - - private var radarTextSize = scale * 16f - private var radarTextColor = defaultColor - private var radarTextPaint = Paint(Paint.ANTI_ALIAS_FLAG).apply { - color = radarTextColor - textSize = radarTextSize - } - - private var isTrackCreated = false - private val trackPath: Path = Path() - private var trackPaint = Paint(Paint.ANTI_ALIAS_FLAG).apply { - color = Color.RED - style = Paint.Style.STROKE - strokeWidth = strokeSize - } - - private val arrowPath = Path() - private var arrowPaint = Paint(Paint.ANTI_ALIAS_FLAG).apply { - color = ContextCompat.getColor(context, R.color.accent) - style = Paint.Style.FILL - strokeWidth = strokeSize - } - - private var shouldShowAim = true - private var aimColor = Color.RED - private var aimPaint = Paint(Paint.ANTI_ALIAS_FLAG).apply { - color = aimColor - style = Paint.Style.STROKE - strokeWidth = strokeSize - } - - private var shouldRotateRadar = true - private var azimuth: Float = 0f - private var pitch: Float = 0f - private var roll: Float = 0f - - fun setScanning(isScanning: Boolean) { - shouldShowSweep = isScanning - } - - fun setPosition(position: SatPos) { - this.position = position - } - - fun setPositions(positions: List) { - this.positions = positions - } - - fun setShowAim(showAim: Boolean) { - shouldShowAim = showAim - } - - fun setOrientation(azimuth: Float, pitch: Float, roll: Float) { - this.azimuth = azimuth - this.pitch = pitch - this.roll = roll - if (shouldRotateRadar) { - this.rotation = -azimuth +@Composable +fun RadarViewCompose(item: SatPos, items: List, orientation: Triple) { + val radarColor = Color(0xFFDCDCDC) + val primaryColor = Color(0xFFFFE082) + val secondaryColor = Color(0xFFDC0000) + val strokeWidth = 4f + val measurer = rememberTextMeasurer() + val sweepDegrees = remember { mutableStateOf(0f) } + val trackCreated = remember { mutableStateOf(false) } + val trackPath = remember { mutableStateOf(Path()) } + val trackEffect = remember { mutableStateOf(PathEffect.cornerPathEffect(0f)) } + Canvas(modifier = Modifier.fillMaxSize()) { + val radius = (size.minDimension / 2f) * 0.95f + if (!trackCreated.value) { + trackPath.value = createTrackPath(items, radius) + trackEffect.value = createTrackEffect(trackPath.value) + trackCreated.value = true } - } - - override fun onDraw(canvas: Canvas) { - val radarWidth = width - paddingLeft - paddingRight - val radarHeight = height - paddingTop - paddingBottom - val radarRadius = min(width, height) * 0.48f - val cx = paddingLeft + radarWidth / 2f - val cy = paddingTop + radarHeight / 2f - - if (positions.isNotEmpty() && !isTrackCreated) { - createPassTrajectory(radarRadius) - createPassTrajectoryArrow() - isTrackCreated = true - } - - canvas.drawColor(ContextCompat.getColor(context, R.color.cardRegular)) - drawRadarCircle(canvas, cx, cy, radarRadius) - drawRadarCross(canvas, cx, cy, radarRadius) - drawRadarText(canvas, cx, radarRadius) - - canvas.translate(cx, cy) - if (positions.isNotEmpty()) { - canvas.drawPath(trackPath, trackPaint) - canvas.drawPath(trackPath, arrowPaint) - } - if (shouldShowBeacons) { - drawSatellite(canvas, radarRadius) - } - if (shouldShowAim) { - drawCrosshair(canvas, azimuth, pitch, radarRadius) - } - if (shouldShowSweep) { - canvas.translate(-cx, -cy) - drawRadarSweep(canvas, cx, cy, radarRadius) - sweepDegrees = (sweepDegrees + 360 / sweepSpeed / 60) % 360 - } - invalidate() - } - - private fun drawRadarCircle(canvas: Canvas, cx: Float, cy: Float, radius: Float) { - for (i in 0 until radarCircleNum) { - canvas.drawCircle(cx, cy, radius - radius / radarCircleNum * i, radarPaint) - } - } - - private fun drawRadarCross(canvas: Canvas, cx: Float, cy: Float, radius: Float) { - canvas.drawLine(cx - radius, cy, cx + radius, cy, radarPaint) - canvas.drawLine(cx, cy - radius, cx, cy + radius, radarPaint) - } - - private fun drawRadarText(canvas: Canvas, cx: Float, radius: Float) { - for (i in 0 until radarCircleNum) { - val degText = " ${(90 / radarCircleNum) * (radarCircleNum - i)}°" - canvas.drawText(degText, cx, radius - radius / radarCircleNum * i, radarTextPaint) - } - } - - private fun drawSatellite(canvas: Canvas, radarRadius: Float) { - position?.let { satPos -> - if (satPos.elevation > 0) { - val satX = sph2CartX(satPos.azimuth, satPos.elevation, radarRadius.toDouble()) - val satY = sph2CartY(satPos.azimuth, satPos.elevation, radarRadius.toDouble()) - canvas.drawCircle(satX, -satY, beaconSize, beaconPaint) + rotate(-orientation.first) { + drawSweep(center, sweepDegrees.value, radius, primaryColor) + drawRadar(radius, radarColor, strokeWidth, 3) + drawInfo(radius, primaryColor, measurer, 3) + translate(center.x, center.y) { + drawTrack(trackPath.value, trackEffect.value, secondaryColor, primaryColor) + if (item.elevation > 0) drawPosition(item, radius, primaryColor) + drawAim(orientation.first, orientation.second, radius, strokeWidth, secondaryColor) } + sweepDegrees.value = (sweepDegrees.value + 360 / 12.0f / 60) % 360 } } - - private fun drawCrosshair(canvas: Canvas, azimuth: Float, pitch: Float, radarRadius: Float) { - val azimuthRad = azimuth.toDouble().toRadians() - val tmpElevation = pitch.toDouble().toRadians() - val elevationRad = if (tmpElevation > 0.0) 0.0 else tmpElevation - val crossX = sph2CartX(azimuthRad, -elevationRad, radarRadius.toDouble()) - val crossY = sph2CartY(azimuthRad, -elevationRad, radarRadius.toDouble()) - canvas.drawLine(crossX - radarTextSize, -crossY, crossX + radarTextSize, -crossY, aimPaint) - canvas.drawLine(crossX, -crossY - radarTextSize, crossX, -crossY + radarTextSize, aimPaint) - canvas.drawCircle(crossX, -crossY, radarTextSize / 2, aimPaint) - } - - private fun sph2CartX(azimuth: Double, elevation: Double, r: Double): Float { - val radius = r * (PI_2 - elevation) / PI_2 - return (radius * cos(PI_2 - azimuth)).toFloat() - } - - private fun sph2CartY(azimuth: Double, elevation: Double, r: Double): Float { - val radius = r * (PI_2 - elevation) / PI_2 - return (radius * sin(PI_2 - azimuth)).toFloat() - } - - private fun createPassTrajectory(radarRadius: Float) { - positions.forEachIndexed { index, satPos -> - val passX = sph2CartX(satPos.azimuth, satPos.elevation, radarRadius.toDouble()) - val passY = sph2CartY(satPos.azimuth, satPos.elevation, radarRadius.toDouble()) - if (index == 0) { - trackPath.moveTo(passX, -passY) - } else { - trackPath.lineTo(passX, -passY) - } - } - } - - private fun createPassTrajectoryArrow() { - val radius = beaconSize - val sides = 3 - val angle = TWO_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(trackPath, false).length - val quarter = trackLength / 4f - val center = trackLength / 2f - val effect = PathDashPathEffect(arrowPath, center, quarter, PathDashPathEffect.Style.ROTATE) - arrowPaint.pathEffect = effect - } - - private fun drawRadarSweep(canvas: Canvas, cx: Float, cy: Float, radius: Float) { - val sweepGradient = SweepGradient( - cx, - cy, - intArrayOf( - Color.TRANSPARENT, - changeAlpha(sweepColor, 0), - changeAlpha(sweepColor, 164), - changeAlpha(sweepColor, 255), - changeAlpha(sweepColor, 255) - ), - floatArrayOf(0.0f, 0.55f, 0.996f, 0.999f, 1f) - ) - sweepPaint.shader = sweepGradient - canvas.rotate(-90 + sweepDegrees, cx, cy) - canvas.drawCircle(cx, cy, radius, sweepPaint) - } - - private fun changeAlpha(color: Int, alpha: Int): Int { - val red = Color.red(color) - val green = Color.green(color) - val blue = Color.blue(color) - return Color.argb(alpha, red, green, blue) - } +} + +private fun DrawScope.drawRadar(radius: Float, color: Color, width: Float, circles: Int) { + for (i in 0 until circles) { + val circleRadius = radius - radius / circles.toFloat() * i.toFloat() + drawCircle(color, circleRadius, style = Stroke(width)) + } + drawLine(color, center.copy(x = center.x - radius), center.copy(x = center.x + radius), width) + drawLine(color, center.copy(y = center.y - radius), center.copy(y = center.y + radius), width) +} + +private fun DrawScope.drawInfo(radius: Float, color: Color, measurer: TextMeasurer, circles: Int) { + for (i in 0 until circles) { + val textY = (radius - radius / circles * i) - 32f + val textDeg = " ${(90 / circles) * (circles - i)}°" + drawText(measurer, textDeg, center.copy(y = textY), style = TextStyle(color, 15.sp)) + } +} + +private fun DrawScope.drawTrack(path: Path, effect: PathEffect, color: Color, effectColor: Color) { + drawPath(path, color, style = Stroke(4f)) + drawPath(path, effectColor, style = Stroke(pathEffect = effect)) +} + +private fun DrawScope.drawPosition(item: SatPos, radius: Float, color: Color) { + val satX = sph2CartX(item.azimuth, item.elevation, radius.toDouble()) + val satY = sph2CartY(item.azimuth, item.elevation, radius.toDouble()) + drawCircle(color, 18f, center.copy(satX, -satY)) +} + +private fun DrawScope.drawAim(azim: Float, elev: Float, radius: Float, width: Float, color: Color) { + val size = 36f + val azimRadians = azim.toDouble().toRadians() + val tempElevRadians = elev.toDouble().toRadians() + val elevRadians = if (tempElevRadians > 0.0) 0.0 else tempElevRadians + val aimX = sph2CartX(azimRadians, -elevRadians, radius.toDouble()) + val aimY = sph2CartY(azimRadians, -elevRadians, radius.toDouble()) + try { + drawLine(color, center.copy(aimX - size, -aimY), center.copy(aimX + size, -aimY), width) + drawLine(color, center.copy(aimX, -aimY - size), center.copy(aimX, -aimY + size), width) + drawCircle(color, size / 2, center.copy(aimX, -aimY), style = Stroke(width)) + } catch (exception: Exception) { +// Timber.d(exception) + } +} + +private fun DrawScope.drawSweep(center: Offset, degrees: Float, radius: Float, color: Color) { + val colors = listOf(Color.Transparent, Color(0x80FFE082), color) + val colorStops = listOf(0.64f, 0.995f, 1f) + val brush = ShaderBrush(SweepGradientShader(center, colors, colorStops)) + rotate(-90 + degrees, center) { drawCircle(brush, radius, style = Fill) } +} + +private fun createTrackPath(positions: List, radius: Float): Path { + val trackPath = Path() + positions.forEachIndexed { index, satPos -> + val passX = sph2CartX(satPos.azimuth, satPos.elevation, radius.toDouble()) + val passY = sph2CartY(satPos.azimuth, satPos.elevation, radius.toDouble()) + if (index == 0) trackPath.moveTo(passX, -passY) else trackPath.lineTo(passX, -passY) + } + return trackPath +} + +private fun createTrackEffect(trackPath: Path): PathEffect { + val shape = Path() + val shapeSides = 3 + val shapeRadius = 18f + val angle = TWO_PI / shapeSides + shape.moveTo((shapeRadius * cos(angle)).toFloat(), (shapeRadius * sin(angle)).toFloat()) + for (i in 1 until shapeSides) { + val x = (shapeRadius * cos(angle - angle * i)).toFloat() + val y = (shapeRadius * sin(angle - angle * i)).toFloat() + shape.lineTo(x, y) + } + shape.close() + val trackLength = PathMeasure().apply { setPath(trackPath, false) }.length + val advance = trackLength / 2f + val phase = trackLength / 4f + return PathEffect.stampedPathEffect(shape, advance, phase, StampedPathEffectStyle.Rotate) +} + +private fun sph2CartX(azim: Double, elev: Double, r: Double): Float { + val radius = r * (PI_2 - elev) / PI_2 + return (radius * cos(PI_2 - azim)).toFloat() +} + +private fun sph2CartY(azim: Double, elev: Double, r: Double): Float { + val radius = r * (PI_2 - elev) / PI_2 + return (radius * sin(PI_2 - azim)).toFloat() } diff --git a/app/src/main/java/com/rtbishop/look4sat/presentation/radarScreen/RadarViewCompose.kt b/app/src/main/java/com/rtbishop/look4sat/presentation/radarScreen/RadarViewCompose.kt deleted file mode 100644 index 4cff8156..00000000 --- a/app/src/main/java/com/rtbishop/look4sat/presentation/radarScreen/RadarViewCompose.kt +++ /dev/null @@ -1,155 +0,0 @@ -@file:OptIn(ExperimentalTextApi::class) - -package com.rtbishop.look4sat.presentation.radarScreen - -import androidx.compose.foundation.Canvas -import androidx.compose.foundation.layout.fillMaxSize -import androidx.compose.runtime.Composable -import androidx.compose.runtime.mutableStateOf -import androidx.compose.runtime.remember -import androidx.compose.ui.Modifier -import androidx.compose.ui.graphics.* -import androidx.compose.ui.graphics.drawscope.* -import androidx.compose.ui.text.* -import androidx.compose.ui.unit.sp -import com.rtbishop.look4sat.domain.predict.PI_2 -import com.rtbishop.look4sat.domain.predict.SatPos -import com.rtbishop.look4sat.domain.predict.TWO_PI -import com.rtbishop.look4sat.utility.toRadians -import kotlin.math.cos -import kotlin.math.sin - -@Composable -fun RadarViewCompose(item: SatPos, items: List, orientation: Triple) { - val measurer = rememberTextMeasurer() - val newTrackPath = remember { mutableStateOf(Path()) } - val newArrowPath = remember { mutableStateOf(Path()) } - var isTrackCreated = false - val yellowColor = Color(0xFFFFE082) - val whiteColor = Color(0xCCFFFFFF) - val redColor = Color(0xFFB71C1C) - val strokeWidth = 4f - Canvas(modifier = Modifier.fillMaxSize()) { - val radius = (size.minDimension / 2f) * 0.95f - if (items.isNotEmpty() && !isTrackCreated) { - newTrackPath.value = createPassTrajectory(items, radius) - newArrowPath.value = createPassTrajectoryArrow() - isTrackCreated = true - } - rotate(-orientation.first) { - drawRadarCirle(radius, whiteColor, strokeWidth) - drawRadarCross(radius, whiteColor, strokeWidth) - drawRadarText(radius, yellowColor, measurer) - translate(center.x, center.y) { - if (items.isNotEmpty()) { - val effect = createPathEffect(newTrackPath.value, newArrowPath.value) - drawPath(newTrackPath.value, redColor, style = Stroke(4f)) - drawPath(newTrackPath.value, yellowColor, style = Stroke(pathEffect = effect)) - } - drawSatellite(item, radius, yellowColor) - drawCrosshair(orientation.first, orientation.second, radius, strokeWidth) - } - } - } -} - -private fun DrawScope.drawRadarCirle(radius: Float, color: Color, width: Float, circles: Int = 3) { - for (i in 0 until circles) { - val circleRadius = radius - radius / circles.toFloat() * i.toFloat() - drawCircle(color, circleRadius, style = Stroke(width)) - } -} - -private fun DrawScope.drawRadarCross(radius: Float, color: Color, width: Float) { - drawLine(color, center, center.copy(x = center.x - radius), strokeWidth = width) - drawLine(color, center, center.copy(x = center.x + radius), strokeWidth = width) - drawLine(color, center, center.copy(y = center.y - radius), strokeWidth = width) - drawLine(color, center, center.copy(y = center.y + radius), strokeWidth = width) -} - -private fun DrawScope.drawRadarText( - radius: Float, color: Color, measurer: TextMeasurer, circles: Int = 3 -) { - for (i in 0..circles) { - val textY = (radius - radius / circles * i) + 25f - val textDeg = " ${(90 / circles) * (circles - i)}°" - drawText(measurer, textDeg, center.copy(y = textY), style = TextStyle(color, 16.sp)) - } -} - -private fun DrawScope.drawSatellite(item: SatPos, radius: Float, color: Color) { - if (item.elevation > 0) { - val satX = sph2CartX(item.azimuth, item.elevation, radius.toDouble()) - val satY = sph2CartY(item.azimuth, item.elevation, radius.toDouble()) - drawCircle(color, 16f, center.copy(satX, -satY)) - } -} - -private fun DrawScope.drawCrosshair(azimuth: Float, elevation: Float, radius: Float, width: Float) { - val redColor = Color(0xFFFF0000) - val size = 36f - val azimuthRad = azimuth.toDouble().toRadians() - val tmpElevation = elevation.toDouble().toRadians() - val elevationRad = if (tmpElevation > 0.0) 0.0 else tmpElevation - val crossX = sph2CartX(azimuthRad, -elevationRad, radius.toDouble()) - val crossY = sph2CartY(azimuthRad, -elevationRad, radius.toDouble()) - drawLine( - redColor, - center.copy(crossX - size, -crossY), - center.copy(crossX + size, -crossY), - strokeWidth = width - ) - drawLine( - redColor, - center.copy(crossX, -crossY - size), - center.copy(crossX, -crossY + size), - strokeWidth = width - ) - drawCircle(redColor, size / 2, center.copy(crossX, -crossY), style = Stroke(width)) -} - -private fun createPassTrajectory(positions: List, radarRadius: Float): Path { - val trackPath = Path() - positions.forEachIndexed { index, satPos -> - val passX = sph2CartX(satPos.azimuth, satPos.elevation, radarRadius.toDouble()) - val passY = sph2CartY(satPos.azimuth, satPos.elevation, radarRadius.toDouble()) - if (index == 0) { - trackPath.moveTo(passX, -passY) - } else { - trackPath.lineTo(passX, -passY) - } - } - return trackPath -} - -private fun createPassTrajectoryArrow(): Path { - val arrowPath = Path() - val radius = 24f - val sides = 3 - val angle = TWO_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() - return arrowPath -} - -private fun createPathEffect(trackPath: Path, arrowPath: Path): PathEffect { - val trackLength = PathMeasure().apply { setPath(trackPath, false) }.length - val quarter = trackLength / 4f - val center = trackLength / 2f - return PathEffect.stampedPathEffect(arrowPath, center, quarter, StampedPathEffectStyle.Rotate) -} - -private fun sph2CartX(azimuth: Double, elevation: Double, r: Double): Float { - val radius = r * (PI_2 - elevation) / PI_2 - return (radius * cos(PI_2 - azimuth)).toFloat() -} - -private fun sph2CartY(azimuth: Double, elevation: Double, r: Double): Float { - val radius = r * (PI_2 - elevation) / PI_2 - return (radius * sin(PI_2 - azimuth)).toFloat() -}