kopia lustrzana https://github.com/rt-bishop/Look4Sat
Reimplemented RadarView in Jetpack Compose
rodzic
442f13ad07
commit
1a67ca1227
|
@ -12,6 +12,6 @@ class ExampleInstrumentedTest {
|
||||||
@Test
|
@Test
|
||||||
fun useAppContext() {
|
fun useAppContext() {
|
||||||
val appContext = InstrumentationRegistry.getInstrumentation().targetContext
|
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.runtime.collectAsState
|
||||||
import androidx.compose.ui.Alignment
|
import androidx.compose.ui.Alignment
|
||||||
import androidx.compose.ui.Modifier
|
import androidx.compose.ui.Modifier
|
||||||
import androidx.compose.ui.platform.LocalContext
|
|
||||||
import androidx.compose.ui.res.painterResource
|
import androidx.compose.ui.res.painterResource
|
||||||
import androidx.compose.ui.res.stringResource
|
import androidx.compose.ui.res.stringResource
|
||||||
import androidx.compose.ui.text.font.FontWeight
|
import androidx.compose.ui.text.font.FontWeight
|
||||||
import androidx.compose.ui.text.style.TextAlign
|
import androidx.compose.ui.text.style.TextAlign
|
||||||
import androidx.compose.ui.unit.dp
|
import androidx.compose.ui.unit.dp
|
||||||
import androidx.compose.ui.unit.sp
|
import androidx.compose.ui.unit.sp
|
||||||
import androidx.compose.ui.viewinterop.AndroidView
|
|
||||||
import androidx.hilt.navigation.compose.hiltViewModel
|
import androidx.hilt.navigation.compose.hiltViewModel
|
||||||
import androidx.navigation.NavController
|
import androidx.navigation.NavController
|
||||||
import com.rtbishop.look4sat.R
|
import com.rtbishop.look4sat.R
|
||||||
import com.rtbishop.look4sat.domain.model.SatRadio
|
import com.rtbishop.look4sat.domain.model.SatRadio
|
||||||
import timber.log.Timber
|
|
||||||
|
|
||||||
@Composable
|
@Composable
|
||||||
fun RadarScreen(navController: NavController) {
|
fun RadarScreen(navController: NavController) {
|
||||||
|
@ -65,19 +62,6 @@ fun RadarScreen(navController: NavController) {
|
||||||
ElevatedCard(modifier = Modifier
|
ElevatedCard(modifier = Modifier
|
||||||
.fillMaxSize()
|
.fillMaxSize()
|
||||||
.weight(1f)) {
|
.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 ->
|
radarData.value?.let { data ->
|
||||||
orientation.value?.let { triple ->
|
orientation.value?.let { triple ->
|
||||||
RadarViewCompose(
|
RadarViewCompose(
|
||||||
|
|
|
@ -1,265 +1,142 @@
|
||||||
/*
|
@file:OptIn(ExperimentalTextApi::class)
|
||||||
* 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/>.
|
|
||||||
*/
|
|
||||||
package com.rtbishop.look4sat.presentation.radarScreen
|
package com.rtbishop.look4sat.presentation.radarScreen
|
||||||
|
|
||||||
import android.content.Context
|
import androidx.compose.foundation.Canvas
|
||||||
import android.graphics.*
|
import androidx.compose.foundation.layout.fillMaxSize
|
||||||
import android.view.View
|
import androidx.compose.runtime.Composable
|
||||||
import androidx.core.content.ContextCompat
|
import androidx.compose.runtime.mutableStateOf
|
||||||
import com.rtbishop.look4sat.R
|
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.PI_2
|
||||||
import com.rtbishop.look4sat.domain.predict.SatPos
|
import com.rtbishop.look4sat.domain.predict.SatPos
|
||||||
import com.rtbishop.look4sat.domain.predict.TWO_PI
|
import com.rtbishop.look4sat.domain.predict.TWO_PI
|
||||||
import com.rtbishop.look4sat.utility.toRadians
|
import com.rtbishop.look4sat.utility.toRadians
|
||||||
import kotlin.math.cos
|
import kotlin.math.cos
|
||||||
import kotlin.math.min
|
|
||||||
import kotlin.math.sin
|
import kotlin.math.sin
|
||||||
|
|
||||||
class RadarView(context: Context) : View(context) {
|
@Composable
|
||||||
|
fun RadarViewCompose(item: SatPos, items: List<SatPos>, orientation: Triple<Float, Float, Float>) {
|
||||||
private val defaultColor = ContextCompat.getColor(context, R.color.accent)
|
val radarColor = Color(0xFFDCDCDC)
|
||||||
private val scale = resources.displayMetrics.density
|
val primaryColor = Color(0xFFFFE082)
|
||||||
private val strokeSize = scale * 2f
|
val secondaryColor = Color(0xFFDC0000)
|
||||||
private var position: SatPos? = null
|
val strokeWidth = 4f
|
||||||
private var positions: List<SatPos> = emptyList()
|
val measurer = rememberTextMeasurer()
|
||||||
|
val sweepDegrees = remember { mutableStateOf(0f) }
|
||||||
private var radarColor = ContextCompat.getColor(context, R.color.textMain)
|
val trackCreated = remember { mutableStateOf(false) }
|
||||||
private var radarCircleNum = 3
|
val trackPath = remember { mutableStateOf(Path()) }
|
||||||
private var radarPaint = Paint(Paint.ANTI_ALIAS_FLAG).apply {
|
val trackEffect = remember { mutableStateOf(PathEffect.cornerPathEffect(0f)) }
|
||||||
color = radarColor
|
Canvas(modifier = Modifier.fillMaxSize()) {
|
||||||
style = Paint.Style.STROKE
|
val radius = (size.minDimension / 2f) * 0.95f
|
||||||
strokeWidth = strokeSize
|
if (!trackCreated.value) {
|
||||||
}
|
trackPath.value = createTrackPath(items, radius)
|
||||||
|
trackEffect.value = createTrackEffect(trackPath.value)
|
||||||
private var shouldShowSweep = true
|
trackCreated.value = 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
|
|
||||||
}
|
}
|
||||||
}
|
rotate(-orientation.first) {
|
||||||
|
drawSweep(center, sweepDegrees.value, radius, primaryColor)
|
||||||
override fun onDraw(canvas: Canvas) {
|
drawRadar(radius, radarColor, strokeWidth, 3)
|
||||||
val radarWidth = width - paddingLeft - paddingRight
|
drawInfo(radius, primaryColor, measurer, 3)
|
||||||
val radarHeight = height - paddingTop - paddingBottom
|
translate(center.x, center.y) {
|
||||||
val radarRadius = min(width, height) * 0.48f
|
drawTrack(trackPath.value, trackEffect.value, secondaryColor, primaryColor)
|
||||||
val cx = paddingLeft + radarWidth / 2f
|
if (item.elevation > 0) drawPosition(item, radius, primaryColor)
|
||||||
val cy = paddingTop + radarHeight / 2f
|
drawAim(orientation.first, orientation.second, radius, strokeWidth, secondaryColor)
|
||||||
|
|
||||||
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)
|
|
||||||
}
|
}
|
||||||
|
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()
|
private fun DrawScope.drawRadar(radius: Float, color: Color, width: Float, circles: Int) {
|
||||||
val tmpElevation = pitch.toDouble().toRadians()
|
for (i in 0 until circles) {
|
||||||
val elevationRad = if (tmpElevation > 0.0) 0.0 else tmpElevation
|
val circleRadius = radius - radius / circles.toFloat() * i.toFloat()
|
||||||
val crossX = sph2CartX(azimuthRad, -elevationRad, radarRadius.toDouble())
|
drawCircle(color, circleRadius, style = Stroke(width))
|
||||||
val crossY = sph2CartY(azimuthRad, -elevationRad, radarRadius.toDouble())
|
}
|
||||||
canvas.drawLine(crossX - radarTextSize, -crossY, crossX + radarTextSize, -crossY, aimPaint)
|
drawLine(color, center.copy(x = center.x - radius), center.copy(x = center.x + radius), width)
|
||||||
canvas.drawLine(crossX, -crossY - radarTextSize, crossX, -crossY + radarTextSize, aimPaint)
|
drawLine(color, center.copy(y = center.y - radius), center.copy(y = center.y + radius), width)
|
||||||
canvas.drawCircle(crossX, -crossY, radarTextSize / 2, aimPaint)
|
}
|
||||||
}
|
|
||||||
|
private fun DrawScope.drawInfo(radius: Float, color: Color, measurer: TextMeasurer, circles: Int) {
|
||||||
private fun sph2CartX(azimuth: Double, elevation: Double, r: Double): Float {
|
for (i in 0 until circles) {
|
||||||
val radius = r * (PI_2 - elevation) / PI_2
|
val textY = (radius - radius / circles * i) - 32f
|
||||||
return (radius * cos(PI_2 - azimuth)).toFloat()
|
val textDeg = " ${(90 / circles) * (circles - i)}°"
|
||||||
}
|
drawText(measurer, textDeg, center.copy(y = textY), style = TextStyle(color, 15.sp))
|
||||||
|
}
|
||||||
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 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 createPassTrajectory(radarRadius: Float) {
|
}
|
||||||
positions.forEachIndexed { index, satPos ->
|
|
||||||
val passX = sph2CartX(satPos.azimuth, satPos.elevation, radarRadius.toDouble())
|
private fun DrawScope.drawPosition(item: SatPos, radius: Float, color: Color) {
|
||||||
val passY = sph2CartY(satPos.azimuth, satPos.elevation, radarRadius.toDouble())
|
val satX = sph2CartX(item.azimuth, item.elevation, radius.toDouble())
|
||||||
if (index == 0) {
|
val satY = sph2CartY(item.azimuth, item.elevation, radius.toDouble())
|
||||||
trackPath.moveTo(passX, -passY)
|
drawCircle(color, 18f, center.copy(satX, -satY))
|
||||||
} else {
|
}
|
||||||
trackPath.lineTo(passX, -passY)
|
|
||||||
}
|
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()
|
||||||
private fun createPassTrajectoryArrow() {
|
val elevRadians = if (tempElevRadians > 0.0) 0.0 else tempElevRadians
|
||||||
val radius = beaconSize
|
val aimX = sph2CartX(azimRadians, -elevRadians, radius.toDouble())
|
||||||
val sides = 3
|
val aimY = sph2CartY(azimRadians, -elevRadians, radius.toDouble())
|
||||||
val angle = TWO_PI / sides
|
try {
|
||||||
arrowPath.moveTo((radius * cos(angle)).toFloat(), (radius * sin(angle)).toFloat())
|
drawLine(color, center.copy(aimX - size, -aimY), center.copy(aimX + size, -aimY), width)
|
||||||
for (i in 1 until sides) {
|
drawLine(color, center.copy(aimX, -aimY - size), center.copy(aimX, -aimY + size), width)
|
||||||
val x = (radius * cos(angle - angle * i)).toFloat()
|
drawCircle(color, size / 2, center.copy(aimX, -aimY), style = Stroke(width))
|
||||||
val y = (radius * sin(angle - angle * i)).toFloat()
|
} catch (exception: Exception) {
|
||||||
arrowPath.lineTo(x, y)
|
// Timber.d(exception)
|
||||||
}
|
}
|
||||||
arrowPath.close()
|
}
|
||||||
val trackLength = PathMeasure(trackPath, false).length
|
|
||||||
val quarter = trackLength / 4f
|
private fun DrawScope.drawSweep(center: Offset, degrees: Float, radius: Float, color: Color) {
|
||||||
val center = trackLength / 2f
|
val colors = listOf(Color.Transparent, Color(0x80FFE082), color)
|
||||||
val effect = PathDashPathEffect(arrowPath, center, quarter, PathDashPathEffect.Style.ROTATE)
|
val colorStops = listOf(0.64f, 0.995f, 1f)
|
||||||
arrowPaint.pathEffect = effect
|
val brush = ShaderBrush(SweepGradientShader(center, colors, colorStops))
|
||||||
}
|
rotate(-90 + degrees, center) { drawCircle(brush, radius, style = Fill) }
|
||||||
|
}
|
||||||
private fun drawRadarSweep(canvas: Canvas, cx: Float, cy: Float, radius: Float) {
|
|
||||||
val sweepGradient = SweepGradient(
|
private fun createTrackPath(positions: List<SatPos>, radius: Float): Path {
|
||||||
cx,
|
val trackPath = Path()
|
||||||
cy,
|
positions.forEachIndexed { index, satPos ->
|
||||||
intArrayOf(
|
val passX = sph2CartX(satPos.azimuth, satPos.elevation, radius.toDouble())
|
||||||
Color.TRANSPARENT,
|
val passY = sph2CartY(satPos.azimuth, satPos.elevation, radius.toDouble())
|
||||||
changeAlpha(sweepColor, 0),
|
if (index == 0) trackPath.moveTo(passX, -passY) else trackPath.lineTo(passX, -passY)
|
||||||
changeAlpha(sweepColor, 164),
|
}
|
||||||
changeAlpha(sweepColor, 255),
|
return trackPath
|
||||||
changeAlpha(sweepColor, 255)
|
}
|
||||||
),
|
|
||||||
floatArrayOf(0.0f, 0.55f, 0.996f, 0.999f, 1f)
|
private fun createTrackEffect(trackPath: Path): PathEffect {
|
||||||
)
|
val shape = Path()
|
||||||
sweepPaint.shader = sweepGradient
|
val shapeSides = 3
|
||||||
canvas.rotate(-90 + sweepDegrees, cx, cy)
|
val shapeRadius = 18f
|
||||||
canvas.drawCircle(cx, cy, radius, sweepPaint)
|
val angle = TWO_PI / shapeSides
|
||||||
}
|
shape.moveTo((shapeRadius * cos(angle)).toFloat(), (shapeRadius * sin(angle)).toFloat())
|
||||||
|
for (i in 1 until shapeSides) {
|
||||||
private fun changeAlpha(color: Int, alpha: Int): Int {
|
val x = (shapeRadius * cos(angle - angle * i)).toFloat()
|
||||||
val red = Color.red(color)
|
val y = (shapeRadius * sin(angle - angle * i)).toFloat()
|
||||||
val green = Color.green(color)
|
shape.lineTo(x, y)
|
||||||
val blue = Color.blue(color)
|
}
|
||||||
return Color.argb(alpha, red, green, blue)
|
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