mirror of
https://github.com/atsunatsu/Look4Sat.git
synced 2026-10-02 03:15:37 +00:00
feat: bidirectional drag sync between elevation curve and radar track
- ElevationCurveChart is now controlled (progress + onProgressChange) - MutualRadarView shows shared time-cursor positions for both stations and is draggable/tappable to move the cursor - MutualPassCard owns a single dragProgress feeding both charts, so dragging either chart moves the other in sync
This commit is contained in:
1 parent
4ca33c37ad
commit
28fa373db0
3 files changed
+51
-11
No files matched your search
+9
-10
@@ -30,10 +30,7 @@ import androidx.compose.foundation.layout.padding
|
|||||||
import androidx.compose.material3.MaterialTheme
|
import androidx.compose.material3.MaterialTheme
|
||||||
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.runtime.mutableFloatStateOf
|
|
||||||
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.toArgb
|
import androidx.compose.ui.graphics.toArgb
|
||||||
@@ -50,6 +47,8 @@ import kotlin.math.roundToInt
|
|||||||
|
|
||||||
/**
|
/**
|
||||||
* Draggable dual-station elevation curve chart.
|
* Draggable dual-station elevation curve chart.
|
||||||
|
* Controlled component: [progress] (0..1) and [onProgressChange] are owned by the parent,
|
||||||
|
* so the same time cursor can be shared with the radar track view.
|
||||||
*/
|
*/
|
||||||
@Composable
|
@Composable
|
||||||
fun ElevationCurveChart(
|
fun ElevationCurveChart(
|
||||||
@@ -57,6 +56,8 @@ fun ElevationCurveChart(
|
|||||||
startTime: Long,
|
startTime: Long,
|
||||||
endTime: Long,
|
endTime: Long,
|
||||||
maxElev: Double,
|
maxElev: Double,
|
||||||
|
progress: Float = 0.5f,
|
||||||
|
onProgressChange: (Float) -> Unit = {},
|
||||||
modifier: Modifier = Modifier
|
modifier: Modifier = Modifier
|
||||||
) {
|
) {
|
||||||
if (samples.isEmpty()) return
|
if (samples.isEmpty()) return
|
||||||
@@ -73,8 +74,6 @@ fun ElevationCurveChart(
|
|||||||
val gridColorArgb = gridColor.toArgb()
|
val gridColorArgb = gridColor.toArgb()
|
||||||
val timeFormat = remember { SimpleDateFormat("HH:mm", Locale.getDefault()) }
|
val timeFormat = remember { SimpleDateFormat("HH:mm", Locale.getDefault()) }
|
||||||
|
|
||||||
var dragProgress by remember { mutableFloatStateOf(0.5f) }
|
|
||||||
|
|
||||||
Column(modifier = modifier.fillMaxWidth()) {
|
Column(modifier = modifier.fillMaxWidth()) {
|
||||||
Canvas(
|
Canvas(
|
||||||
modifier = Modifier
|
modifier = Modifier
|
||||||
@@ -83,12 +82,12 @@ fun ElevationCurveChart(
|
|||||||
.background(MaterialTheme.colorScheme.surfaceVariant.copy(alpha = 0.3f))
|
.background(MaterialTheme.colorScheme.surfaceVariant.copy(alpha = 0.3f))
|
||||||
.pointerInput(Unit) {
|
.pointerInput(Unit) {
|
||||||
detectTapGestures { offset ->
|
detectTapGestures { offset ->
|
||||||
dragProgress = (offset.x / size.width.toFloat()).coerceIn(0f, 1f)
|
onProgressChange((offset.x / size.width.toFloat()).coerceIn(0f, 1f))
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
.pointerInput(Unit) {
|
.pointerInput(Unit) {
|
||||||
detectDragGestures { _, dragAmount ->
|
detectDragGestures { change, _ ->
|
||||||
dragProgress = (dragProgress + dragAmount.x / size.width.toFloat()).coerceIn(0f, 1f)
|
onProgressChange((change.position.x / size.width.toFloat()).coerceIn(0f, 1f))
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
) {
|
) {
|
||||||
@@ -180,8 +179,8 @@ fun ElevationCurveChart(
|
|||||||
drawPath(pathB, colorB, style = Stroke(width = 2.5f))
|
drawPath(pathB, colorB, style = Stroke(width = 2.5f))
|
||||||
|
|
||||||
// Drag indicator
|
// Drag indicator
|
||||||
val dragX = plotLeft + dragProgress * plotWidth
|
val dragX = plotLeft + progress * plotWidth
|
||||||
val dragTime = startTime + (dragProgress * (endTime - startTime)).toLong()
|
val dragTime = startTime + (progress * (endTime - startTime)).toLong()
|
||||||
|
|
||||||
drawLine(
|
drawLine(
|
||||||
onSurfaceColor,
|
onSurfaceColor,
|
||||||
|
|||||||
@@ -18,6 +18,8 @@
|
|||||||
package com.rtbishop.look4sat.feature.mutual
|
package com.rtbishop.look4sat.feature.mutual
|
||||||
|
|
||||||
import androidx.compose.foundation.Canvas
|
import androidx.compose.foundation.Canvas
|
||||||
|
import androidx.compose.foundation.gestures.detectDragGestures
|
||||||
|
import androidx.compose.foundation.gestures.detectTapGestures
|
||||||
import androidx.compose.foundation.layout.Arrangement
|
import androidx.compose.foundation.layout.Arrangement
|
||||||
import androidx.compose.foundation.layout.Column
|
import androidx.compose.foundation.layout.Column
|
||||||
import androidx.compose.foundation.layout.Row
|
import androidx.compose.foundation.layout.Row
|
||||||
@@ -34,6 +36,7 @@ import androidx.compose.ui.graphics.Path
|
|||||||
import androidx.compose.ui.graphics.PathEffect
|
import androidx.compose.ui.graphics.PathEffect
|
||||||
import androidx.compose.ui.graphics.drawscope.DrawScope
|
import androidx.compose.ui.graphics.drawscope.DrawScope
|
||||||
import androidx.compose.ui.graphics.drawscope.Stroke
|
import androidx.compose.ui.graphics.drawscope.Stroke
|
||||||
|
import androidx.compose.ui.input.pointer.pointerInput
|
||||||
import androidx.compose.ui.text.TextMeasurer
|
import androidx.compose.ui.text.TextMeasurer
|
||||||
import androidx.compose.ui.text.TextStyle
|
import androidx.compose.ui.text.TextStyle
|
||||||
import androidx.compose.ui.text.drawText
|
import androidx.compose.ui.text.drawText
|
||||||
@@ -50,10 +53,14 @@ private const val STROKE_WIDTH = 2.5f
|
|||||||
/**
|
/**
|
||||||
* Polar radar chart showing the satellite track as seen from both stations
|
* Polar radar chart showing the satellite track as seen from both stations
|
||||||
* on one plot. Station A is drawn solid, station B dashed.
|
* on one plot. Station A is drawn solid, station B dashed.
|
||||||
|
* Controlled: [progress] (0..1) and [onProgressChange] are owned by the parent
|
||||||
|
* so the time cursor can be shared with the elevation curve chart.
|
||||||
*/
|
*/
|
||||||
@Composable
|
@Composable
|
||||||
fun MutualRadarView(
|
fun MutualRadarView(
|
||||||
trackSamples: List<TrackSample>,
|
trackSamples: List<TrackSample>,
|
||||||
|
progress: Float = 0.5f,
|
||||||
|
onProgressChange: (Float) -> Unit = {},
|
||||||
labelA: String = "站点A",
|
labelA: String = "站点A",
|
||||||
labelB: String = "站点B",
|
labelB: String = "站点B",
|
||||||
modifier: Modifier = Modifier
|
modifier: Modifier = Modifier
|
||||||
@@ -66,12 +73,29 @@ fun MutualRadarView(
|
|||||||
val textColor = MaterialTheme.colorScheme.onSurfaceVariant
|
val textColor = MaterialTheme.colorScheme.onSurfaceVariant
|
||||||
val measurer = rememberTextMeasurer()
|
val measurer = rememberTextMeasurer()
|
||||||
|
|
||||||
|
// Current-time samples (both stations' position at the shared cursor)
|
||||||
|
val t0 = trackSamples.first().time
|
||||||
|
val t1 = trackSamples.last().time
|
||||||
|
val selectedTime = t0 + ((t1 - t0) * progress).toLong()
|
||||||
|
val visibleSamples = trackSamples.filter { it.time <= selectedTime }
|
||||||
|
val currentSample = visibleSamples.lastOrNull() ?: trackSamples.first()
|
||||||
|
|
||||||
Column(modifier = modifier.fillMaxWidth()) {
|
Column(modifier = modifier.fillMaxWidth()) {
|
||||||
Canvas(
|
Canvas(
|
||||||
modifier = Modifier
|
modifier = Modifier
|
||||||
.fillMaxWidth()
|
.fillMaxWidth()
|
||||||
.aspectRatio(1f)
|
.aspectRatio(1f)
|
||||||
.padding(4.dp)
|
.padding(4.dp)
|
||||||
|
.pointerInput(Unit) {
|
||||||
|
detectTapGestures { offset ->
|
||||||
|
onProgressChange((offset.x / size.width.toFloat()).coerceIn(0f, 1f))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
.pointerInput(Unit) {
|
||||||
|
detectDragGestures { change, _ ->
|
||||||
|
onProgressChange((change.position.x / size.width.toFloat()).coerceIn(0f, 1f))
|
||||||
|
}
|
||||||
|
}
|
||||||
) {
|
) {
|
||||||
val radius = size.minDimension / 2f * 0.92f
|
val radius = size.minDimension / 2f * 0.92f
|
||||||
val center = Offset(size.width / 2f, size.height / 2f)
|
val center = Offset(size.width / 2f, size.height / 2f)
|
||||||
@@ -125,6 +149,14 @@ fun MutualRadarView(
|
|||||||
if (last.elevationA > 0f) drawCircle(colorA, 5f, losA)
|
if (last.elevationA > 0f) drawCircle(colorA, 5f, losA)
|
||||||
if (last.elevationB > 0f) drawCircle(colorB, 5f, losB)
|
if (last.elevationB > 0f) drawCircle(colorB, 5f, losB)
|
||||||
|
|
||||||
|
// Shared time-cursor positions (both stations at selectedTime)
|
||||||
|
val cursorA = sph2Cart(center, currentSample.azimuthA, currentSample.elevationA, radius)
|
||||||
|
val cursorB = sph2Cart(center, currentSample.azimuthB, currentSample.elevationB, radius)
|
||||||
|
drawCircle(colorA, 14f, cursorA, style = Stroke(3f))
|
||||||
|
drawCircle(colorA, 6f, cursorA)
|
||||||
|
drawCircle(colorB, 14f, cursorB, style = Stroke(3f))
|
||||||
|
drawCircle(colorB, 6f, cursorB)
|
||||||
|
|
||||||
// Center dot
|
// Center dot
|
||||||
drawCircle(gridColor, 4f, center)
|
drawCircle(gridColor, 4f, center)
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -48,7 +48,9 @@ import androidx.compose.material3.Text
|
|||||||
import androidx.compose.runtime.Composable
|
import androidx.compose.runtime.Composable
|
||||||
import androidx.compose.runtime.collectAsState
|
import androidx.compose.runtime.collectAsState
|
||||||
import androidx.compose.runtime.getValue
|
import androidx.compose.runtime.getValue
|
||||||
|
import androidx.compose.runtime.mutableFloatStateOf
|
||||||
import androidx.compose.runtime.remember
|
import androidx.compose.runtime.remember
|
||||||
|
import androidx.compose.runtime.setValue
|
||||||
import androidx.compose.ui.Alignment
|
import androidx.compose.ui.Alignment
|
||||||
import androidx.compose.ui.Modifier
|
import androidx.compose.ui.Modifier
|
||||||
import androidx.compose.ui.res.painterResource
|
import androidx.compose.ui.res.painterResource
|
||||||
@@ -361,6 +363,9 @@ private fun MutualPassCard(
|
|||||||
onClick: () -> Unit,
|
onClick: () -> Unit,
|
||||||
onNavigateToRadar: () -> Unit
|
onNavigateToRadar: () -> Unit
|
||||||
) {
|
) {
|
||||||
|
// Shared time cursor: both the elevation curve and the radar track view
|
||||||
|
// are controlled by this single progress value for bidirectional drag sync.
|
||||||
|
var dragProgress by remember(pass) { mutableFloatStateOf(0.5f) }
|
||||||
ElevatedCard(
|
ElevatedCard(
|
||||||
modifier = Modifier
|
modifier = Modifier
|
||||||
.fillMaxWidth()
|
.fillMaxWidth()
|
||||||
@@ -419,6 +424,8 @@ private fun MutualPassCard(
|
|||||||
)
|
)
|
||||||
MutualRadarView(
|
MutualRadarView(
|
||||||
trackSamples = pass.trackSamples,
|
trackSamples = pass.trackSamples,
|
||||||
|
progress = dragProgress,
|
||||||
|
onProgressChange = { dragProgress = it },
|
||||||
labelA = "你",
|
labelA = "你",
|
||||||
labelB = "友台"
|
labelB = "友台"
|
||||||
)
|
)
|
||||||
@@ -429,7 +436,9 @@ private fun MutualPassCard(
|
|||||||
samples = pass.elevationSamples,
|
samples = pass.elevationSamples,
|
||||||
startTime = pass.startTime,
|
startTime = pass.startTime,
|
||||||
endTime = pass.endTime,
|
endTime = pass.endTime,
|
||||||
maxElev = maxOf(pass.maxElevationA, pass.maxElevationB, 10.0)
|
maxElev = maxOf(pass.maxElevationA, pass.maxElevationB, 10.0),
|
||||||
|
progress = dragProgress,
|
||||||
|
onProgressChange = { dragProgress = it }
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
Spacer(Modifier.height(8.dp))
|
Spacer(Modifier.height(8.dp))
|
||||||
|
|||||||
Reference in new issue
Block a user