Skip to content

Commit 2df6df8

Browse files
jamesarichCopilot
andcommitted
feat(map): add line gradient, zoom buttons, camera padding
- NodeTrackLayers: replace flat color with lineProgress() gradient (faded blue → vivid blue showing position age) - MapControlsOverlay: add +/- zoom buttons in secondary toolbar (improves desktop/accessibility where pinch isn't natural) - MapScreen: add padding to zoom-to-fit-all camera animation to avoid UI controls overlap - Add lineProgress() expression helper (line-progress MapLibre expr) - Add MeshtasticIcons.Remove (minus) icon Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
1 parent 9054f6f commit 2df6df8

8 files changed

Lines changed: 121 additions & 52 deletions

File tree

.skills/compose-ui/strings-index.txt

Lines changed: 2 additions & 0 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.
Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,9 @@
1+
<vector xmlns:android="http://schemas.android.com/apk/res/android"
2+
android:width="24dp"
3+
android:height="24dp"
4+
android:viewportWidth="960"
5+
android:viewportHeight="960">
6+
<path
7+
android:fillColor="#FFFFFFFF"
8+
android:pathData="M240,520Q223,520 211.5,508.5Q200,497 200,480Q200,463 211.5,451.5Q223,440 240,440L720,440Q737,440 748.5,451.5Q760,463 760,480Q760,497 748.5,508.5Q737,520 720,520L240,520Z"/>
9+
</vector>

core/resources/src/commonMain/composeResources/values/strings.xml

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -675,6 +675,8 @@
675675
<string name="map_type_normal">Normal</string>
676676
<string name="map_type_satellite">Satellite</string>
677677
<string name="map_type_terrain">Terrain</string>
678+
<string name="map_zoom_in">Zoom in</string>
679+
<string name="map_zoom_out">Zoom out</string>
678680
<string name="mark_as_read">Mark as read</string>
679681
<string name="match_all">Match All | Any</string>
680682
<string name="match_any">Match Any | All</string>

core/ui/src/commonMain/kotlin/org/meshtastic/core/ui/icon/Actions.kt

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -46,6 +46,7 @@ import org.meshtastic.core.resources.ic_qr_code
4646
import org.meshtastic.core.resources.ic_qr_code_2
4747
import org.meshtastic.core.resources.ic_qr_code_scanner
4848
import org.meshtastic.core.resources.ic_refresh
49+
import org.meshtastic.core.resources.ic_remove
4950
import org.meshtastic.core.resources.ic_reply
5051
import org.meshtastic.core.resources.ic_restart_alt
5152
import org.meshtastic.core.resources.ic_restore
@@ -136,3 +137,5 @@ val MeshtasticIcons.BarChart: ImageVector
136137
@Composable get() = vectorResource(Res.drawable.ic_bar_chart)
137138
val MeshtasticIcons.List: ImageVector
138139
@Composable get() = vectorResource(Res.drawable.ic_list)
140+
val MeshtasticIcons.Remove: ImageVector
141+
@Composable get() = vectorResource(Res.drawable.ic_remove)

feature/map/src/commonMain/kotlin/org/meshtastic/feature/map/MapScreen.kt

Lines changed: 18 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -17,6 +17,7 @@
1717
package org.meshtastic.feature.map
1818

1919
import androidx.compose.foundation.layout.Box
20+
import androidx.compose.foundation.layout.PaddingValues
2021
import androidx.compose.foundation.layout.fillMaxSize
2122
import androidx.compose.foundation.layout.padding
2223
import androidx.compose.material3.Scaffold
@@ -65,6 +66,8 @@ import org.meshtastic.feature.map.util.toGeoPositionOrNull
6566
import org.maplibre.spatialk.geojson.Position as GeoPosition
6667

6768
private const val WAYPOINT_ZOOM = 15.0
69+
private const val MIN_ZOOM = 0.0
70+
private const val MAX_ZOOM = 24.0
6871
private val MAP_OVERLAY_PADDING = 16.dp
6972

7073
/**
@@ -249,7 +252,7 @@ fun MapScreen(
249252
}
250253
}
251254
val bbox = computeBoundingBox(positions) ?: return@MapFilterDropdown
252-
scope.launch { cameraState.animateTo(bbox) }
255+
scope.launch { cameraState.animateTo(bbox, padding = PaddingValues(48.dp)) }
253256
},
254257
)
255258
},
@@ -282,6 +285,20 @@ fun MapScreen(
282285
}
283286
}
284287
},
288+
onZoomIn = {
289+
scope.launch {
290+
cameraState.animateTo(
291+
cameraState.position.copy(zoom = minOf(cameraState.position.zoom + 1.0, MAX_ZOOM)),
292+
)
293+
}
294+
},
295+
onZoomOut = {
296+
scope.launch {
297+
cameraState.animateTo(
298+
cameraState.position.copy(zoom = maxOf(cameraState.position.zoom - 1.0, MIN_ZOOM)),
299+
)
300+
}
301+
},
285302
)
286303

287304
// Scale bar — auto-shows on zoom change, hides after 3 seconds

feature/map/src/commonMain/kotlin/org/meshtastic/feature/map/component/MapControlsOverlay.kt

Lines changed: 71 additions & 47 deletions
Original file line numberDiff line numberDiff line change
@@ -17,6 +17,7 @@
1717
package org.meshtastic.feature.map.component
1818

1919
import androidx.compose.foundation.layout.Box
20+
import androidx.compose.foundation.layout.Column
2021
import androidx.compose.foundation.layout.padding
2122
import androidx.compose.foundation.layout.size
2223
import androidx.compose.material3.Badge
@@ -28,21 +29,26 @@ import androidx.compose.material3.HorizontalFloatingToolbar
2829
import androidx.compose.material3.MaterialTheme
2930
import androidx.compose.material3.Text
3031
import androidx.compose.runtime.Composable
32+
import androidx.compose.ui.Alignment
3133
import androidx.compose.ui.Modifier
3234
import androidx.compose.ui.draw.rotate
3335
import androidx.compose.ui.unit.dp
3436
import org.jetbrains.compose.resources.stringResource
3537
import org.meshtastic.core.resources.Res
3638
import org.meshtastic.core.resources.map_filter
39+
import org.meshtastic.core.resources.map_zoom_in
40+
import org.meshtastic.core.resources.map_zoom_out
3741
import org.meshtastic.core.resources.orient_north
3842
import org.meshtastic.core.resources.refresh
3943
import org.meshtastic.core.resources.toggle_my_position
44+
import org.meshtastic.core.ui.icon.Add
4045
import org.meshtastic.core.ui.icon.LocationOn
4146
import org.meshtastic.core.ui.icon.MapCompass
4247
import org.meshtastic.core.ui.icon.MeshtasticIcons
4348
import org.meshtastic.core.ui.icon.MyLocation
4449
import org.meshtastic.core.ui.icon.NearMe
4550
import org.meshtastic.core.ui.icon.Refresh
51+
import org.meshtastic.core.ui.icon.Remove
4652
import org.meshtastic.core.ui.icon.Tune
4753
import org.meshtastic.core.ui.theme.StatusColors.StatusRed
4854
import kotlin.math.abs
@@ -63,7 +69,7 @@ import kotlin.math.abs
6369
* @param onRefresh Callback when the refresh button is clicked.
6470
*/
6571
@OptIn(ExperimentalMaterial3ExpressiveApi::class)
66-
@Suppress("LongParameterList")
72+
@Suppress("LongParameterList", "LongMethod")
6773
@Composable
6874
fun MapControlsOverlay(
6975
onToggleFilterMenu: () -> Unit,
@@ -81,68 +87,86 @@ fun MapControlsOverlay(
8187
showRefresh: Boolean = false,
8288
isRefreshing: Boolean = false,
8389
onRefresh: () -> Unit = {},
90+
onZoomIn: () -> Unit = {},
91+
onZoomOut: () -> Unit = {},
8492
) {
85-
HorizontalFloatingToolbar(
86-
expanded = true,
87-
modifier = modifier,
88-
colors = FloatingToolbarDefaults.standardFloatingToolbarColors(),
89-
) {
90-
// Compass
91-
CompassButton(onClick = onCompassClick, bearing = bearing, isFollowing = followPhoneBearing)
93+
Column(modifier = modifier, horizontalAlignment = Alignment.End) {
94+
HorizontalFloatingToolbar(expanded = true, colors = FloatingToolbarDefaults.standardFloatingToolbarColors()) {
95+
// Compass
96+
CompassButton(onClick = onCompassClick, bearing = bearing, isFollowing = followPhoneBearing)
9297

93-
// Filter button + dropdown with badge
94-
Box {
95-
if (activeFilterCount > 0) {
96-
BadgedBox(badge = { Badge { Text(activeFilterCount.toString()) } }) {
98+
// Filter button + dropdown with badge
99+
Box {
100+
if (activeFilterCount > 0) {
101+
BadgedBox(badge = { Badge { Text(activeFilterCount.toString()) } }) {
102+
MapButton(
103+
icon = MeshtasticIcons.Tune,
104+
contentDescription = stringResource(Res.string.map_filter),
105+
onClick = onToggleFilterMenu,
106+
)
107+
}
108+
} else {
97109
MapButton(
98110
icon = MeshtasticIcons.Tune,
99111
contentDescription = stringResource(Res.string.map_filter),
100112
onClick = onToggleFilterMenu,
101113
)
102114
}
103-
} else {
104-
MapButton(
105-
icon = MeshtasticIcons.Tune,
106-
contentDescription = stringResource(Res.string.map_filter),
107-
onClick = onToggleFilterMenu,
108-
)
115+
filterDropdownContent()
109116
}
110-
filterDropdownContent()
111-
}
112117

113-
// Map type selector (flavor-specific)
114-
mapTypeContent()
118+
// Map type selector (flavor-specific)
119+
mapTypeContent()
115120

116-
// Layers button (flavor-specific)
117-
layersContent()
121+
// Layers button (flavor-specific)
122+
layersContent()
118123

119-
// Refresh button (optional)
120-
if (showRefresh) {
121-
if (isRefreshing) {
122-
Box(modifier = Modifier.padding(8.dp)) {
123-
CircularProgressIndicator(modifier = Modifier.size(24.dp), strokeWidth = 2.dp)
124+
// Refresh button (optional)
125+
if (showRefresh) {
126+
if (isRefreshing) {
127+
Box(modifier = Modifier.padding(8.dp)) {
128+
CircularProgressIndicator(modifier = Modifier.size(24.dp), strokeWidth = 2.dp)
129+
}
130+
} else {
131+
MapButton(
132+
icon = MeshtasticIcons.Refresh,
133+
contentDescription = stringResource(Res.string.refresh),
134+
onClick = onRefresh,
135+
)
124136
}
125-
} else {
126-
MapButton(
127-
icon = MeshtasticIcons.Refresh,
128-
contentDescription = stringResource(Res.string.refresh),
129-
onClick = onRefresh,
130-
)
131137
}
138+
139+
// Location tracking button — 3 states: Off (MyLocation), Tracking (NearMe), TrackingNorth (LocationOn)
140+
MapButton(
141+
icon =
142+
when {
143+
!isLocationTrackingEnabled -> MeshtasticIcons.MyLocation
144+
isTrackingBearing -> MeshtasticIcons.NearMe
145+
else -> MeshtasticIcons.LocationOn
146+
},
147+
contentDescription = stringResource(Res.string.toggle_my_position),
148+
iconTint = if (isLocationTrackingEnabled) MaterialTheme.colorScheme.primary else null,
149+
onClick = onToggleLocationTracking,
150+
)
132151
}
133152

134-
// Location tracking button — 3 states: Off (MyLocation), Tracking (NearMe), TrackingNorth (LocationOn)
135-
MapButton(
136-
icon =
137-
when {
138-
!isLocationTrackingEnabled -> MeshtasticIcons.MyLocation
139-
isTrackingBearing -> MeshtasticIcons.NearMe
140-
else -> MeshtasticIcons.LocationOn
141-
},
142-
contentDescription = stringResource(Res.string.toggle_my_position),
143-
iconTint = if (isLocationTrackingEnabled) MaterialTheme.colorScheme.primary else null,
144-
onClick = onToggleLocationTracking,
145-
)
153+
// Zoom buttons (useful for desktop/accessibility where pinch isn't natural)
154+
HorizontalFloatingToolbar(
155+
expanded = true,
156+
modifier = Modifier.padding(top = 4.dp),
157+
colors = FloatingToolbarDefaults.standardFloatingToolbarColors(),
158+
) {
159+
MapButton(
160+
icon = MeshtasticIcons.Add,
161+
contentDescription = stringResource(Res.string.map_zoom_in),
162+
onClick = onZoomIn,
163+
)
164+
MapButton(
165+
icon = MeshtasticIcons.Remove,
166+
contentDescription = stringResource(Res.string.map_zoom_out),
167+
onClick = onZoomOut,
168+
)
169+
}
146170
}
147171
}
148172

feature/map/src/commonMain/kotlin/org/meshtastic/feature/map/component/NodeTrackLayers.kt

Lines changed: 6 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -25,6 +25,8 @@ import org.maplibre.compose.expressions.dsl.asString
2525
import org.maplibre.compose.expressions.dsl.const
2626
import org.maplibre.compose.expressions.dsl.eq
2727
import org.maplibre.compose.expressions.dsl.feature
28+
import org.maplibre.compose.expressions.dsl.interpolate
29+
import org.maplibre.compose.expressions.dsl.linear
2830
import org.maplibre.compose.expressions.value.LineCap
2931
import org.maplibre.compose.expressions.value.LineJoin
3032
import org.maplibre.compose.layers.CircleLayer
@@ -33,12 +35,13 @@ import org.maplibre.compose.sources.GeoJsonData
3335
import org.maplibre.compose.sources.GeoJsonOptions
3436
import org.maplibre.compose.sources.rememberGeoJsonSource
3537
import org.maplibre.compose.util.ClickResult
38+
import org.meshtastic.feature.map.util.lineProgress
3639
import org.meshtastic.feature.map.util.positionsToLineString
3740
import org.meshtastic.feature.map.util.positionsToPointFeatures
3841

3942
private val TrackColor = Color(0xFF2196F3)
43+
private val TrackColorFaded = Color(0x662196F3)
4044
private val SelectedPointColor = Color(0xFFF44336)
41-
private const val TRACK_OPACITY = 0.8f
4245
private const val SELECTED_OPACITY = 0.9f
4346

4447
/**
@@ -62,13 +65,12 @@ internal fun NodeTrackLayers(
6265
options = GeoJsonOptions(lineMetrics = true),
6366
)
6467

65-
// Track line with gradient
68+
// Track line with gradient (oldest positions faded → newest positions vivid)
6669
LineLayer(
6770
id = "node-track-line",
6871
source = lineSource,
6972
width = const(3.dp),
70-
color = const(TrackColor), // Blue
71-
opacity = const(TRACK_OPACITY),
73+
gradient = interpolate(linear(), lineProgress(), 0 to const(TrackColorFaded), 1 to const(TrackColor)),
7274
cap = const(LineCap.Round),
7375
join = const(LineJoin.Round),
7476
)

feature/map/src/commonMain/kotlin/org/meshtastic/feature/map/util/MapConstants.kt

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -18,6 +18,9 @@ package org.meshtastic.feature.map.util
1818

1919
import androidx.compose.ui.unit.Dp
2020
import androidx.compose.ui.unit.dp
21+
import org.maplibre.compose.expressions.ast.Expression
22+
import org.maplibre.compose.expressions.ast.FunctionCall
23+
import org.maplibre.compose.expressions.value.FloatValue
2124
import org.maplibre.spatialk.geojson.BoundingBox
2225
import org.maplibre.spatialk.geojson.Position as GeoPosition
2326

@@ -57,3 +60,10 @@ internal fun computeBoundingBox(positions: List<GeoPosition>): BoundingBox? {
5760
northeast = GeoPosition(longitude = lngs.max(), latitude = lats.max()),
5861
)
5962
}
63+
64+
/**
65+
* Gets the progress along a line feature, from 0 at the start to 1 at the end. Can only be used with GeoJSON sources
66+
* that specify `lineMetrics = true`. Use with [interpolate][org.maplibre.compose.expressions.dsl.interpolate] to create
67+
* gradient colors.
68+
*/
69+
internal fun lineProgress(): Expression<FloatValue> = FunctionCall.of("line-progress").cast()

0 commit comments

Comments
 (0)