kopia lustrzana https://github.com/rt-bishop/Look4Sat
Reimplemented RadarView in Jetpack Compose
rodzic
442f13ad07
commit
1a67ca1227
|
@ -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)
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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(
|
||||
|
|
|
@ -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 <https://www.gnu.org/licenses/>.
|
||||
*/
|
||||
@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<SatPos> = 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<SatPos>) {
|
||||
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<SatPos>, orientation: Triple<Float, Float, Float>) {
|
||||
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<SatPos>, 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()
|
||||
}
|
||||
|
|
|
@ -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<SatPos>, orientation: Triple<Float, Float, Float>) {
|
||||
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<SatPos>, 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()
|
||||
}
|
Ładowanie…
Reference in New Issue