From 28fa373db0baa0f42b78bea40eebf359744ecda1 Mon Sep 17 00:00:00 2001 From: atsunatsu Date: Mon, 3 Aug 2026 00:58:09 +0800 Subject: [PATCH] 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 --- .../feature/mutual/ElevationCurveChart.kt | 19 ++++++----- .../feature/mutual/MutualRadarView.kt | 32 +++++++++++++++++++ .../look4sat/feature/mutual/MutualScreen.kt | 11 ++++++- 3 files changed, 51 insertions(+), 11 deletions(-) diff --git a/feature/mutual/src/main/java/com/rtbishop/look4sat/feature/mutual/ElevationCurveChart.kt b/feature/mutual/src/main/java/com/rtbishop/look4sat/feature/mutual/ElevationCurveChart.kt index c81df4de..ba055df0 100644 --- a/feature/mutual/src/main/java/com/rtbishop/look4sat/feature/mutual/ElevationCurveChart.kt +++ b/feature/mutual/src/main/java/com/rtbishop/look4sat/feature/mutual/ElevationCurveChart.kt @@ -30,10 +30,7 @@ import androidx.compose.foundation.layout.padding import androidx.compose.material3.MaterialTheme import androidx.compose.material3.Text import androidx.compose.runtime.Composable -import androidx.compose.runtime.getValue -import androidx.compose.runtime.mutableFloatStateOf 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.toArgb @@ -50,6 +47,8 @@ import kotlin.math.roundToInt /** * 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 fun ElevationCurveChart( @@ -57,6 +56,8 @@ fun ElevationCurveChart( startTime: Long, endTime: Long, maxElev: Double, + progress: Float = 0.5f, + onProgressChange: (Float) -> Unit = {}, modifier: Modifier = Modifier ) { if (samples.isEmpty()) return @@ -73,8 +74,6 @@ fun ElevationCurveChart( val gridColorArgb = gridColor.toArgb() val timeFormat = remember { SimpleDateFormat("HH:mm", Locale.getDefault()) } - var dragProgress by remember { mutableFloatStateOf(0.5f) } - Column(modifier = modifier.fillMaxWidth()) { Canvas( modifier = Modifier @@ -83,12 +82,12 @@ fun ElevationCurveChart( .background(MaterialTheme.colorScheme.surfaceVariant.copy(alpha = 0.3f)) .pointerInput(Unit) { detectTapGestures { offset -> - dragProgress = (offset.x / size.width.toFloat()).coerceIn(0f, 1f) + onProgressChange((offset.x / size.width.toFloat()).coerceIn(0f, 1f)) } } .pointerInput(Unit) { - detectDragGestures { _, dragAmount -> - dragProgress = (dragProgress + dragAmount.x / size.width.toFloat()).coerceIn(0f, 1f) + detectDragGestures { change, _ -> + onProgressChange((change.position.x / size.width.toFloat()).coerceIn(0f, 1f)) } } ) { @@ -180,8 +179,8 @@ fun ElevationCurveChart( drawPath(pathB, colorB, style = Stroke(width = 2.5f)) // Drag indicator - val dragX = plotLeft + dragProgress * plotWidth - val dragTime = startTime + (dragProgress * (endTime - startTime)).toLong() + val dragX = plotLeft + progress * plotWidth + val dragTime = startTime + (progress * (endTime - startTime)).toLong() drawLine( onSurfaceColor, diff --git a/feature/mutual/src/main/java/com/rtbishop/look4sat/feature/mutual/MutualRadarView.kt b/feature/mutual/src/main/java/com/rtbishop/look4sat/feature/mutual/MutualRadarView.kt index a093c195..a5ae3248 100644 --- a/feature/mutual/src/main/java/com/rtbishop/look4sat/feature/mutual/MutualRadarView.kt +++ b/feature/mutual/src/main/java/com/rtbishop/look4sat/feature/mutual/MutualRadarView.kt @@ -18,6 +18,8 @@ package com.rtbishop.look4sat.feature.mutual 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.Column 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.drawscope.DrawScope 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.TextStyle 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 * 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 fun MutualRadarView( trackSamples: List, + progress: Float = 0.5f, + onProgressChange: (Float) -> Unit = {}, labelA: String = "站点A", labelB: String = "站点B", modifier: Modifier = Modifier @@ -66,12 +73,29 @@ fun MutualRadarView( val textColor = MaterialTheme.colorScheme.onSurfaceVariant 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()) { Canvas( modifier = Modifier .fillMaxWidth() .aspectRatio(1f) .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 center = Offset(size.width / 2f, size.height / 2f) @@ -125,6 +149,14 @@ fun MutualRadarView( if (last.elevationA > 0f) drawCircle(colorA, 5f, losA) 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 drawCircle(gridColor, 4f, center) } diff --git a/feature/mutual/src/main/java/com/rtbishop/look4sat/feature/mutual/MutualScreen.kt b/feature/mutual/src/main/java/com/rtbishop/look4sat/feature/mutual/MutualScreen.kt index dc39a00c..1c5695ff 100644 --- a/feature/mutual/src/main/java/com/rtbishop/look4sat/feature/mutual/MutualScreen.kt +++ b/feature/mutual/src/main/java/com/rtbishop/look4sat/feature/mutual/MutualScreen.kt @@ -48,7 +48,9 @@ import androidx.compose.material3.Text import androidx.compose.runtime.Composable import androidx.compose.runtime.collectAsState import androidx.compose.runtime.getValue +import androidx.compose.runtime.mutableFloatStateOf import androidx.compose.runtime.remember +import androidx.compose.runtime.setValue import androidx.compose.ui.Alignment import androidx.compose.ui.Modifier import androidx.compose.ui.res.painterResource @@ -361,6 +363,9 @@ private fun MutualPassCard( onClick: () -> 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( modifier = Modifier .fillMaxWidth() @@ -419,6 +424,8 @@ private fun MutualPassCard( ) MutualRadarView( trackSamples = pass.trackSamples, + progress = dragProgress, + onProgressChange = { dragProgress = it }, labelA = "你", labelB = "友台" ) @@ -429,7 +436,9 @@ private fun MutualPassCard( samples = pass.elevationSamples, startTime = pass.startTime, 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))