mirror of
https://github.com/rt-bishop/Look4Sat.git
synced 2026-10-02 03:15:37 +00:00
Simplified RadarScreen/View, added sensors smoothing
This commit is contained in:
1 parent
8c1df334d8
commit
279594c425
3 files changed
+311
-407
No files matched your search
@@ -35,78 +35,109 @@ class SensorsRepo(
|
|||||||
private val sensorManager: SensorManager,
|
private val sensorManager: SensorManager,
|
||||||
private val sensor: Sensor?,
|
private val sensor: Sensor?,
|
||||||
private val windowManager: WindowManager
|
private val windowManager: WindowManager
|
||||||
) :
|
) : SensorEventListener, ISensorsRepo {
|
||||||
SensorEventListener, ISensorsRepo {
|
|
||||||
|
|
||||||
private val _orientation = MutableStateFlow(Pair(0f, 0f))
|
private val _orientation = MutableStateFlow(Pair(0f, 0f))
|
||||||
private val rotationMatrix = FloatArray(9)
|
private val rotationMatrix = FloatArray(9)
|
||||||
|
private val tempMatrix = FloatArray(9)
|
||||||
private val orientationValues = FloatArray(3)
|
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 val orientation: StateFlow<Pair<Float, Float>> = _orientation
|
||||||
|
|
||||||
override fun getMagDeclination(geoPos: GeoPos, time: Long): Float {
|
override fun getMagDeclination(geoPos: GeoPos, time: Long): Float {
|
||||||
val latitude = geoPos.latitude.toFloat()
|
return GeomagneticField(
|
||||||
val longitude = geoPos.longitude.toFloat()
|
geoPos.latitude.toFloat(),
|
||||||
return GeomagneticField(latitude, longitude, geoPos.altitude.toFloat(), time).declination
|
geoPos.longitude.toFloat(),
|
||||||
|
geoPos.altitude.toFloat(),
|
||||||
|
time
|
||||||
|
).declination
|
||||||
}
|
}
|
||||||
|
|
||||||
override fun enableSensor() {
|
override fun enableSensor() {
|
||||||
|
hasInitialReading = false
|
||||||
sensor?.let { sensorManager.registerListener(this, it, 8000) }
|
sensor?.let { sensorManager.registerListener(this, it, 8000) }
|
||||||
}
|
}
|
||||||
|
|
||||||
override fun disableSensor() = sensorManager.unregisterListener(this)
|
override fun disableSensor() = sensorManager.unregisterListener(this)
|
||||||
|
|
||||||
override fun onAccuracyChanged(sensor: Sensor, accuracy: Int) {
|
override fun onAccuracyChanged(sensor: Sensor, accuracy: Int) = Unit
|
||||||
sensorAccuracy = accuracy
|
|
||||||
}
|
|
||||||
|
|
||||||
override fun onSensorChanged(event: SensorEvent) = when {
|
override fun onSensorChanged(event: SensorEvent) {
|
||||||
sensorAccuracy == SensorManager.SENSOR_STATUS_UNRELIABLE -> {}
|
if (event.sensor == sensor) updateOrientation(event.values)
|
||||||
event.sensor == sensor -> updateOrientation(event.values)
|
|
||||||
else -> {}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
private fun getDisplayRotation(): Int {
|
private fun getDisplayRotation(): Int {
|
||||||
return try {
|
return try {
|
||||||
@Suppress("DEPRECATION")
|
@Suppress("DEPRECATION")
|
||||||
windowManager.defaultDisplay.rotation
|
windowManager.defaultDisplay.rotation
|
||||||
} catch (e: Exception) {
|
} catch (_: Exception) {
|
||||||
Surface.ROTATION_0
|
Surface.ROTATION_0
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
private fun transformRotationMatrix(rotationMatrix: FloatArray, rotation: Int) {
|
private fun remapForRotation(rotation: Int) {
|
||||||
val tempMatrix = FloatArray(9)
|
val remapped = when (rotation) {
|
||||||
when (rotation) {
|
Surface.ROTATION_90 -> SensorManager.remapCoordinateSystem(
|
||||||
Surface.ROTATION_0 -> {
|
rotationMatrix, SensorManager.AXIS_Y, SensorManager.AXIS_MINUS_X, tempMatrix
|
||||||
SensorManager.remapCoordinateSystem(rotationMatrix, SensorManager.AXIS_X, SensorManager.AXIS_Y, tempMatrix)
|
)
|
||||||
System.arraycopy(tempMatrix, 0, rotationMatrix, 0, 9)
|
|
||||||
}
|
Surface.ROTATION_180 -> SensorManager.remapCoordinateSystem(
|
||||||
Surface.ROTATION_90 -> {
|
rotationMatrix, SensorManager.AXIS_MINUS_X, SensorManager.AXIS_MINUS_Y, tempMatrix
|
||||||
SensorManager.remapCoordinateSystem(rotationMatrix, SensorManager.AXIS_Y, SensorManager.AXIS_MINUS_X, tempMatrix)
|
)
|
||||||
SensorManager.remapCoordinateSystem(tempMatrix, SensorManager.AXIS_MINUS_X, SensorManager.AXIS_Y, rotationMatrix)
|
|
||||||
}
|
Surface.ROTATION_270 -> SensorManager.remapCoordinateSystem(
|
||||||
Surface.ROTATION_180 -> {
|
rotationMatrix, SensorManager.AXIS_MINUS_Y, SensorManager.AXIS_X, tempMatrix
|
||||||
SensorManager.remapCoordinateSystem(rotationMatrix, SensorManager.AXIS_MINUS_X, SensorManager.AXIS_MINUS_Y, tempMatrix)
|
)
|
||||||
System.arraycopy(tempMatrix, 0, rotationMatrix, 0, 9)
|
|
||||||
}
|
else -> false
|
||||||
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)
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
if (remapped) System.arraycopy(tempMatrix, 0, rotationMatrix, 0, 9)
|
||||||
}
|
}
|
||||||
|
|
||||||
private fun updateOrientation(rotationVector: FloatArray) {
|
private fun updateOrientation(rotationVector: FloatArray) {
|
||||||
SensorManager.getRotationMatrixFromVector(rotationMatrix, rotationVector)
|
SensorManager.getRotationMatrixFromVector(rotationMatrix, rotationVector)
|
||||||
transformRotationMatrix(rotationMatrix, getDisplayRotation())
|
remapForRotation(getDisplayRotation())
|
||||||
SensorManager.getOrientation(rotationMatrix, orientationValues)
|
SensorManager.getOrientation(rotationMatrix, orientationValues)
|
||||||
val azimuth = (orientationValues[0] * RAD2DEG).toFloat()
|
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 magneticAzimuth = (azimuth + 360f) % 360f
|
||||||
val roundedAzimuth = round(magneticAzimuth * 10) / 10
|
|
||||||
val roundedPitch = round(pitch * 10) / 10
|
if (!hasInitialReading) {
|
||||||
_orientation.value = Pair(roundedAzimuth, roundedPitch)
|
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.Surface
|
||||||
import androidx.compose.material3.Text
|
import androidx.compose.material3.Text
|
||||||
import androidx.compose.runtime.Composable
|
import androidx.compose.runtime.Composable
|
||||||
|
import androidx.compose.runtime.getValue
|
||||||
import androidx.compose.ui.Alignment
|
import androidx.compose.ui.Alignment
|
||||||
import androidx.compose.ui.Modifier
|
import androidx.compose.ui.Modifier
|
||||||
import androidx.compose.ui.draw.alpha
|
import androidx.compose.ui.draw.alpha
|
||||||
@@ -62,6 +63,7 @@ import androidx.navigation.NavGraphBuilder
|
|||||||
import androidx.navigation.compose.composable
|
import androidx.navigation.compose.composable
|
||||||
import androidx.navigation.navArgument
|
import androidx.navigation.navArgument
|
||||||
import com.rtbishop.look4sat.core.domain.model.SatRadio
|
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.domain.utility.toDegrees
|
||||||
import com.rtbishop.look4sat.core.presentation.EmptyListCard
|
import com.rtbishop.look4sat.core.presentation.EmptyListCard
|
||||||
import com.rtbishop.look4sat.core.presentation.IconCard
|
import com.rtbishop.look4sat.core.presentation.IconCard
|
||||||
@@ -84,14 +86,13 @@ fun NavGraphBuilder.radarDestination(navigateUp: () -> Unit) {
|
|||||||
)
|
)
|
||||||
composable(radarRoute, radarArgs) {
|
composable(radarRoute, radarArgs) {
|
||||||
val viewModel = viewModel(RadarViewModel::class.java, factory = RadarViewModel.Factory)
|
val viewModel = viewModel(RadarViewModel::class.java, factory = RadarViewModel.Factory)
|
||||||
val uiState = viewModel.uiState.collectAsStateWithLifecycle().value
|
val uiState by viewModel.uiState.collectAsStateWithLifecycle()
|
||||||
RadarScreen(uiState, navigateUp)
|
RadarScreen(uiState, navigateUp)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@Composable
|
@Composable
|
||||||
private fun RadarScreen(uiState: RadarState, navigateUp: () -> Unit) {
|
private fun RadarScreen(uiState: RadarState, navigateUp: () -> Unit) {
|
||||||
// BluetoothCIV.init(LocalContext.current)
|
|
||||||
val addToCalendar: () -> Unit = {
|
val addToCalendar: () -> Unit = {
|
||||||
uiState.currentPass?.let { pass ->
|
uiState.currentPass?.let { pass ->
|
||||||
uiState.sendAction(RadarAction.AddToCalendar(pass.name, pass.aosTime, pass.losTime))
|
uiState.sendAction(RadarAction.AddToCalendar(pass.name, pass.aosTime, pass.losTime))
|
||||||
@@ -102,9 +103,11 @@ private fun RadarScreen(uiState: RadarState, navigateUp: () -> Unit) {
|
|||||||
Column(
|
Column(
|
||||||
modifier = Modifier
|
modifier = Modifier
|
||||||
.layoutPadding()
|
.layoutPadding()
|
||||||
.keepScreenOn(), verticalArrangement = Arrangement.spacedBy(6.dp)
|
.keepScreenOn(),
|
||||||
|
verticalArrangement = Arrangement.spacedBy(6.dp)
|
||||||
) {
|
) {
|
||||||
if (isVerticalLayout()) {
|
val isVertical = isVerticalLayout()
|
||||||
|
if (isVertical) {
|
||||||
TopBar {
|
TopBar {
|
||||||
IconCard(action = navigateUp, resId = R.drawable.ic_back)
|
IconCard(action = navigateUp, resId = R.drawable.ic_back)
|
||||||
TimerRow(timeString = uiState.currentTime, isTimeAos = uiState.isCurrentTimeAos)
|
TimerRow(timeString = uiState.currentTime, isTimeAos = uiState.isCurrentTimeAos)
|
||||||
@@ -119,196 +122,123 @@ private fun RadarScreen(uiState: RadarState, navigateUp: () -> Unit) {
|
|||||||
IconCard(action = addToCalendar, resId = R.drawable.ic_calendar)
|
IconCard(action = addToCalendar, resId = R.drawable.ic_calendar)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
if(isVerticalLayout()) {
|
if (isVertical) {
|
||||||
ElevatedCard(modifier = Modifier.weight(1f)) {
|
RadarCard(uiState, Modifier.weight(1f))
|
||||||
Box(contentAlignment = Alignment.Center) {
|
TransmittersCard(uiState, Modifier.weight(1f))
|
||||||
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()
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
} else {
|
} else {
|
||||||
Row(horizontalArrangement = Arrangement.spacedBy(6.dp)) {
|
Row(horizontalArrangement = Arrangement.spacedBy(6.dp)) {
|
||||||
ElevatedCard(modifier = Modifier.weight(1f)) {
|
RadarCard(uiState, Modifier.weight(1f))
|
||||||
Box(contentAlignment = Alignment.Center) {
|
TransmittersCard(uiState, Modifier.weight(1f))
|
||||||
if (uiState.orbitalPos == null) {
|
}
|
||||||
ElevatedCard(modifier = Modifier.fillMaxSize()) {
|
}
|
||||||
EmptyListCard(message = "")
|
}
|
||||||
}
|
}
|
||||||
}
|
|
||||||
uiState.orbitalPos?.let { position ->
|
@Composable
|
||||||
RadarViewCompose(
|
private fun RadarCard(uiState: RadarState, modifier: Modifier = Modifier) {
|
||||||
item = position,
|
ElevatedCard(modifier = modifier) {
|
||||||
items = uiState.satTrack,
|
Box(contentAlignment = Alignment.Center) {
|
||||||
azimElev = uiState.orientationValues,
|
val position = uiState.orbitalPos
|
||||||
shouldShowSweep = uiState.shouldShowSweep,
|
if (position == null) {
|
||||||
shouldUseCompass = uiState.shouldUseCompass,
|
ElevatedCard(modifier = Modifier.fillMaxSize()) {
|
||||||
modifier = Modifier.align(Alignment.Center)
|
EmptyListCard(message = "")
|
||||||
)
|
|
||||||
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)) {
|
} else {
|
||||||
if (uiState.transmitters.isEmpty()) {
|
RadarViewCompose(
|
||||||
Box(contentAlignment = Alignment.Center, modifier = Modifier.fillMaxSize()) {
|
item = position,
|
||||||
Column(
|
items = uiState.satTrack,
|
||||||
horizontalAlignment = Alignment.CenterHorizontally,
|
azimElev = uiState.orientationValues,
|
||||||
verticalArrangement = Arrangement.spacedBy(24.dp),
|
shouldShowSweep = uiState.shouldShowSweep,
|
||||||
modifier = Modifier.padding(32.dp)
|
shouldUseCompass = uiState.shouldUseCompass,
|
||||||
) {
|
modifier = Modifier.align(Alignment.Center)
|
||||||
Text(text = """¯\_(ツ)_/¯""", fontSize = 32.sp)
|
)
|
||||||
Text(
|
PositionOverlay(position)
|
||||||
text = stringResource(R.string.empty_list_message),
|
}
|
||||||
fontSize = 21.sp,
|
}
|
||||||
textAlign = TextAlign.Center
|
}
|
||||||
)
|
}
|
||||||
Text(
|
|
||||||
text = stringResource(R.string.radar_no_data),
|
@Composable
|
||||||
fontSize = 18.sp,
|
private fun PositionOverlay(position: OrbitalPos) {
|
||||||
textAlign = TextAlign.Center
|
Column(
|
||||||
)
|
verticalArrangement = Arrangement.SpaceBetween,
|
||||||
}
|
modifier = Modifier
|
||||||
}
|
.fillMaxSize()
|
||||||
} else {
|
.padding(horizontal = 6.dp, vertical = 4.dp)
|
||||||
Box(modifier = Modifier.fillMaxSize(), contentAlignment = Alignment.Center) {
|
) {
|
||||||
TransmittersList(
|
Row(
|
||||||
transmitters = uiState.transmitters,
|
horizontalArrangement = Arrangement.SpaceBetween,
|
||||||
selectedUuid = uiState.selectedTransmitterUuid,
|
modifier = Modifier.fillMaxWidth()
|
||||||
onSelect = { uuid ->
|
) {
|
||||||
if (uiState.selectedTransmitterUuid != null) {
|
RadarLabel(
|
||||||
uiState.sendAction(RadarAction.SelectTransmitter(uuid))
|
value = stringResource(R.string.radar_az_value, position.azimuth.toDegrees()),
|
||||||
}
|
label = stringResource(R.string.radar_az_text),
|
||||||
}
|
alignment = Alignment.Start,
|
||||||
)
|
labelFirst = false
|
||||||
if (uiState.orbitalPos?.eclipsed == true) {
|
)
|
||||||
EclipsedIndicator()
|
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()
|
||||||
|
) {
|
||||||
|
RadarLabel(
|
||||||
|
value = stringResource(R.string.radar_alt_value, position.altitude),
|
||||||
|
label = stringResource(R.string.radar_alt_text),
|
||||||
|
alignment = Alignment.Start,
|
||||||
|
labelFirst = true
|
||||||
|
)
|
||||||
|
RadarLabel(
|
||||||
|
value = stringResource(R.string.radar_alt_value, position.distance),
|
||||||
|
label = stringResource(R.string.radar_dist_text),
|
||||||
|
alignment = Alignment.End,
|
||||||
|
labelFirst = true
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@Composable
|
||||||
|
private fun RadarLabel(
|
||||||
|
value: String,
|
||||||
|
label: String,
|
||||||
|
alignment: Alignment.Horizontal,
|
||||||
|
labelFirst: Boolean
|
||||||
|
) {
|
||||||
|
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(
|
||||||
|
transmitters = uiState.transmitters,
|
||||||
|
selectedUuid = uiState.selectedTransmitterUuid,
|
||||||
|
onSelect = { uuid ->
|
||||||
|
if (uiState.selectedTransmitterUuid != null) {
|
||||||
|
uiState.sendAction(RadarAction.SelectTransmitter(uuid))
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
)
|
||||||
|
if (uiState.orbitalPos?.eclipsed == true) {
|
||||||
|
EclipsedIndicator()
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -316,22 +246,25 @@ private fun RadarScreen(uiState: RadarState, navigateUp: () -> Unit) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
@Composable
|
@Composable
|
||||||
private fun RadarTextTop(value: Double, text: String, isLeft: Boolean) {
|
private fun EmptyTransmittersContent() {
|
||||||
val alignment = if (isLeft) Alignment.Start else Alignment.End
|
Box(contentAlignment = Alignment.Center, modifier = Modifier.fillMaxSize()) {
|
||||||
val degValue = stringResource(R.string.radar_az_value, value.toDegrees())
|
Column(
|
||||||
Column(horizontalAlignment = alignment) {
|
horizontalAlignment = Alignment.CenterHorizontally,
|
||||||
Text(text = degValue, fontSize = 18.sp)
|
verticalArrangement = Arrangement.spacedBy(24.dp),
|
||||||
Text(text = text, fontSize = 15.sp)
|
modifier = Modifier.padding(32.dp)
|
||||||
}
|
) {
|
||||||
}
|
Text(text = """¯\_(ツ)_/¯""", fontSize = 32.sp)
|
||||||
|
Text(
|
||||||
@Composable
|
text = stringResource(R.string.empty_list_message),
|
||||||
private fun RadarTextBottom(value: Double, text: String, isLeft: Boolean) {
|
fontSize = 21.sp,
|
||||||
val alignment = if (isLeft) Alignment.Start else Alignment.End
|
textAlign = TextAlign.Center
|
||||||
val degValue = stringResource(R.string.radar_alt_value, value)
|
)
|
||||||
Column(horizontalAlignment = alignment) {
|
Text(
|
||||||
Text(text = text, fontSize = 15.sp)
|
text = stringResource(R.string.radar_no_data),
|
||||||
Text(text = degValue, fontSize = 18.sp)
|
fontSize = 18.sp,
|
||||||
|
textAlign = TextAlign.Center
|
||||||
|
)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -342,12 +275,13 @@ private fun TransmittersList(
|
|||||||
onSelect: (String) -> Unit
|
onSelect: (String) -> Unit
|
||||||
) {
|
) {
|
||||||
LazyColumn(modifier = Modifier.fillMaxSize()) {
|
LazyColumn(modifier = Modifier.fillMaxSize()) {
|
||||||
items(items = transmitters, key = { item -> item.uuid }) { radio ->
|
items(items = transmitters, key = { it.uuid }) { radio ->
|
||||||
TransmitterItem(
|
TransmitterItem(
|
||||||
radio,
|
radio = radio,
|
||||||
(selectedUuid != null),
|
isClickable = selectedUuid != null,
|
||||||
(radio.uuid == selectedUuid)
|
isSelected = radio.uuid == selectedUuid,
|
||||||
) { onSelect(radio.uuid) }
|
onClick = { onSelect(radio.uuid) }
|
||||||
|
)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -366,23 +300,20 @@ private fun TransmitterItemPreview() {
|
|||||||
private fun EclipsedIndicator() {
|
private fun EclipsedIndicator() {
|
||||||
val bgColor = MaterialTheme.colorScheme.primary
|
val bgColor = MaterialTheme.colorScheme.primary
|
||||||
val bgShape = MaterialTheme.shapes.medium
|
val bgShape = MaterialTheme.shapes.medium
|
||||||
val infiniteTransition = rememberInfiniteTransition()
|
val infiniteTransition = rememberInfiniteTransition(label = "eclipsed")
|
||||||
val textAlpha = infiniteTransition.animateFloat(
|
val textAlpha by infiniteTransition.animateFloat(
|
||||||
initialValue = 1.0f,
|
initialValue = 1.0f,
|
||||||
targetValue = 0.0f,
|
targetValue = 0.0f,
|
||||||
animationSpec = infiniteRepeatable(
|
animationSpec = infiniteRepeatable(
|
||||||
animation = tween(
|
animation = tween(durationMillis = 1000, delayMillis = 25, easing = LinearEasing),
|
||||||
durationMillis = 1000,
|
|
||||||
delayMillis = 25,
|
|
||||||
easing = LinearEasing
|
|
||||||
),
|
|
||||||
repeatMode = RepeatMode.Reverse
|
repeatMode = RepeatMode.Reverse
|
||||||
)
|
),
|
||||||
|
label = "eclipsedAlpha"
|
||||||
)
|
)
|
||||||
Box(
|
Box(
|
||||||
modifier = Modifier
|
modifier = Modifier
|
||||||
.fillMaxSize()
|
.fillMaxSize()
|
||||||
.alpha(textAlpha.value)
|
.alpha(textAlpha)
|
||||||
.border(width = 2.dp, color = bgColor, shape = bgShape),
|
.border(width = 2.dp, color = bgColor, shape = bgShape),
|
||||||
contentAlignment = Alignment.Center
|
contentAlignment = Alignment.Center
|
||||||
) {
|
) {
|
||||||
@@ -447,45 +378,52 @@ private fun TransmitterItem(
|
|||||||
}
|
}
|
||||||
|
|
||||||
@Composable
|
@Composable
|
||||||
private fun FrequencyRow(radio: SatRadio, isDownlink: Boolean, modifier: Modifier = Modifier) {
|
private fun FrequencyRow(radio: SatRadio, isDownlink: Boolean) {
|
||||||
Row(verticalAlignment = Alignment.CenterVertically, modifier = modifier.fillMaxWidth()) {
|
Row(verticalAlignment = Alignment.CenterVertically, modifier = Modifier.fillMaxWidth()) {
|
||||||
val rotateMod = Modifier.rotate(if (isDownlink) 90f else -90f)
|
val desc = if (isDownlink) stringResource(R.string.radar_downlink)
|
||||||
val weightMod = Modifier.weight(1f)
|
else stringResource(R.string.radar_uplink)
|
||||||
val desc = if (isDownlink) stringResource(R.string.radar_downlink) else stringResource(R.string.radar_uplink)
|
|
||||||
Text(
|
Text(
|
||||||
text = if (isDownlink) "D:" else "U:",
|
text = if (isDownlink) "D:" else "U:",
|
||||||
textAlign = TextAlign.Center,
|
textAlign = TextAlign.Center,
|
||||||
fontSize = 18.sp,
|
fontSize = 18.sp,
|
||||||
color = MaterialTheme.colorScheme.onSurface,
|
color = MaterialTheme.colorScheme.onSurface,
|
||||||
modifier = modifier
|
modifier = Modifier
|
||||||
.size(width = 24.dp, height = 24.dp)
|
.size(24.dp)
|
||||||
.semantics { contentDescription = desc }
|
.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(
|
||||||
text = "-",
|
text = "-",
|
||||||
textAlign = TextAlign.Center,
|
textAlign = TextAlign.Center,
|
||||||
fontSize = 21.sp,
|
fontSize = 21.sp,
|
||||||
fontWeight = FontWeight.Bold,
|
fontWeight = FontWeight.Bold,
|
||||||
color = MaterialTheme.colorScheme.primary,
|
color = MaterialTheme.colorScheme.primary
|
||||||
modifier = modifier
|
)
|
||||||
|
FrequencyText(
|
||||||
|
frequency = if (isDownlink) radio.downlinkHigh else radio.uplinkHigh,
|
||||||
|
modifier = Modifier.weight(1f)
|
||||||
)
|
)
|
||||||
FrequencyText(if (isDownlink) radio.downlinkHigh else radio.uplinkHigh, weightMod)
|
|
||||||
Icon(
|
Icon(
|
||||||
painter = painterResource(id = R.drawable.ic_arrow),
|
painter = painterResource(id = R.drawable.ic_arrow),
|
||||||
tint = MaterialTheme.colorScheme.onSurface,
|
tint = MaterialTheme.colorScheme.onSurface,
|
||||||
contentDescription = null,
|
contentDescription = null,
|
||||||
modifier = rotateMod.size(width = 24.dp, height = 24.dp)
|
modifier = Modifier
|
||||||
|
.rotate(if (isDownlink) 90f else -90f)
|
||||||
|
.size(24.dp)
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@Composable
|
@Composable
|
||||||
private fun FrequencyText(frequency: Long?, modifier: Modifier = Modifier) {
|
private fun FrequencyText(frequency: Long?, modifier: Modifier = Modifier) {
|
||||||
val noLinkText = stringResource(R.string.radar_no_link)
|
val text = frequency?.let {
|
||||||
val freqValue = frequency?.let { it / 1000000f }
|
stringResource(id = R.string.radar_link_low, it / 1000000f)
|
||||||
|
} ?: stringResource(R.string.radar_no_link)
|
||||||
Text(
|
Text(
|
||||||
text = freqValue?.let { stringResource(id = R.string.radar_link_low, it) } ?: noLinkText,
|
text = text,
|
||||||
textAlign = TextAlign.Center,
|
textAlign = TextAlign.Center,
|
||||||
fontSize = 21.sp,
|
fontSize = 21.sp,
|
||||||
fontWeight = FontWeight.Bold,
|
fontWeight = FontWeight.Bold,
|
||||||
|
|||||||
@@ -25,9 +25,11 @@ import androidx.compose.foundation.Canvas
|
|||||||
import androidx.compose.foundation.layout.aspectRatio
|
import androidx.compose.foundation.layout.aspectRatio
|
||||||
import androidx.compose.material3.MaterialTheme
|
import androidx.compose.material3.MaterialTheme
|
||||||
import androidx.compose.runtime.Composable
|
import androidx.compose.runtime.Composable
|
||||||
|
import androidx.compose.runtime.getValue
|
||||||
import androidx.compose.runtime.mutableFloatStateOf
|
import androidx.compose.runtime.mutableFloatStateOf
|
||||||
import androidx.compose.runtime.mutableStateOf
|
import androidx.compose.runtime.mutableStateOf
|
||||||
import androidx.compose.runtime.remember
|
import androidx.compose.runtime.remember
|
||||||
|
import androidx.compose.runtime.setValue
|
||||||
import androidx.compose.ui.Modifier
|
import androidx.compose.ui.Modifier
|
||||||
import androidx.compose.ui.geometry.Offset
|
import androidx.compose.ui.geometry.Offset
|
||||||
import androidx.compose.ui.graphics.Color
|
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.cos
|
||||||
import kotlin.math.sin
|
import kotlin.math.sin
|
||||||
|
|
||||||
|
private const val CIRCLES = 3
|
||||||
|
private const val STROKE_WIDTH = 6f
|
||||||
|
private const val SWEEP_INCREMENT = 360f / 12f / 60f
|
||||||
|
|
||||||
@Composable
|
@Composable
|
||||||
fun RadarViewCompose(
|
fun RadarViewCompose(
|
||||||
item: OrbitalPos,
|
item: OrbitalPos,
|
||||||
@@ -62,149 +68,81 @@ fun RadarViewCompose(
|
|||||||
shouldUseCompass: Boolean,
|
shouldUseCompass: Boolean,
|
||||||
modifier: Modifier = Modifier
|
modifier: Modifier = Modifier
|
||||||
) {
|
) {
|
||||||
// val context = LocalContext.current
|
|
||||||
// val view = LocalView.current
|
|
||||||
val radarColor = MaterialTheme.colorScheme.secondary
|
val radarColor = MaterialTheme.colorScheme.secondary
|
||||||
val trackColor = MaterialTheme.colorScheme.primary
|
val trackColor = MaterialTheme.colorScheme.primary
|
||||||
val aimColor = MaterialTheme.colorScheme.error
|
val aimColor = MaterialTheme.colorScheme.error
|
||||||
val strokeWidth = 6f
|
|
||||||
val animTransition = rememberInfiniteTransition(label = "animScale")
|
val animTransition = rememberInfiniteTransition(label = "animScale")
|
||||||
val animSpec = infiniteRepeatable<Float>(tween(1000))
|
val animScale by animTransition.animateFloat(
|
||||||
val animScale = animTransition.animateFloat(16f, 64f, animSpec, label = "animScale")
|
initialValue = 16f,
|
||||||
// val aimThreshold = 0.05f
|
targetValue = 64f,
|
||||||
|
animationSpec = infiniteRepeatable(tween(1000)),
|
||||||
|
label = "animScale"
|
||||||
|
)
|
||||||
val measurer = rememberTextMeasurer()
|
val measurer = rememberTextMeasurer()
|
||||||
val sweepDegrees = remember { mutableFloatStateOf(0f) }
|
var sweepDegrees by remember { mutableFloatStateOf(0f) }
|
||||||
val trackLastRadius = remember { mutableStateOf(0f) }
|
var cachedRadius by remember { mutableFloatStateOf(0f) }
|
||||||
val trackPath = remember { mutableStateOf(Path()) }
|
var trackPath by remember { mutableStateOf(Path()) }
|
||||||
val trackEffect = remember { mutableStateOf(PathEffect.cornerPathEffect(0f)) }
|
var trackEffect by 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()
|
|
||||||
// }
|
|
||||||
// }
|
|
||||||
|
|
||||||
Canvas(modifier = modifier.aspectRatio(1f)) {
|
Canvas(modifier = modifier.aspectRatio(1f)) {
|
||||||
val radius = (size.minDimension / 2f) * 0.95f
|
val radius = size.minDimension / 2f * 0.95f
|
||||||
if(radius != trackLastRadius.value) {
|
if (radius != cachedRadius) {
|
||||||
trackPath.value = createTrackPath(items, radius)
|
trackPath = createTrackPath(items, radius)
|
||||||
trackEffect.value = createTrackEffect(trackPath.value)
|
trackEffect = createTrackEffect(trackPath)
|
||||||
trackLastRadius.value = radius
|
cachedRadius = radius
|
||||||
}
|
}
|
||||||
rotate(if (shouldUseCompass) -azimElev.first else 0f) {
|
rotate(if (shouldUseCompass) -azimElev.first else 0f) {
|
||||||
if (shouldShowSweep) {
|
if (shouldShowSweep) drawSweep(center, sweepDegrees, radius, trackColor)
|
||||||
drawSweep(center, sweepDegrees.floatValue, radius, trackColor)
|
drawRadar(radius, radarColor)
|
||||||
}
|
drawElevationLabels(radius, trackColor, measurer)
|
||||||
drawRadar(radius, radarColor, strokeWidth, 3)
|
|
||||||
drawInfo(radius, trackColor, measurer, 3)
|
|
||||||
translate(center.x, center.y) {
|
translate(center.x, center.y) {
|
||||||
drawTrack(trackPath.value, trackEffect.value, aimColor, trackColor)
|
drawTrack(trackPath, trackEffect, aimColor, trackColor)
|
||||||
if (item.elevation > 0) {
|
if (item.elevation > 0) drawPosition(item, radius, animScale, trackColor)
|
||||||
drawPosition(item, radius, animScale.value, 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) {
|
private fun DrawScope.drawRadar(radius: Float, color: Color) {
|
||||||
for (i in 0 until circles) {
|
val step = radius / CIRCLES
|
||||||
val circleRadius = radius - radius / circles.toFloat() * i.toFloat()
|
for (i in 0 until CIRCLES) {
|
||||||
drawCircle(color, circleRadius, style = Stroke(width))
|
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, Offset(center.x - radius, center.y), Offset(center.x + radius, center.y), STROKE_WIDTH)
|
||||||
drawLine(color, center.copy(y = center.y - radius), center.copy(y = center.y + radius), 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) {
|
private fun DrawScope.drawElevationLabels(radius: Float, color: Color, measurer: TextMeasurer) {
|
||||||
for (i in 0 until circles) {
|
val step = radius / CIRCLES
|
||||||
val textY = (radius - radius / circles * i) - 32f
|
val degStep = 90 / CIRCLES
|
||||||
val textDeg = " ${(90 / circles) * (circles - i)}°"
|
val style = TextStyle(color, 15.sp)
|
||||||
drawText(measurer, textDeg, center.copy(y = textY), 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) {
|
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))
|
drawPath(path, effectColor, style = Stroke(pathEffect = effect))
|
||||||
}
|
}
|
||||||
|
|
||||||
private fun DrawScope.drawPosition(item: OrbitalPos, radius: Float, posRadius: Float, color: Color) {
|
private fun DrawScope.drawPosition(item: OrbitalPos, radius: Float, posRadius: Float, color: Color) {
|
||||||
val satX = sph2CartX(item.azimuth, item.elevation, radius.toDouble())
|
val pos = sph2Cart(item.azimuth, item.elevation, radius.toDouble())
|
||||||
val satY = sph2CartY(item.azimuth, item.elevation, radius.toDouble())
|
drawCircle(color, 16f, pos)
|
||||||
drawCircle(color, 16f, center.copy(satX, -satY))
|
drawCircle(color.copy(alpha = 1 - (posRadius / 64f)), posRadius, pos)
|
||||||
drawCircle(color.copy(alpha = 1 - (posRadius / 64f)), posRadius, center.copy(satX, -satY))
|
|
||||||
}
|
}
|
||||||
|
|
||||||
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 size = 36f
|
||||||
val azimRadians = azim.toDouble().toRadians()
|
val azimRad = azim.toDouble().toRadians()
|
||||||
val tempElevRadians = elev.toDouble().toRadians()
|
val elevRad = elev.toDouble().toRadians().coerceAtMost(0.0)
|
||||||
val elevRadians = if (tempElevRadians > 0.0) 0.0 else tempElevRadians
|
val pos = sph2Cart(azimRad, -elevRad, radius.toDouble())
|
||||||
val aimX = sph2CartX(azimRadians, -elevRadians, radius.toDouble())
|
drawLine(color, Offset(pos.x - size, pos.y), Offset(pos.x + size, pos.y), STROKE_WIDTH)
|
||||||
val aimY = sph2CartY(azimRadians, -elevRadians, radius.toDouble())
|
drawLine(color, Offset(pos.x, pos.y - size), Offset(pos.x, pos.y + size), STROKE_WIDTH)
|
||||||
try {
|
drawCircle(color, size / 2, pos, style = Stroke(STROKE_WIDTH))
|
||||||
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)
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
private fun DrawScope.drawSweep(center: Offset, degrees: Float, radius: Float, color: Color) {
|
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 {
|
private fun createTrackPath(positions: List<OrbitalPos>, radius: Float): Path {
|
||||||
val trackPath = Path()
|
val trackPath = Path()
|
||||||
positions.forEachIndexed { index, satPos ->
|
positions.forEachIndexed { index, pos ->
|
||||||
val passX = sph2CartX(satPos.azimuth, satPos.elevation, radius.toDouble())
|
val offset = sph2Cart(pos.azimuth, pos.elevation, radius.toDouble())
|
||||||
val passY = sph2CartY(satPos.azimuth, satPos.elevation, radius.toDouble())
|
if (index == 0) trackPath.moveTo(offset.x, offset.y) else trackPath.lineTo(offset.x, offset.y)
|
||||||
if (index == 0) trackPath.moveTo(passX, -passY) else trackPath.lineTo(passX, -passY)
|
|
||||||
}
|
}
|
||||||
return trackPath
|
return trackPath
|
||||||
}
|
}
|
||||||
|
|
||||||
private fun createTrackEffect(trackPath: Path): PathEffect {
|
private fun createTrackEffect(trackPath: Path): PathEffect {
|
||||||
val shape = Path()
|
|
||||||
val shapeRadius = 24f
|
val shapeRadius = 24f
|
||||||
val angle = 120.0.toRadians()
|
val angle = 120.0.toRadians()
|
||||||
shape.moveTo((shapeRadius * cos(angle)).toFloat(), (shapeRadius * sin(angle)).toFloat())
|
val shape = Path().apply {
|
||||||
for (i in 1 until 3) {
|
moveTo((shapeRadius * cos(angle)).toFloat(), (shapeRadius * sin(angle)).toFloat())
|
||||||
val x = (shapeRadius * cos(angle - angle * i)).toFloat()
|
for (i in 1 until 3) {
|
||||||
val y = (shapeRadius * sin(angle - angle * i)).toFloat()
|
lineTo(
|
||||||
shape.lineTo(x, y)
|
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 trackLength = PathMeasure().apply { setPath(trackPath, false) }.length
|
||||||
val advance = trackLength / 2f
|
return PathEffect.stampedPathEffect(shape, trackLength / 2f, trackLength / 4f, StampedPathEffectStyle.Rotate)
|
||||||
val phase = trackLength / 4f
|
|
||||||
return PathEffect.stampedPathEffect(shape, advance, phase, 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
|
val radius = r * (PI_2 - elev) / PI_2
|
||||||
return (radius * cos(PI_2 - azim)).toFloat()
|
return Offset(
|
||||||
}
|
x = (radius * cos(PI_2 - azim)).toFloat(),
|
||||||
|
y = -(radius * sin(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()
|
|
||||||
}
|
}
|
||||||
Reference in new issue
Block a user