Skip to content

Commit d151b71

Browse files
jamesarichCopilot
andcommitted
feat(map): inline map polish & location permission feedback
- InlineMap: add short name SymbolLayer label above marker - InlineMap: adaptive zoom (zoom out for imprecise positions >500m) - MapScreen: show snackbar when tapping location button without permission instead of silently doing nothing - Add map_location_unavailable string resource Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
1 parent 2df6df8 commit d151b71

4 files changed

Lines changed: 33 additions & 3 deletions

File tree

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

Lines changed: 1 addition & 0 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

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

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -650,6 +650,7 @@
650650
<string name="map_filter">Map Filter\n</string>
651651
<string name="map_layer_formats">Map layers support .kml, .kmz, or GeoJSON formats.</string>
652652
<string name="map_load_error">Map failed to load</string>
653+
<string name="map_location_unavailable">Location permission required for tracking</string>
653654
<string name="map_node_popup_details">%1$s&lt;br&gt;Last heard: %2$s&lt;br&gt;Last position: %3$s&lt;br&gt;Battery: %4$s</string>
654655
<string name="map_offline_manager">Offline Manager</string>
655656
<string name="map_purge_fail">SQL Cache purge failed, see logcat for details</string>

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

Lines changed: 5 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -51,6 +51,7 @@ import org.meshtastic.core.resources.Res
5151
import org.meshtastic.core.resources.map
5252
import org.meshtastic.core.resources.map_empty_state
5353
import org.meshtastic.core.resources.map_load_error
54+
import org.meshtastic.core.resources.map_location_unavailable
5455
import org.meshtastic.core.resources.waypoint_deleted
5556
import org.meshtastic.core.resources.waypoint_sent
5657
import org.meshtastic.core.ui.component.MainAppBar
@@ -112,6 +113,7 @@ fun MapScreen(
112113

113114
// Snackbar messages for map load error
114115
val mapLoadErrorMsg = stringResource(Res.string.map_load_error)
116+
val locationUnavailableMsg = stringResource(Res.string.map_location_unavailable)
115117
val waypointSentMsg = stringResource(Res.string.waypoint_sent)
116118
val waypointDeletedMsg = stringResource(Res.string.waypoint_deleted)
117119

@@ -263,7 +265,9 @@ fun MapScreen(
263265
isLocationTrackingEnabled = isLocationTrackingEnabled,
264266
isTrackingBearing = bearingUpdate == BearingUpdate.TRACK_LOCATION,
265267
onToggleLocationTracking = {
266-
if (!isLocationTrackingEnabled) {
268+
if (!locationAvailable) {
269+
scope.launch { snackbarHostState.showSnackbar(locationUnavailableMsg) }
270+
} else if (!isLocationTrackingEnabled) {
267271
// Off → Track with bearing
268272
bearingUpdate = BearingUpdate.TRACK_LOCATION
269273
isLocationTrackingEnabled = true

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

Lines changed: 26 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -22,10 +22,14 @@ import androidx.compose.runtime.remember
2222
import androidx.compose.ui.Modifier
2323
import androidx.compose.ui.graphics.Color
2424
import androidx.compose.ui.unit.dp
25+
import androidx.compose.ui.unit.em
2526
import org.maplibre.compose.camera.CameraPosition
2627
import org.maplibre.compose.camera.rememberCameraState
2728
import org.maplibre.compose.expressions.dsl.const
29+
import org.maplibre.compose.expressions.dsl.offset
30+
import org.maplibre.compose.expressions.value.SymbolAnchor
2831
import org.maplibre.compose.layers.CircleLayer
32+
import org.maplibre.compose.layers.SymbolLayer
2933
import org.maplibre.compose.map.GestureOptions
3034
import org.maplibre.compose.map.MapOptions
3135
import org.maplibre.compose.map.MaplibreMap
@@ -44,7 +48,10 @@ import org.meshtastic.feature.map.util.precisionBitsToMeters
4448
import org.meshtastic.feature.map.util.toGeoPositionOrNull
4549

4650
private const val DEFAULT_ZOOM = 15.0
51+
private const val LOW_PRECISION_ZOOM = 12.0
52+
private const val PRECISION_THRESHOLD_METERS = 500
4753
private const val PRECISION_CIRCLE_FILL_ALPHA = 0.15f
54+
private const val LABEL_OFFSET = -2f
4855

4956
/**
5057
* A compact, non-interactive map showing a single node's position. Used in node detail screens. Replaces both the
@@ -55,8 +62,12 @@ fun InlineMap(node: Node, modifier: Modifier = Modifier) {
5562
val position = node.validPosition ?: return
5663
val geoPos = toGeoPositionOrNull(position.latitude_i, position.longitude_i) ?: return
5764

65+
// Adaptive zoom: zoom out for imprecise positions so the precision circle is visible
66+
val precisionMeters = precisionBitsToMeters(position.precision_bits)
67+
val zoom = if (precisionMeters > PRECISION_THRESHOLD_METERS) LOW_PRECISION_ZOOM else DEFAULT_ZOOM
68+
5869
key(node.num) {
59-
val cameraState = rememberCameraState(firstPosition = CameraPosition(target = geoPos, zoom = DEFAULT_ZOOM))
70+
val cameraState = rememberCameraState(firstPosition = CameraPosition(target = geoPos, zoom = zoom))
6071

6172
val nodeFeature =
6273
remember(node.num, geoPos) {
@@ -82,8 +93,21 @@ fun InlineMap(node: Node, modifier: Modifier = Modifier) {
8293
strokeColor = const(Color.White),
8394
)
8495

96+
// Short name label above the marker
97+
val shortName = node.user.short_name
98+
if (!shortName.isNullOrBlank()) {
99+
SymbolLayer(
100+
id = "inline-node-label",
101+
source = source,
102+
textField = const(shortName).cast(),
103+
textSize = const(0.9f.em),
104+
textOffset = offset(0f.em, LABEL_OFFSET.em),
105+
textAnchor = const(SymbolAnchor.Bottom),
106+
textColor = const(Color.DarkGray),
107+
)
108+
}
109+
85110
// Precision circle — radius computed from precision_meters using latitude-aware metersPerDp
86-
val precisionMeters = precisionBitsToMeters(position.precision_bits)
87111
val metersPerDp = cameraState.metersPerDpAtTarget
88112
if (precisionMeters > 0 && metersPerDp > 0) {
89113
val radiusDp = (precisionMeters / metersPerDp).dp

0 commit comments

Comments
 (0)