Simplified RadarScreen/View, added sensors smoothing

This commit is contained in:
Arty Bishop committed 2026-03-28 20:04:03 +00:00
1 parent 8c1df334d8
commit 279594c425
3 files changed
+262 -358

No files matched your search

@@ -35,78 +35,109 @@ class SensorsRepo(
private val sensorManager: SensorManager,
private val sensor: Sensor?,
private val windowManager: WindowManager
) :
SensorEventListener, ISensorsRepo {
) : SensorEventListener, ISensorsRepo {
private val _orientation = MutableStateFlow(Pair(0f, 0f))
private val rotationMatrix = FloatArray(9)
private val tempMatrix = FloatArray(9)
private val orientationValues = FloatArray(3)
private var sensorAccuracy = SensorManager.SENSOR_STATUS_UNRELIABLE
private var smoothAzimuth = 0f
private var smoothPitch = 0f
private var hasInitialReading = false
companion object {
private const val SMOOTHING_FACTOR = 0.15f
}
override val orientation: StateFlow<Pair<Float, Float>> = _orientation
override fun getMagDeclination(geoPos: GeoPos, time: Long): Float {
val latitude = geoPos.latitude.toFloat()
val longitude = geoPos.longitude.toFloat()
return GeomagneticField(latitude, longitude, geoPos.altitude.toFloat(), time).declination
return GeomagneticField(
geoPos.latitude.toFloat(),
geoPos.longitude.toFloat(),
geoPos.altitude.toFloat(),
time
).declination
}
override fun enableSensor() {
hasInitialReading = false
sensor?.let { sensorManager.registerListener(this, it, 8000) }
}
override fun disableSensor() = sensorManager.unregisterListener(this)
override fun onAccuracyChanged(sensor: Sensor, accuracy: Int) {
sensorAccuracy = accuracy
}
override fun onAccuracyChanged(sensor: Sensor, accuracy: Int) = Unit
override fun onSensorChanged(event: SensorEvent) = when {
sensorAccuracy == SensorManager.SENSOR_STATUS_UNRELIABLE -> {}
event.sensor == sensor -> updateOrientation(event.values)
else -> {}
override fun onSensorChanged(event: SensorEvent) {
if (event.sensor == sensor) updateOrientation(event.values)
}
private fun getDisplayRotation(): Int {
return try {
@Suppress("DEPRECATION")
windowManager.defaultDisplay.rotation
} catch (e: Exception) {
} catch (_: Exception) {
Surface.ROTATION_0
}
}
private fun transformRotationMatrix(rotationMatrix: FloatArray, rotation: Int) {
val tempMatrix = FloatArray(9)
when (rotation) {
Surface.ROTATION_0 -> {
SensorManager.remapCoordinateSystem(rotationMatrix, SensorManager.AXIS_X, SensorManager.AXIS_Y, tempMatrix)
System.arraycopy(tempMatrix, 0, rotationMatrix, 0, 9)
}
Surface.ROTATION_90 -> {
SensorManager.remapCoordinateSystem(rotationMatrix, SensorManager.AXIS_Y, SensorManager.AXIS_MINUS_X, tempMatrix)
SensorManager.remapCoordinateSystem(tempMatrix, SensorManager.AXIS_MINUS_X, SensorManager.AXIS_Y, rotationMatrix)
}
Surface.ROTATION_180 -> {
SensorManager.remapCoordinateSystem(rotationMatrix, SensorManager.AXIS_MINUS_X, SensorManager.AXIS_MINUS_Y, tempMatrix)
System.arraycopy(tempMatrix, 0, rotationMatrix, 0, 9)
}
Surface.ROTATION_270 -> {
SensorManager.remapCoordinateSystem(rotationMatrix, SensorManager.AXIS_MINUS_Y, SensorManager.AXIS_X, tempMatrix)
SensorManager.remapCoordinateSystem(tempMatrix, SensorManager.AXIS_MINUS_X, SensorManager.AXIS_Y, rotationMatrix)
}
private fun remapForRotation(rotation: Int) {
val remapped = when (rotation) {
Surface.ROTATION_90 -> SensorManager.remapCoordinateSystem(
rotationMatrix, SensorManager.AXIS_Y, SensorManager.AXIS_MINUS_X, tempMatrix
)
Surface.ROTATION_180 -> SensorManager.remapCoordinateSystem(
rotationMatrix, SensorManager.AXIS_MINUS_X, SensorManager.AXIS_MINUS_Y, tempMatrix
)
Surface.ROTATION_270 -> SensorManager.remapCoordinateSystem(
rotationMatrix, SensorManager.AXIS_MINUS_Y, SensorManager.AXIS_X, tempMatrix
)
else -> false
}
if (remapped) System.arraycopy(tempMatrix, 0, rotationMatrix, 0, 9)
}
private fun updateOrientation(rotationVector: FloatArray) {
SensorManager.getRotationMatrixFromVector(rotationMatrix, rotationVector)
transformRotationMatrix(rotationMatrix, getDisplayRotation())
remapForRotation(getDisplayRotation())
SensorManager.getOrientation(rotationMatrix, orientationValues)
val azimuth = (orientationValues[0] * RAD2DEG).toFloat()
val pitch = (orientationValues[1] * RAD2DEG).toFloat() // roll [2]
val pitch = (orientationValues[1] * RAD2DEG).toFloat()
val magneticAzimuth = (azimuth + 360f) % 360f
val roundedAzimuth = round(magneticAzimuth * 10) / 10
val roundedPitch = round(pitch * 10) / 10
_orientation.value = Pair(roundedAzimuth, roundedPitch)
if (!hasInitialReading) {
smoothAzimuth = magneticAzimuth
smoothPitch = pitch
hasInitialReading = true
} else {
smoothAzimuth = lowPassAngle(smoothAzimuth, magneticAzimuth)
smoothPitch = lowPass(smoothPitch, pitch)
}
_orientation.value = Pair(
round(smoothAzimuth * 10) / 10,
round(smoothPitch * 10) / 10
)
}
/** Standard exponential low-pass filter. */
private fun lowPass(previous: Float, current: Float): Float {
return previous + SMOOTHING_FACTOR * (current - previous)
}
/**
* Low-pass filter that accounts for the 0°/360° wraparound.
* Always takes the shortest angular path between the two values.
*/
private fun lowPassAngle(previous: Float, current: Float): Float {
var delta = current - previous
// Normalise delta into the range (-180, 180]
while (delta > 180f) delta -= 360f
while (delta <= -180f) delta += 360f
return (previous + SMOOTHING_FACTOR * delta + 360f) % 360f
}
}
@@ -42,6 +42,7 @@ import androidx.compose.material3.MaterialTheme
import androidx.compose.material3.Surface
import androidx.compose.material3.Text
import androidx.compose.runtime.Composable
import androidx.compose.runtime.getValue
import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier
import androidx.compose.ui.draw.alpha
@@ -62,6 +63,7 @@ import androidx.navigation.NavGraphBuilder
import androidx.navigation.compose.composable
import androidx.navigation.navArgument
import com.rtbishop.look4sat.core.domain.model.SatRadio
import com.rtbishop.look4sat.core.domain.predict.OrbitalPos
import com.rtbishop.look4sat.core.domain.utility.toDegrees
import com.rtbishop.look4sat.core.presentation.EmptyListCard
import com.rtbishop.look4sat.core.presentation.IconCard
@@ -84,14 +86,13 @@ fun NavGraphBuilder.radarDestination(navigateUp: () -> Unit) {
)
composable(radarRoute, radarArgs) {
val viewModel = viewModel(RadarViewModel::class.java, factory = RadarViewModel.Factory)
val uiState = viewModel.uiState.collectAsStateWithLifecycle().value
val uiState by viewModel.uiState.collectAsStateWithLifecycle()
RadarScreen(uiState, navigateUp)
}
}
@Composable
private fun RadarScreen(uiState: RadarState, navigateUp: () -> Unit) {
// BluetoothCIV.init(LocalContext.current)
val addToCalendar: () -> Unit = {
uiState.currentPass?.let { pass ->
uiState.sendAction(RadarAction.AddToCalendar(pass.name, pass.aosTime, pass.losTime))
@@ -102,9 +103,11 @@ private fun RadarScreen(uiState: RadarState, navigateUp: () -> Unit) {
Column(
modifier = Modifier
.layoutPadding()
.keepScreenOn(), verticalArrangement = Arrangement.spacedBy(6.dp)
.keepScreenOn(),
verticalArrangement = Arrangement.spacedBy(6.dp)
) {
if (isVerticalLayout()) {
val isVertical = isVerticalLayout()
if (isVertical) {
TopBar {
IconCard(action = navigateUp, resId = R.drawable.ic_back)
TimerRow(timeString = uiState.currentTime, isTimeAos = uiState.isCurrentTimeAos)
@@ -119,111 +122,28 @@ private fun RadarScreen(uiState: RadarState, navigateUp: () -> Unit) {
IconCard(action = addToCalendar, resId = R.drawable.ic_calendar)
}
}
if(isVerticalLayout()) {
ElevatedCard(modifier = Modifier.weight(1f)) {
Box(contentAlignment = Alignment.Center) {
if (uiState.orbitalPos == null) {
ElevatedCard(modifier = Modifier.fillMaxSize()) {
EmptyListCard(message = "")
}
}
uiState.orbitalPos?.let { position ->
RadarViewCompose(
item = position,
items = uiState.satTrack,
azimElev = uiState.orientationValues,
shouldShowSweep = uiState.shouldShowSweep,
shouldUseCompass = uiState.shouldUseCompass,
modifier = Modifier.align(Alignment.Center)
)
Column(
verticalArrangement = Arrangement.SpaceBetween,
modifier = Modifier
.fillMaxSize()
.padding(horizontal = 6.dp, vertical = 4.dp)
) {
Row(
horizontalArrangement = Arrangement.SpaceBetween,
modifier = Modifier.fillMaxWidth()
) {
RadarTextTop(
position.azimuth,
stringResource(R.string.radar_az_text),
true
)
RadarTextTop(
position.elevation,
stringResource(R.string.radar_el_text),
false
)
}
Row(
horizontalArrangement = Arrangement.SpaceBetween,
modifier = Modifier.fillMaxWidth()
) {
RadarTextBottom(
position.altitude,
stringResource(R.string.radar_alt_text),
true
)
RadarTextBottom(
position.distance,
stringResource(R.string.radar_dist_text),
false
)
}
}
}
}
}
ElevatedCard(modifier = Modifier.weight(1f)) {
if (uiState.transmitters.isEmpty()) {
Box(contentAlignment = Alignment.Center, modifier = Modifier.fillMaxSize()) {
Column(
horizontalAlignment = Alignment.CenterHorizontally,
verticalArrangement = Arrangement.spacedBy(24.dp),
modifier = Modifier.padding(32.dp)
) {
Text(text = """¯\_(ツ)_/¯""", fontSize = 32.sp)
Text(
text = stringResource(R.string.empty_list_message),
fontSize = 21.sp,
textAlign = TextAlign.Center
)
Text(
text = stringResource(R.string.radar_no_data),
fontSize = 18.sp,
textAlign = TextAlign.Center
)
}
}
} else {
Box(modifier = Modifier.fillMaxSize(), contentAlignment = Alignment.Center) {
TransmittersList(
transmitters = uiState.transmitters,
selectedUuid = uiState.selectedTransmitterUuid,
onSelect = { uuid ->
if (uiState.selectedTransmitterUuid != null) {
uiState.sendAction(RadarAction.SelectTransmitter(uuid))
}
}
)
if (uiState.orbitalPos?.eclipsed == true) {
EclipsedIndicator()
}
}
}
}
if (isVertical) {
RadarCard(uiState, Modifier.weight(1f))
TransmittersCard(uiState, Modifier.weight(1f))
} else {
Row(horizontalArrangement = Arrangement.spacedBy(6.dp)) {
ElevatedCard(modifier = Modifier.weight(1f)) {
RadarCard(uiState, Modifier.weight(1f))
TransmittersCard(uiState, Modifier.weight(1f))
}
}
}
}
@Composable
private fun RadarCard(uiState: RadarState, modifier: Modifier = Modifier) {
ElevatedCard(modifier = modifier) {
Box(contentAlignment = Alignment.Center) {
if (uiState.orbitalPos == null) {
val position = uiState.orbitalPos
if (position == null) {
ElevatedCard(modifier = Modifier.fillMaxSize()) {
EmptyListCard(message = "")
}
}
uiState.orbitalPos?.let { position ->
} else {
RadarViewCompose(
item = position,
items = uiState.satTrack,
@@ -232,6 +152,14 @@ private fun RadarScreen(uiState: RadarState, navigateUp: () -> Unit) {
shouldUseCompass = uiState.shouldUseCompass,
modifier = Modifier.align(Alignment.Center)
)
PositionOverlay(position)
}
}
}
}
@Composable
private fun PositionOverlay(position: OrbitalPos) {
Column(
verticalArrangement = Arrangement.SpaceBetween,
modifier = Modifier
@@ -242,57 +170,62 @@ private fun RadarScreen(uiState: RadarState, navigateUp: () -> Unit) {
horizontalArrangement = Arrangement.SpaceBetween,
modifier = Modifier.fillMaxWidth()
) {
RadarTextTop(
position.azimuth,
stringResource(R.string.radar_az_text),
true
RadarLabel(
value = stringResource(R.string.radar_az_value, position.azimuth.toDegrees()),
label = stringResource(R.string.radar_az_text),
alignment = Alignment.Start,
labelFirst = false
)
RadarTextTop(
position.elevation,
stringResource(R.string.radar_el_text),
false
RadarLabel(
value = stringResource(R.string.radar_az_value, position.elevation.toDegrees()),
label = stringResource(R.string.radar_el_text),
alignment = Alignment.End,
labelFirst = false
)
}
Row(
horizontalArrangement = Arrangement.SpaceBetween,
modifier = Modifier.fillMaxWidth()
) {
RadarTextBottom(
position.altitude,
stringResource(R.string.radar_alt_text),
true
RadarLabel(
value = stringResource(R.string.radar_alt_value, position.altitude),
label = stringResource(R.string.radar_alt_text),
alignment = Alignment.Start,
labelFirst = true
)
RadarTextBottom(
position.distance,
stringResource(R.string.radar_dist_text),
false
RadarLabel(
value = stringResource(R.string.radar_alt_value, position.distance),
label = stringResource(R.string.radar_dist_text),
alignment = Alignment.End,
labelFirst = true
)
}
}
}
}
}
ElevatedCard(modifier = Modifier.weight(1f)) {
if (uiState.transmitters.isEmpty()) {
Box(contentAlignment = Alignment.Center, modifier = Modifier.fillMaxSize()) {
Column(
horizontalAlignment = Alignment.CenterHorizontally,
verticalArrangement = Arrangement.spacedBy(24.dp),
modifier = Modifier.padding(32.dp)
@Composable
private fun RadarLabel(
value: String,
label: String,
alignment: Alignment.Horizontal,
labelFirst: Boolean
) {
Text(text = """¯\_(ツ)_/¯""", fontSize = 32.sp)
Text(
text = stringResource(R.string.empty_list_message),
fontSize = 21.sp,
textAlign = TextAlign.Center
)
Text(
text = stringResource(R.string.radar_no_data),
fontSize = 18.sp,
textAlign = TextAlign.Center
)
Column(horizontalAlignment = alignment) {
if (labelFirst) {
Text(text = label, fontSize = 15.sp)
Text(text = value, fontSize = 18.sp)
} else {
Text(text = value, fontSize = 18.sp)
Text(text = label, fontSize = 15.sp)
}
}
}
@Composable
private fun TransmittersCard(uiState: RadarState, modifier: Modifier = Modifier) {
ElevatedCard(modifier = modifier) {
if (uiState.transmitters.isEmpty()) {
EmptyTransmittersContent()
} else {
Box(modifier = Modifier.fillMaxSize(), contentAlignment = Alignment.Center) {
TransmittersList(
@@ -311,28 +244,28 @@ private fun RadarScreen(uiState: RadarState, navigateUp: () -> Unit) {
}
}
}
}
}
}
@Composable
private fun RadarTextTop(value: Double, text: String, isLeft: Boolean) {
val alignment = if (isLeft) Alignment.Start else Alignment.End
val degValue = stringResource(R.string.radar_az_value, value.toDegrees())
Column(horizontalAlignment = alignment) {
Text(text = degValue, fontSize = 18.sp)
Text(text = text, fontSize = 15.sp)
private fun EmptyTransmittersContent() {
Box(contentAlignment = Alignment.Center, modifier = Modifier.fillMaxSize()) {
Column(
horizontalAlignment = Alignment.CenterHorizontally,
verticalArrangement = Arrangement.spacedBy(24.dp),
modifier = Modifier.padding(32.dp)
) {
Text(text = """¯\_(ツ)_/¯""", fontSize = 32.sp)
Text(
text = stringResource(R.string.empty_list_message),
fontSize = 21.sp,
textAlign = TextAlign.Center
)
Text(
text = stringResource(R.string.radar_no_data),
fontSize = 18.sp,
textAlign = TextAlign.Center
)
}
}
@Composable
private fun RadarTextBottom(value: Double, text: String, isLeft: Boolean) {
val alignment = if (isLeft) Alignment.Start else Alignment.End
val degValue = stringResource(R.string.radar_alt_value, value)
Column(horizontalAlignment = alignment) {
Text(text = text, fontSize = 15.sp)
Text(text = degValue, fontSize = 18.sp)
}
}
@Composable
@@ -342,12 +275,13 @@ private fun TransmittersList(
onSelect: (String) -> Unit
) {
LazyColumn(modifier = Modifier.fillMaxSize()) {
items(items = transmitters, key = { item -> item.uuid }) { radio ->
items(items = transmitters, key = { it.uuid }) { radio ->
TransmitterItem(
radio,
(selectedUuid != null),
(radio.uuid == selectedUuid)
) { onSelect(radio.uuid) }
radio = radio,
isClickable = selectedUuid != null,
isSelected = radio.uuid == selectedUuid,
onClick = { onSelect(radio.uuid) }
)
}
}
}
@@ -366,23 +300,20 @@ private fun TransmitterItemPreview() {
private fun EclipsedIndicator() {
val bgColor = MaterialTheme.colorScheme.primary
val bgShape = MaterialTheme.shapes.medium
val infiniteTransition = rememberInfiniteTransition()
val textAlpha = infiniteTransition.animateFloat(
val infiniteTransition = rememberInfiniteTransition(label = "eclipsed")
val textAlpha by infiniteTransition.animateFloat(
initialValue = 1.0f,
targetValue = 0.0f,
animationSpec = infiniteRepeatable(
animation = tween(
durationMillis = 1000,
delayMillis = 25,
easing = LinearEasing
),
animation = tween(durationMillis = 1000, delayMillis = 25, easing = LinearEasing),
repeatMode = RepeatMode.Reverse
)
),
label = "eclipsedAlpha"
)
Box(
modifier = Modifier
.fillMaxSize()
.alpha(textAlpha.value)
.alpha(textAlpha)
.border(width = 2.dp, color = bgColor, shape = bgShape),
contentAlignment = Alignment.Center
) {
@@ -447,45 +378,52 @@ private fun TransmitterItem(
}
@Composable
private fun FrequencyRow(radio: SatRadio, isDownlink: Boolean, modifier: Modifier = Modifier) {
Row(verticalAlignment = Alignment.CenterVertically, modifier = modifier.fillMaxWidth()) {
val rotateMod = Modifier.rotate(if (isDownlink) 90f else -90f)
val weightMod = Modifier.weight(1f)
val desc = if (isDownlink) stringResource(R.string.radar_downlink) else stringResource(R.string.radar_uplink)
private fun FrequencyRow(radio: SatRadio, isDownlink: Boolean) {
Row(verticalAlignment = Alignment.CenterVertically, modifier = Modifier.fillMaxWidth()) {
val desc = if (isDownlink) stringResource(R.string.radar_downlink)
else stringResource(R.string.radar_uplink)
Text(
text = if (isDownlink) "D:" else "U:",
textAlign = TextAlign.Center,
fontSize = 18.sp,
color = MaterialTheme.colorScheme.onSurface,
modifier = modifier
.size(width = 24.dp, height = 24.dp)
modifier = Modifier
.size(24.dp)
.semantics { contentDescription = desc }
)
FrequencyText(if (isDownlink) radio.downlinkLow else radio.uplinkLow, weightMod)
FrequencyText(
frequency = if (isDownlink) radio.downlinkLow else radio.uplinkLow,
modifier = Modifier.weight(1f)
)
Text(
text = "-",
textAlign = TextAlign.Center,
fontSize = 21.sp,
fontWeight = FontWeight.Bold,
color = MaterialTheme.colorScheme.primary,
modifier = modifier
color = MaterialTheme.colorScheme.primary
)
FrequencyText(
frequency = if (isDownlink) radio.downlinkHigh else radio.uplinkHigh,
modifier = Modifier.weight(1f)
)
FrequencyText(if (isDownlink) radio.downlinkHigh else radio.uplinkHigh, weightMod)
Icon(
painter = painterResource(id = R.drawable.ic_arrow),
tint = MaterialTheme.colorScheme.onSurface,
contentDescription = null,
modifier = rotateMod.size(width = 24.dp, height = 24.dp)
modifier = Modifier
.rotate(if (isDownlink) 90f else -90f)
.size(24.dp)
)
}
}
@Composable
private fun FrequencyText(frequency: Long?, modifier: Modifier = Modifier) {
val noLinkText = stringResource(R.string.radar_no_link)
val freqValue = frequency?.let { it / 1000000f }
val text = frequency?.let {
stringResource(id = R.string.radar_link_low, it / 1000000f)
} ?: stringResource(R.string.radar_no_link)
Text(
text = freqValue?.let { stringResource(id = R.string.radar_link_low, it) } ?: noLinkText,
text = text,
textAlign = TextAlign.Center,
fontSize = 21.sp,
fontWeight = FontWeight.Bold,
@@ -25,9 +25,11 @@ import androidx.compose.foundation.Canvas
import androidx.compose.foundation.layout.aspectRatio
import androidx.compose.material3.MaterialTheme
import androidx.compose.runtime.Composable
import androidx.compose.runtime.getValue
import androidx.compose.runtime.mutableFloatStateOf
import androidx.compose.runtime.mutableStateOf
import androidx.compose.runtime.remember
import androidx.compose.runtime.setValue
import androidx.compose.ui.Modifier
import androidx.compose.ui.geometry.Offset
import androidx.compose.ui.graphics.Color
@@ -53,6 +55,10 @@ import com.rtbishop.look4sat.core.domain.utility.toRadians
import kotlin.math.cos
import kotlin.math.sin
private const val CIRCLES = 3
private const val STROKE_WIDTH = 6f
private const val SWEEP_INCREMENT = 360f / 12f / 60f
@Composable
fun RadarViewCompose(
item: OrbitalPos,
@@ -62,149 +68,81 @@ fun RadarViewCompose(
shouldUseCompass: Boolean,
modifier: Modifier = Modifier
) {
// val context = LocalContext.current
// val view = LocalView.current
val radarColor = MaterialTheme.colorScheme.secondary
val trackColor = MaterialTheme.colorScheme.primary
val aimColor = MaterialTheme.colorScheme.error
val strokeWidth = 6f
val animTransition = rememberInfiniteTransition(label = "animScale")
val animSpec = infiniteRepeatable<Float>(tween(1000))
val animScale = animTransition.animateFloat(16f, 64f, animSpec, label = "animScale")
// val aimThreshold = 0.05f
val animScale by animTransition.animateFloat(
initialValue = 16f,
targetValue = 64f,
animationSpec = infiniteRepeatable(tween(1000)),
label = "animScale"
)
val measurer = rememberTextMeasurer()
val sweepDegrees = remember { mutableFloatStateOf(0f) }
val trackLastRadius = remember { mutableStateOf(0f) }
val trackPath = remember { mutableStateOf(Path()) }
val trackEffect = remember { mutableStateOf(PathEffect.cornerPathEffect(0f)) }
// val soundPool = remember { mutableStateOf<SoundPool?>(null) }
// val beepSoundId = remember { mutableIntStateOf(0) }
// val aimTargetDifference = remember { mutableFloatStateOf(0f) }
// LaunchedEffect(item.azimuth, item.elevation, azimElev.first, azimElev.second) {
// val aimAzimuthRadians = azimElev.first.toDouble().toRadians()
// val aimElevationRadians = abs(min(azimElev.second, 0f)).toDouble().toRadians()
// // radius of 0.5 makes the aimTargetDifference range 0.0 to 1.0
// val radius = 0.5
// val aimX = sph2CartX(aimAzimuthRadians, aimElevationRadians, radius)
// val aimY = sph2CartY(aimAzimuthRadians, aimElevationRadians, radius)
// val satX = sph2CartX(item.azimuth, item.elevation, radius)
// val satY = sph2CartY(item.azimuth, item.elevation, radius)
// aimTargetDifference.floatValue = sqrt((satX - aimX).pow(2) + (satY - aimY).pow(2))
// val minPlaybackRate = 0.5f
// val maxPlaybackRate = 2.0f
// val playbackRate =
// maxPlaybackRate - (aimTargetDifference.floatValue / (maxPlaybackRate - minPlaybackRate))
// soundPool.value?.setRate(beepSoundId.intValue, playbackRate)
// }
//
// LaunchedEffect(aimTargetDifference.floatValue < aimThreshold) {
// if (aimTargetDifference.floatValue < aimThreshold) {
// view.performHapticFeedback(HapticFeedbackConstantsCompat.VIRTUAL_KEY)
// soundPool.value?.pause(beepSoundId.intValue)
// } else {
// soundPool.value?.resume(beepSoundId.intValue)
// }
// }
//
// LaunchedEffect(item.elevation > 0) {
// if(item.elevation > 0) {
// soundPool.value?.resume(beepSoundId.intValue)
// } else {
// soundPool.value?.pause(beepSoundId.intValue)
// }
// }
//
// DisposableEffect(Unit) {
// val audioAttributes = AudioAttributes.Builder()
// .setUsage(AudioAttributes.USAGE_ASSISTANCE_SONIFICATION)
// .setContentType(AudioAttributes.CONTENT_TYPE_SONIFICATION)
// .build()
// soundPool.value = SoundPool.Builder()
// .setMaxStreams(1)
// .setAudioAttributes(audioAttributes)
// .build()
// beepSoundId.intValue = soundPool.value?.load(context, R.raw.beep, 1) ?: 0
// soundPool.value?.setOnLoadCompleteListener { soundPool, _, status ->
// if (status == 0) {
// soundPool.play(beepSoundId.intValue, 0.5f, 0.5f, 0, -1, 1f)
// }
// }
// onDispose {
// soundPool.value?.release()
// }
// }
var sweepDegrees by remember { mutableFloatStateOf(0f) }
var cachedRadius by remember { mutableFloatStateOf(0f) }
var trackPath by remember { mutableStateOf(Path()) }
var trackEffect by remember { mutableStateOf(PathEffect.cornerPathEffect(0f)) }
Canvas(modifier = modifier.aspectRatio(1f)) {
val radius = (size.minDimension / 2f) * 0.95f
if(radius != trackLastRadius.value) {
trackPath.value = createTrackPath(items, radius)
trackEffect.value = createTrackEffect(trackPath.value)
trackLastRadius.value = radius
val radius = size.minDimension / 2f * 0.95f
if (radius != cachedRadius) {
trackPath = createTrackPath(items, radius)
trackEffect = createTrackEffect(trackPath)
cachedRadius = radius
}
rotate(if (shouldUseCompass) -azimElev.first else 0f) {
if (shouldShowSweep) {
drawSweep(center, sweepDegrees.floatValue, radius, trackColor)
}
drawRadar(radius, radarColor, strokeWidth, 3)
drawInfo(radius, trackColor, measurer, 3)
if (shouldShowSweep) drawSweep(center, sweepDegrees, radius, trackColor)
drawRadar(radius, radarColor)
drawElevationLabels(radius, trackColor, measurer)
translate(center.x, center.y) {
drawTrack(trackPath.value, trackEffect.value, aimColor, trackColor)
if (item.elevation > 0) {
drawPosition(item, radius, animScale.value, trackColor)
drawTrack(trackPath, trackEffect, aimColor, trackColor)
if (item.elevation > 0) drawPosition(item, radius, animScale, trackColor)
if (shouldUseCompass) drawAim(azimElev.first, azimElev.second, radius, aimColor)
}
if (shouldUseCompass) {
drawAim(azimElev.first, azimElev.second, radius, strokeWidth, aimColor)
}
}
sweepDegrees.floatValue = (sweepDegrees.floatValue + 360 / 12.0f / 60) % 360
sweepDegrees = (sweepDegrees + SWEEP_INCREMENT) % 360f
}
}
}
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))
private fun DrawScope.drawRadar(radius: Float, color: Color) {
val step = radius / CIRCLES
for (i in 0 until CIRCLES) {
drawCircle(color, radius - step * i, style = Stroke(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)
drawLine(color, Offset(center.x - radius, center.y), Offset(center.x + radius, center.y), STROKE_WIDTH)
drawLine(color, Offset(center.x, center.y - radius), Offset(center.x, center.y + radius), STROKE_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.drawElevationLabels(radius: Float, color: Color, measurer: TextMeasurer) {
val step = radius / CIRCLES
val degStep = 90 / CIRCLES
val style = TextStyle(color, 15.sp)
for (i in 0 until CIRCLES) {
val textY = (radius - step * i) - 32f
drawText(measurer, " ${degStep * (CIRCLES - i)}°", Offset(center.x, textY), style = style)
}
}
private fun DrawScope.drawTrack(path: Path, effect: PathEffect, color: Color, effectColor: Color) {
drawPath(path, color, style = Stroke(6f))
drawPath(path, color, style = Stroke(STROKE_WIDTH))
drawPath(path, effectColor, style = Stroke(pathEffect = effect))
}
private fun DrawScope.drawPosition(item: OrbitalPos, radius: Float, posRadius: Float, color: Color) {
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))
drawCircle(color.copy(alpha = 1 - (posRadius / 64f)), posRadius, center.copy(satX, -satY))
val pos = sph2Cart(item.azimuth, item.elevation, radius.toDouble())
drawCircle(color, 16f, pos)
drawCircle(color.copy(alpha = 1 - (posRadius / 64f)), posRadius, pos)
}
private fun DrawScope.drawAim(azim: Float, elev: Float, radius: Float, width: Float, color: Color) {
private fun DrawScope.drawAim(azim: Float, elev: Float, radius: 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) {
println(exception)
}
val azimRad = azim.toDouble().toRadians()
val elevRad = elev.toDouble().toRadians().coerceAtMost(0.0)
val pos = sph2Cart(azimRad, -elevRad, radius.toDouble())
drawLine(color, Offset(pos.x - size, pos.y), Offset(pos.x + size, pos.y), STROKE_WIDTH)
drawLine(color, Offset(pos.x, pos.y - size), Offset(pos.x, pos.y + size), STROKE_WIDTH)
drawCircle(color, size / 2, pos, style = Stroke(STROKE_WIDTH))
}
private fun DrawScope.drawSweep(center: Offset, degrees: Float, radius: Float, color: Color) {
@@ -216,37 +154,34 @@ private fun DrawScope.drawSweep(center: Offset, degrees: Float, radius: Float, c
private fun createTrackPath(positions: List<OrbitalPos>, 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)
positions.forEachIndexed { index, pos ->
val offset = sph2Cart(pos.azimuth, pos.elevation, radius.toDouble())
if (index == 0) trackPath.moveTo(offset.x, offset.y) else trackPath.lineTo(offset.x, offset.y)
}
return trackPath
}
private fun createTrackEffect(trackPath: Path): PathEffect {
val shape = Path()
val shapeRadius = 24f
val angle = 120.0.toRadians()
shape.moveTo((shapeRadius * cos(angle)).toFloat(), (shapeRadius * sin(angle)).toFloat())
val shape = Path().apply {
moveTo((shapeRadius * cos(angle)).toFloat(), (shapeRadius * sin(angle)).toFloat())
for (i in 1 until 3) {
val x = (shapeRadius * cos(angle - angle * i)).toFloat()
val y = (shapeRadius * sin(angle - angle * i)).toFloat()
shape.lineTo(x, y)
lineTo(
x = (shapeRadius * cos(angle - angle * i)).toFloat(),
y = (shapeRadius * sin(angle - angle * i)).toFloat()
)
}
close()
}
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)
return PathEffect.stampedPathEffect(shape, trackLength / 2f, trackLength / 4f, StampedPathEffectStyle.Rotate)
}
private fun sph2CartX(azim: Double, elev: Double, r: Double): Float {
private fun sph2Cart(azim: Double, elev: Double, r: Double): Offset {
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()
return Offset(
x = (radius * cos(PI_2 - azim)).toFloat(),
y = -(radius * sin(PI_2 - azim)).toFloat()
)
}