mirror of
https://github.com/meshtastic/Meshtastic-Android.git
synced 2026-07-08 12:26:23 -04:00
Style GeoJSON overlays from simplestyle-spec (fill/stroke) (#6088)
Co-authored-by: James Rich <james.a.rich@gmail.com> Co-authored-by: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
committed by
GitHub
parent
bf5f86daae
commit
034005b4fa
@@ -90,7 +90,10 @@ import com.google.maps.android.compose.rememberCameraPositionState
|
||||
import com.google.maps.android.compose.rememberUpdatedMarkerState
|
||||
import com.google.maps.android.compose.widgets.ScaleBar
|
||||
import com.google.maps.android.data.Layer
|
||||
import com.google.maps.android.data.geojson.GeoJsonFeature
|
||||
import com.google.maps.android.data.geojson.GeoJsonLayer
|
||||
import com.google.maps.android.data.geojson.GeoJsonLineStringStyle
|
||||
import com.google.maps.android.data.geojson.GeoJsonPolygonStyle
|
||||
import com.google.maps.android.data.kml.KmlLayer
|
||||
import kotlinx.coroutines.CoroutineScope
|
||||
import kotlinx.coroutines.launch
|
||||
@@ -154,6 +157,8 @@ import org.meshtastic.proto.Position
|
||||
import org.meshtastic.proto.Waypoint
|
||||
import kotlin.math.abs
|
||||
import kotlin.math.max
|
||||
import kotlin.math.roundToInt
|
||||
import android.graphics.Color as AndroidColor
|
||||
|
||||
// region --- Map Mode ---
|
||||
|
||||
@@ -192,6 +197,11 @@ private val GEOFENCE_OVERLAY_COLOR = Color(0xFFFF9800)
|
||||
private const val GEOFENCE_FILL_ALPHA = 0.12f
|
||||
private const val GEOFENCE_STROKE_WIDTH = 2f
|
||||
|
||||
// simplestyle-spec fallbacks for imported GeoJSON overlays that omit these keys; tune here.
|
||||
// 0.35 lets stacked contour bands read as a gradient.
|
||||
private const val DEFAULT_GEOJSON_FILL_OPACITY = 0.35f
|
||||
private const val DEFAULT_GEOJSON_STROKE_WIDTH = 2f
|
||||
|
||||
// Minimum lat/lon delta (~11 m) between the two box-authoring corner taps; below this the box would be degenerate
|
||||
// (zero-area) so the second tap is ignored.
|
||||
private const val BOX_AUTHORING_MIN_CORNER_DELTA = 1e-4
|
||||
@@ -1207,7 +1217,9 @@ private fun MapLayerOverlay(layerItem: MapLayerItem, mapViewModel: MapViewModel)
|
||||
LayerType.KML -> KmlLayer(map, inputStream, context)
|
||||
|
||||
LayerType.GEOJSON ->
|
||||
GeoJsonLayer(map, JSONObject(inputStream.bufferedReader().use { it.readText() }))
|
||||
GeoJsonLayer(map, JSONObject(inputStream.bufferedReader().use { it.readText() })).also {
|
||||
it.applySimpleStyleSpec()
|
||||
}
|
||||
}
|
||||
} catch (e: Exception) {
|
||||
Logger.withTag("MapView").e(e) { "Error loading map layer: ${layerItem.name}" }
|
||||
@@ -1248,6 +1260,82 @@ private fun Layer.safeAddLayerToMap() {
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Apply simplestyle-spec (https://github.com/mapbox/simplestyle-spec) properties to a GeoJSON layer.
|
||||
*
|
||||
* Google's [GeoJsonLayer] otherwise applies one default style to every feature, so exports that carry per-feature
|
||||
* colors render unstyled. In particular, Meshtastic Site Planner coverage contours set `fill`/`stroke` (plus a legacy
|
||||
* `color`) and `fill-opacity`; read those and style each polygon/line so the coverage draws in its dBm colors instead
|
||||
* of the default black outline.
|
||||
*/
|
||||
private fun GeoJsonLayer.applySimpleStyleSpec() {
|
||||
for (feature in features) {
|
||||
val fill = feature.cssColor("fill") ?: feature.cssColor("color")
|
||||
val stroke = feature.cssColor("stroke") ?: feature.cssColor("color")
|
||||
val fillOpacity = feature.getProperty("fill-opacity")?.toFloatOrNull()
|
||||
val strokeWidth = feature.getProperty("stroke-width")?.toFloatOrNull() ?: DEFAULT_GEOJSON_STROKE_WIDTH
|
||||
when (feature.geometry?.geometryType) {
|
||||
"Polygon",
|
||||
"MultiPolygon",
|
||||
->
|
||||
feature.polygonStyle =
|
||||
GeoJsonPolygonStyle().apply {
|
||||
fill?.let { fillColor = it.resolveFillAlpha(fillOpacity) }
|
||||
stroke?.let { strokeColor = it }
|
||||
this.strokeWidth = strokeWidth
|
||||
}
|
||||
|
||||
"LineString",
|
||||
"MultiLineString",
|
||||
->
|
||||
feature.lineStringStyle =
|
||||
GeoJsonLineStringStyle().apply {
|
||||
stroke?.let { color = it }
|
||||
width = strokeWidth
|
||||
}
|
||||
|
||||
else -> Unit // Points keep the default marker.
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private fun GeoJsonFeature.cssColor(key: String): Int? = getProperty(key)?.let { parseCssColor(it) }
|
||||
|
||||
/**
|
||||
* Resolve a polygon fill's alpha: `fill-opacity` wins when present; otherwise keep any alpha the color already carries
|
||||
* (`rgba()`/`#AARRGGBB`), falling back to [DEFAULT_GEOJSON_FILL_OPACITY] for opaque fills.
|
||||
*/
|
||||
private fun Int.resolveFillAlpha(fillOpacity: Float?): Int = when {
|
||||
fillOpacity != null -> withAlpha(fillOpacity)
|
||||
AndroidColor.alpha(this) < 255 -> this
|
||||
else -> withAlpha(DEFAULT_GEOJSON_FILL_OPACITY)
|
||||
}
|
||||
|
||||
/** Parse a hex (`#RRGGBB`/`#AARRGGBB`), `rgb()/rgba()`, or named color to an ARGB int; null if invalid. */
|
||||
private fun parseCssColor(raw: String): Int? {
|
||||
val value = raw.trim()
|
||||
return try {
|
||||
if (value.startsWith("rgb", ignoreCase = true)) {
|
||||
val parts = value.substringAfter('(').substringBefore(')').split(',').map { it.trim() }
|
||||
if (parts.size < 3) return null
|
||||
val alpha = if (parts.size >= 4) (parts[3].toFloat() * 255f).roundToInt() else 255
|
||||
AndroidColor.argb(alpha, parts[0].toInt(), parts[1].toInt(), parts[2].toInt())
|
||||
} else {
|
||||
AndroidColor.parseColor(value) // #hex or named color
|
||||
}
|
||||
} catch (e: IllegalArgumentException) {
|
||||
Logger.withTag("MapView").w(e) { "Unparseable GeoJSON color: $raw" }
|
||||
null
|
||||
}
|
||||
}
|
||||
|
||||
private fun Int.withAlpha(opacity: Float): Int = AndroidColor.argb(
|
||||
(opacity.coerceIn(0f, 1f) * 255f).roundToInt(),
|
||||
AndroidColor.red(this),
|
||||
AndroidColor.green(this),
|
||||
AndroidColor.blue(this),
|
||||
)
|
||||
|
||||
// endregion
|
||||
|
||||
// region --- Utilities ---
|
||||
|
||||
Reference in New Issue
Block a user