mirror of
https://github.com/meshtastic/Meshtastic-Android.git
synced 2026-05-12 08:42:01 -04:00
fix(map): address review round 2 — precision circles, traceroute, i18n
- Fix precision circle radius: use zoom-based exponential interpolation to convert meters to pixels instead of treating meters as dp values - Fix InlineMap precision circle: compute pixel radius from meters at the fixed zoom-15 display level - Fix TracerouteLayers: wrap callback in LaunchedEffect to avoid state updates during composition; add nodes to remember keys for fresh hop labels; use relatedNodeNums.size for accurate total count - Fix compass bearing: use epsilon comparison (±0.5°) instead of exact float equality to prevent flickering near north - Localize EditWaypointDialog: replace hardcoded English strings with stringResource() using existing waypoint_edit/waypoint_new resources - Format coordinates to 6 decimal places in waypoint position display
This commit is contained in:
@@ -362,6 +362,7 @@
|
||||
<string name="waypoint_edit">Edit waypoint</string>
|
||||
<string name="waypoint_delete">Delete waypoint?</string>
|
||||
<string name="waypoint_new">New waypoint</string>
|
||||
<string name="waypoint_lock_to_my_node">Lock to my node</string>
|
||||
<string name="waypoint_received">Received waypoint: %1$s</string>
|
||||
<string name="error_duty_cycle">Duty Cycle limit reached. Cannot send messages right now, please try again later.</string>
|
||||
<string name="remove">Remove</string>
|
||||
|
||||
@@ -46,12 +46,16 @@ import org.meshtastic.core.resources.delete
|
||||
import org.meshtastic.core.resources.description
|
||||
import org.meshtastic.core.resources.name
|
||||
import org.meshtastic.core.resources.send
|
||||
import org.meshtastic.core.resources.waypoint_edit
|
||||
import org.meshtastic.core.resources.waypoint_lock_to_my_node
|
||||
import org.meshtastic.core.resources.waypoint_new
|
||||
import org.meshtastic.feature.map.util.convertIntToEmoji
|
||||
import org.maplibre.spatialk.geojson.Position as GeoPosition
|
||||
|
||||
private const val MAX_NAME_LENGTH = 29
|
||||
private const val MAX_DESCRIPTION_LENGTH = 99
|
||||
private const val DEFAULT_EMOJI = 0x1F4CD // Round Pushpin
|
||||
private const val COORDINATE_PRECISION = 1_000_000L
|
||||
|
||||
/**
|
||||
* Dialog for creating or editing a waypoint on the map.
|
||||
@@ -81,7 +85,7 @@ fun EditWaypointDialog(
|
||||
onDismissRequest = onDismiss,
|
||||
title = {
|
||||
Text(
|
||||
text = if (isEditing) "Edit Waypoint" else "New Waypoint",
|
||||
text = stringResource(if (isEditing) Res.string.waypoint_edit else Res.string.waypoint_new),
|
||||
style = MaterialTheme.typography.headlineSmall,
|
||||
)
|
||||
},
|
||||
@@ -122,7 +126,10 @@ fun EditWaypointDialog(
|
||||
horizontalArrangement = Arrangement.SpaceBetween,
|
||||
modifier = Modifier.fillMaxWidth(),
|
||||
) {
|
||||
Text("Lock to my node", style = MaterialTheme.typography.bodyMedium)
|
||||
Text(
|
||||
stringResource(Res.string.waypoint_lock_to_my_node),
|
||||
style = MaterialTheme.typography.bodyMedium,
|
||||
)
|
||||
Switch(checked = locked, onCheckedChange = { locked = it })
|
||||
}
|
||||
|
||||
@@ -130,7 +137,7 @@ fun EditWaypointDialog(
|
||||
if (position != null) {
|
||||
Spacer(modifier = Modifier.height(4.dp))
|
||||
Text(
|
||||
text = "${position.latitude}, ${position.longitude}",
|
||||
text = "${position.latitude.formatCoord()}, ${position.longitude.formatCoord()}",
|
||||
style = MaterialTheme.typography.bodySmall,
|
||||
color = MaterialTheme.colorScheme.onSurfaceVariant,
|
||||
)
|
||||
@@ -166,3 +173,9 @@ fun EditWaypointDialog(
|
||||
},
|
||||
)
|
||||
}
|
||||
|
||||
/** Format a coordinate to 6 decimal places without using JVM-only String.format(). */
|
||||
private fun Double.formatCoord(): String {
|
||||
val rounded = (this * COORDINATE_PRECISION).toLong() / COORDINATE_PRECISION.toDouble()
|
||||
return rounded.toString()
|
||||
}
|
||||
|
||||
@@ -45,6 +45,9 @@ private const val COORDINATE_SCALE = 1e-7
|
||||
private const val PRECISION_CIRCLE_FILL_ALPHA = 0.15f
|
||||
private const val PRECISION_CIRCLE_STROKE_ALPHA = 0.3f
|
||||
|
||||
/** Ground resolution at zoom 15 (equatorial): ~4.773 meters per pixel. */
|
||||
private const val METERS_PER_PIXEL_ZOOM15 = 4.773
|
||||
|
||||
/**
|
||||
* A compact, non-interactive map showing a single node's position. Used in node detail screens. Replaces both the
|
||||
* Google Maps and OSMDroid inline map implementations.
|
||||
@@ -89,13 +92,14 @@ fun InlineMap(node: Node, modifier: Modifier = Modifier) {
|
||||
strokeColor = const(Color.White),
|
||||
)
|
||||
|
||||
// Precision circle
|
||||
// Precision circle — radius computed from precision_meters at zoom 15
|
||||
val precisionMeters = precisionBitsToMeters(position.precision_bits ?: 0)
|
||||
if (precisionMeters > 0) {
|
||||
val radiusDp = (precisionMeters / METERS_PER_PIXEL_ZOOM15).dp
|
||||
CircleLayer(
|
||||
id = "inline-node-precision",
|
||||
source = source,
|
||||
radius = const(40.dp), // visual approximation
|
||||
radius = const(radiusDp),
|
||||
color = const(Color(node.colors.second).copy(alpha = PRECISION_CIRCLE_FILL_ALPHA)),
|
||||
strokeWidth = const(1.dp),
|
||||
strokeColor = const(Color(node.colors.second).copy(alpha = PRECISION_CIRCLE_STROKE_ALPHA)),
|
||||
|
||||
@@ -42,6 +42,7 @@ import org.meshtastic.core.ui.icon.NearMe
|
||||
import org.meshtastic.core.ui.icon.Refresh
|
||||
import org.meshtastic.core.ui.icon.Tune
|
||||
import org.meshtastic.core.ui.theme.StatusColors.StatusRed
|
||||
import kotlin.math.abs
|
||||
|
||||
/**
|
||||
* Shared map controls overlay using [HorizontalFloatingToolbar] for Material 3 Expressive styling. Provides compass,
|
||||
@@ -131,12 +132,14 @@ fun MapControlsOverlay(
|
||||
}
|
||||
}
|
||||
|
||||
private const val BEARING_NORTH_THRESHOLD = 0.5f
|
||||
|
||||
@Composable
|
||||
private fun CompassButton(onClick: () -> Unit, bearing: Float, isFollowing: Boolean) {
|
||||
val iconTint =
|
||||
when {
|
||||
isFollowing -> MaterialTheme.colorScheme.primary
|
||||
bearing == 0f -> MaterialTheme.colorScheme.StatusRed
|
||||
abs(bearing) < BEARING_NORTH_THRESHOLD -> MaterialTheme.colorScheme.StatusRed
|
||||
else -> null
|
||||
}
|
||||
MapButton(
|
||||
|
||||
@@ -32,9 +32,13 @@ import org.maplibre.compose.expressions.dsl.const
|
||||
import org.maplibre.compose.expressions.dsl.convertToColor
|
||||
import org.maplibre.compose.expressions.dsl.convertToNumber
|
||||
import org.maplibre.compose.expressions.dsl.dp
|
||||
import org.maplibre.compose.expressions.dsl.exponential
|
||||
import org.maplibre.compose.expressions.dsl.feature
|
||||
import org.maplibre.compose.expressions.dsl.interpolate
|
||||
import org.maplibre.compose.expressions.dsl.not
|
||||
import org.maplibre.compose.expressions.dsl.offset
|
||||
import org.maplibre.compose.expressions.dsl.times
|
||||
import org.maplibre.compose.expressions.dsl.zoom
|
||||
import org.maplibre.compose.layers.CircleLayer
|
||||
import org.maplibre.compose.layers.HillshadeLayer
|
||||
import org.maplibre.compose.layers.SymbolLayer
|
||||
@@ -63,6 +67,19 @@ private const val CLUSTER_RADIUS = 50
|
||||
private const val CLUSTER_MIN_POINTS = 10
|
||||
private const val PRECISION_CIRCLE_FILL_ALPHA = 0.1f
|
||||
private const val PRECISION_CIRCLE_STROKE_ALPHA = 0.3f
|
||||
|
||||
/**
|
||||
* Ground resolution at the equator: meters per pixel = 156543.03 / 2^zoom. We use an exponential(2) interpolation with
|
||||
* two stops to compute the conversion factor from meters to pixels at each zoom level. The result is multiplied by the
|
||||
* per-feature `precision_meters` property to produce a screen-pixel radius.
|
||||
*/
|
||||
private const val EQUATORIAL_METERS_PER_PIXEL_ZOOM0 = 156543.03f
|
||||
private const val PRECISION_ZOOM_MIN = 0
|
||||
private const val PRECISION_ZOOM_MAX = 24
|
||||
private const val PRECISION_SCALE_MIN = 1f / EQUATORIAL_METERS_PER_PIXEL_ZOOM0
|
||||
|
||||
@Suppress("MagicNumber")
|
||||
private const val PRECISION_SCALE_MAX = 16_777_216f / EQUATORIAL_METERS_PER_PIXEL_ZOOM0 // 2^24
|
||||
private const val CLUSTER_OPACITY = 0.85f
|
||||
private const val LABEL_OFFSET_EM = 1.5f
|
||||
private const val CLUSTER_ZOOM_INCREMENT = 2.0
|
||||
@@ -232,13 +249,21 @@ private fun NodeMarkerLayers(
|
||||
iconAllowOverlap = const(true),
|
||||
)
|
||||
|
||||
// Precision circles — sized by precision_meters property
|
||||
// Precision circles — sized by precision_meters property converted to screen pixels via zoom interpolation
|
||||
if (showPrecisionCircle) {
|
||||
// Meters-to-pixels factor doubles with each zoom level (equatorial approximation)
|
||||
val metersToPixels =
|
||||
interpolate(
|
||||
exponential(2f),
|
||||
zoom(),
|
||||
PRECISION_ZOOM_MIN to const(PRECISION_SCALE_MIN),
|
||||
PRECISION_ZOOM_MAX to const(PRECISION_SCALE_MAX),
|
||||
)
|
||||
CircleLayer(
|
||||
id = "node-precision",
|
||||
source = nodesSource,
|
||||
filter = !feature.has("cluster"),
|
||||
radius = feature["precision_meters"].convertToNumber(const(0f)).dp,
|
||||
radius = (feature["precision_meters"].convertToNumber(const(0f)) * metersToPixels).dp,
|
||||
color =
|
||||
feature["background_color"].convertToColor(
|
||||
const(NodeMarkerColor.copy(alpha = PRECISION_CIRCLE_FILL_ALPHA)),
|
||||
|
||||
@@ -17,6 +17,7 @@
|
||||
package org.meshtastic.feature.map.component
|
||||
|
||||
import androidx.compose.runtime.Composable
|
||||
import androidx.compose.runtime.LaunchedEffect
|
||||
import androidx.compose.runtime.remember
|
||||
import androidx.compose.ui.graphics.Color
|
||||
import androidx.compose.ui.unit.dp
|
||||
@@ -62,12 +63,12 @@ fun TracerouteLayers(
|
||||
if (overlay == null) return
|
||||
|
||||
// Build route line features
|
||||
val routeData = remember(overlay, nodePositions) { buildTracerouteGeoJson(overlay, nodePositions, nodes) }
|
||||
val routeData = remember(overlay, nodePositions, nodes) { buildTracerouteGeoJson(overlay, nodePositions, nodes) }
|
||||
|
||||
// Report mappable count
|
||||
// Report mappable count via side effect (avoid state updates during composition)
|
||||
val mappableCount = routeData.hopFeatures.features.size
|
||||
val totalCount = overlay.forwardRoute.size + overlay.returnRoute.size
|
||||
onMappableCountChanged(mappableCount, totalCount)
|
||||
val totalCount = overlay.relatedNodeNums.size
|
||||
LaunchedEffect(mappableCount, totalCount) { onMappableCountChanged(mappableCount, totalCount) }
|
||||
|
||||
// Forward route line
|
||||
if (routeData.forwardLine.features.isNotEmpty()) {
|
||||
|
||||
Reference in New Issue
Block a user