From aed1c4228616180bcc40b82eb757fb483ac90e93 Mon Sep 17 00:00:00 2001 From: James Rich Date: Tue, 2 Jun 2026 13:30:52 -0500 Subject: [PATCH] chore(map): bump MapLibre Compose to 0.13.0, light up desktop base map, drop Google Maps leftovers Rebased onto main and dusted off the MapLibre Compose Multiplatform branch. - Bump maplibre-compose 0.12.1 -> 0.13.0 and migrate the redesigned location API (BearingUpdate.TRACK_LOCATION -> TRACK_AUTOMATIC; LocationPuck locationState -> location). - desktopApp: add a host-detecting maplibre-native-bindings-jni runtime backend so the desktop base map renders (Metal on macOS arm64, OpenGL on Linux/Windows amd64). Without it Gradle links no native renderer and the map canvas is black. - Gate Compose map overlays off on the JVM target via a mapOverlaysSupported expect/actual flag (true Android/iOS, false desktop). maplibre-compose 0.13.0 stubs the desktop layers/sources API with TODO(), so composing markers/waypoints/tracks/traceroute threw NotImplementedError and tore down the window. Desktop now renders base-map-only; overlays auto-enable when upstream implements desktop layers. - Remove orphaned Google Maps leftovers the rebase carried over from main's #5702/#5709 marker work (MarkerBitmapRenderer.kt + play-services-maps); this branch replaces Google Maps entirely. Co-Authored-By: Claude Opus 4.8 (1M context) --- .../app/map/component/MarkerBitmapRenderer.kt | 144 ------------------ desktopApp/build.gradle.kts | 35 +++++ .../feature/map/MapOverlaysSupport.kt | 20 +++ .../feature/map/MapOverlaysSupport.kt | 31 ++++ .../org/meshtastic/feature/map/MapScreen.kt | 27 ++-- .../feature/map/component/InlineMap.kt | 5 + .../map/component/MaplibreMapContent.kt | 7 +- .../feature/map/component/NodeTrackMap.kt | 4 + .../feature/map/component/TracerouteMap.kt | 4 + .../feature/map/MapOverlaysSupport.kt | 20 +++ .../feature/map/MapOverlaysSupport.kt | 23 +++ gradle/libs.versions.toml | 9 +- 12 files changed, 167 insertions(+), 162 deletions(-) delete mode 100644 androidApp/src/google/kotlin/org/meshtastic/app/map/component/MarkerBitmapRenderer.kt create mode 100644 feature/map/src/androidMain/kotlin/org/meshtastic/feature/map/MapOverlaysSupport.kt create mode 100644 feature/map/src/commonMain/kotlin/org/meshtastic/feature/map/MapOverlaysSupport.kt create mode 100644 feature/map/src/iosMain/kotlin/org/meshtastic/feature/map/MapOverlaysSupport.kt create mode 100644 feature/map/src/jvmMain/kotlin/org/meshtastic/feature/map/MapOverlaysSupport.kt diff --git a/androidApp/src/google/kotlin/org/meshtastic/app/map/component/MarkerBitmapRenderer.kt b/androidApp/src/google/kotlin/org/meshtastic/app/map/component/MarkerBitmapRenderer.kt deleted file mode 100644 index 9b0c161eb8..0000000000 --- a/androidApp/src/google/kotlin/org/meshtastic/app/map/component/MarkerBitmapRenderer.kt +++ /dev/null @@ -1,144 +0,0 @@ -/* - * Copyright (c) 2026 Meshtastic LLC - * - * This program is free software: you can redistribute it and/or modify - * it under the terms of the GNU General Public License as published by - * the Free Software Foundation, either version 3 of the License, or - * (at your option) any later version. - * - * This program is distributed in the hope that it will be useful, - * but WITHOUT ANY WARRANTY; without even the implied warranty of - * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the - * GNU General Public License for more details. - * - * You should have received a copy of the GNU General Public License - * along with this program. If not, see . - */ -package org.meshtastic.app.map.component - -import android.content.Context -import android.graphics.Canvas -import android.graphics.Paint -import android.graphics.RectF -import android.graphics.Typeface -import android.text.TextPaint -import androidx.compose.runtime.Composable -import androidx.compose.runtime.remember -import androidx.compose.ui.platform.LocalContext -import androidx.compose.ui.platform.LocalDensity -import androidx.core.graphics.createBitmap -import com.google.android.gms.maps.MapsInitializer -import com.google.android.gms.maps.model.BitmapDescriptor -import com.google.android.gms.maps.model.BitmapDescriptorFactory -import org.meshtastic.core.model.Node - -private const val CHIP_CORNER_RADIUS_DP = 4f -private const val CHIP_PADDING_HORIZONTAL_DP = 8f -private const val CHIP_MIN_WIDTH_DP = 64f -private const val CHIP_MIN_HEIGHT_DP = 28f -private const val CHIP_TEXT_SIZE_SP = 14f -private const val EMOJI_TEXT_SIZE_SP = 32f -private const val EMOJI_PADDING_DP = 2f - -/** - * Renders a node chip marker as a [BitmapDescriptor] using Canvas — avoids the off-screen ComposeView pipeline in - * maps-compose's `MarkerComposable`/`rememberComposeBitmapDescriptor` which can crash with "The ComposeView was - * measured to have a width or height of zero" during subcomposition races (googlemaps/android-maps-compose#875). - */ -@Composable -fun rememberNodeChipDescriptor(node: Node): BitmapDescriptor { - val context = LocalContext.current - val density = LocalDensity.current.density - val fontScale = LocalDensity.current.fontScale - return remember(node.num, node.user.short_name, node.colors, node.isIgnored) { - ensureMapsInitialized(context) - renderNodeChipBitmap(node, density, fontScale) - } -} - -/** Renders an emoji waypoint marker as a [BitmapDescriptor] using Canvas. */ -@Composable -fun rememberEmojiMarkerDescriptor(codePoint: Int): BitmapDescriptor { - val context = LocalContext.current - val density = LocalDensity.current.density - val fontScale = LocalDensity.current.fontScale - return remember(codePoint) { - ensureMapsInitialized(context) - renderEmojiBitmap(codePoint, density, fontScale) - } -} - -/** - * [BitmapDescriptorFactory] only works after the Maps SDK has been initialized, which normally happens when a - * GoogleMap/MapView is created. These descriptors are built during composition, and on the node-detail inline map the - * icon is computed before that screen's GoogleMap has loaded the SDK — so [BitmapDescriptorFactory.fromBitmap] crashes - * with "IBitmapDescriptorFactory is not initialized". Initialize explicitly first; [MapsInitializer.initialize] is - * synchronous and idempotent, so it is a no-op once the SDK is already up. - */ -@Suppress("DEPRECATION") -private fun ensureMapsInitialized(context: Context) { - MapsInitializer.initialize(context) -} - -private fun renderNodeChipBitmap(node: Node, density: Float, fontScale: Float): BitmapDescriptor { - val (textColorInt, nodeColorInt) = node.colors - val scaledDensity = density * fontScale - - val textPaint = - TextPaint(Paint.ANTI_ALIAS_FLAG).apply { - textSize = CHIP_TEXT_SIZE_SP * scaledDensity - typeface = Typeface.DEFAULT_BOLD - color = textColorInt - textAlign = Paint.Align.CENTER - isStrikeThruText = node.isIgnored - } - val label = node.user.short_name.ifEmpty { "???" } - - val textWidth = textPaint.measureText(label) - val paddingH = CHIP_PADDING_HORIZONTAL_DP * density - val minWidth = CHIP_MIN_WIDTH_DP * density - val minHeight = CHIP_MIN_HEIGHT_DP * density - - val width = maxOf(minWidth, textWidth + paddingH * 2).toInt() - val height = minHeight.toInt() - - val bitmap = createBitmap(width, height) - val canvas = Canvas(bitmap) - - val bgPaint = Paint(Paint.ANTI_ALIAS_FLAG).apply { color = nodeColorInt } - val cornerRadius = CHIP_CORNER_RADIUS_DP * density - canvas.drawRoundRect(RectF(0f, 0f, width.toFloat(), height.toFloat()), cornerRadius, cornerRadius, bgPaint) - - val textX = width / 2f - val textY = (height / 2f) - ((textPaint.descent() + textPaint.ascent()) / 2f) - canvas.drawText(label, textX, textY, textPaint) - - return BitmapDescriptorFactory.fromBitmap(bitmap) -} - -private fun renderEmojiBitmap(codePoint: Int, density: Float, fontScale: Float): BitmapDescriptor { - val scaledDensity = density * fontScale - val textPaint = - TextPaint(Paint.ANTI_ALIAS_FLAG).apply { - textSize = EMOJI_TEXT_SIZE_SP * scaledDensity - textAlign = Paint.Align.CENTER - } - val emoji = String(Character.toChars(codePoint)) - val padding = EMOJI_PADDING_DP * density - - val textWidth = textPaint.measureText(emoji) - val metrics = textPaint.fontMetrics - val textHeight = metrics.descent - metrics.ascent - - val width = (textWidth + padding * 2).toInt().coerceAtLeast(1) - val height = (textHeight + padding * 2).toInt().coerceAtLeast(1) - - val bitmap = createBitmap(width, height) - val canvas = Canvas(bitmap) - - val textX = width / 2f - val textY = padding - metrics.ascent - canvas.drawText(emoji, textX, textY, textPaint) - - return BitmapDescriptorFactory.fromBitmap(bitmap) -} diff --git a/desktopApp/build.gradle.kts b/desktopApp/build.gradle.kts index d684fafaef..c54a322059 100644 --- a/desktopApp/build.gradle.kts +++ b/desktopApp/build.gradle.kts @@ -233,7 +233,42 @@ compose.desktop { } } +/** + * Selects the MapLibre Native desktop backend capability matching the build host. + * + * MapLibre Compose ships the native renderer as mutually-exclusive per-OS/arch capability variants of + * `maplibre-native-bindings-jni`. Gradle can't auto-pick among them, so without an explicit selection no native library + * is linked and the map canvas renders black. See https://maplibre.org/maplibre-compose/getting-started/. + * + * macOS is Apple-Silicon only (Metal); Linux/Windows use OpenGL here for broadest driver/Flatpak/CI compatibility + * (Vulkan variants — `*-amd64-vulkan` — are also published if preferred). + */ +fun maplibreNativeTarget(): String { + val os = System.getProperty("os.name").lowercase() + val arch = System.getProperty("os.arch").lowercase() + return when { + os.contains("mac") || os.contains("darwin") -> { + require(arch == "aarch64" || arch == "arm64") { + "MapLibre Native desktop ships only a macOS arm64 (Apple Silicon) backend; host arch '$arch' is unsupported." + } + "macos-aarch64-metal" + } + + os.contains("win") -> "windows-amd64-opengl" + + else -> "linux-amd64-opengl" + } +} + dependencies { + // MapLibre Native renderer for the current desktop host (runtime-only). Selects exactly one OS/arch + // capability — required, or the map renders black. See maplibreNativeTarget() above. + runtimeOnly("org.maplibre.compose:maplibre-native-bindings-jni:${libs.versions.maplibre.compose.get()}") { + capabilities { + requireCapability("org.maplibre.compose:maplibre-native-bindings-jni-${maplibreNativeTarget()}") + } + } + implementation(libs.aboutlibraries.core) implementation(libs.aboutlibraries.compose.m3) diff --git a/feature/map/src/androidMain/kotlin/org/meshtastic/feature/map/MapOverlaysSupport.kt b/feature/map/src/androidMain/kotlin/org/meshtastic/feature/map/MapOverlaysSupport.kt new file mode 100644 index 0000000000..c1f6cc6a02 --- /dev/null +++ b/feature/map/src/androidMain/kotlin/org/meshtastic/feature/map/MapOverlaysSupport.kt @@ -0,0 +1,20 @@ +/* + * Copyright (c) 2026 Meshtastic LLC + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + */ +package org.meshtastic.feature.map + +/** Android implements the full MapLibre Compose sources/layers API. */ +actual val mapOverlaysSupported: Boolean = true diff --git a/feature/map/src/commonMain/kotlin/org/meshtastic/feature/map/MapOverlaysSupport.kt b/feature/map/src/commonMain/kotlin/org/meshtastic/feature/map/MapOverlaysSupport.kt new file mode 100644 index 0000000000..dc57f4959d --- /dev/null +++ b/feature/map/src/commonMain/kotlin/org/meshtastic/feature/map/MapOverlaysSupport.kt @@ -0,0 +1,31 @@ +/* + * Copyright (c) 2026 Meshtastic LLC + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + */ +package org.meshtastic.feature.map + +/** + * Whether the current platform's MapLibre Compose target implements programmatic map sources and layers + * (`rememberGeoJsonSource`, `CircleLayer`, `SymbolLayer`, `LineLayer`, `HillshadeLayer`, …). + * + * As of maplibre-compose 0.13.0 the desktop (JVM) target stubs the **entire** sources/layers API with `TODO()`, so + * composing any overlay throws `NotImplementedError` ("An operation is not implemented") and tears down the window. The + * base map style still renders natively from its style URI. Every source/layer composition must therefore be guarded + * behind this flag; when `false`, only the base map is shown. + * + * Re-evaluate on each maplibre-compose upgrade — once the desktop target implements layers/sources, set this `true` for + * JVM and the guards become no-ops. + */ +expect val mapOverlaysSupported: Boolean diff --git a/feature/map/src/commonMain/kotlin/org/meshtastic/feature/map/MapScreen.kt b/feature/map/src/commonMain/kotlin/org/meshtastic/feature/map/MapScreen.kt index 42d0f6c2e6..dd104fe584 100644 --- a/feature/map/src/commonMain/kotlin/org/meshtastic/feature/map/MapScreen.kt +++ b/feature/map/src/commonMain/kotlin/org/meshtastic/feature/map/MapScreen.kt @@ -133,7 +133,7 @@ fun MapScreen( // Location tracking state: 3-mode cycling (Off → Track → TrackBearing → Off) var isLocationTrackingEnabled by remember { mutableStateOf(false) } - var bearingUpdate by remember { mutableStateOf(BearingUpdate.TRACK_LOCATION) } + var bearingUpdate by remember { mutableStateOf(BearingUpdate.TRACK_AUTOMATIC) } val locationProvider = rememberLocationProviderOrNull() val locationState = rememberUserLocationState(locationProvider ?: rememberNullLocationProvider()) val locationAvailable = locationProvider != null @@ -144,8 +144,13 @@ fun MapScreen( if (isLocationTrackingEnabled) { when (bearingUpdate) { BearingUpdate.IGNORE -> GestureOptions.PositionLocked + BearingUpdate.ALWAYS_NORTH -> GestureOptions.ZoomOnly - BearingUpdate.TRACK_LOCATION -> GestureOptions.ZoomOnly + + BearingUpdate.TRACK_AUTOMATIC, + BearingUpdate.TRACK_COURSE, + BearingUpdate.TRACK_ORIENTATION, + -> GestureOptions.ZoomOnly } } else { GestureOptions.Standard @@ -220,7 +225,7 @@ fun MapScreen( LocationTrackingEffect( locationState = locationState, enabled = isLocationTrackingEnabled, - trackBearing = bearingUpdate == BearingUpdate.TRACK_LOCATION, + trackBearing = bearingUpdate == BearingUpdate.TRACK_AUTOMATIC, ) { cameraState.updateFromLocation(updateBearing = bearingUpdate) } @@ -239,7 +244,7 @@ fun MapScreen( modifier = Modifier.align(Alignment.TopEnd).padding(paddingValues), bearing = cameraState.position.bearing.toFloat(), onCompassClick = { scope.launch { cameraState.animateTo(cameraState.position.copy(bearing = 0.0)) } }, - followPhoneBearing = isLocationTrackingEnabled && bearingUpdate == BearingUpdate.TRACK_LOCATION, + followPhoneBearing = isLocationTrackingEnabled && bearingUpdate == BearingUpdate.TRACK_AUTOMATIC, activeFilterCount = activeFilterCount, filterDropdownContent = { MapFilterDropdown( @@ -267,27 +272,23 @@ fun MapScreen( }, layersContent = { OfflineMapContent(styleUri = selectedMapStyle.styleUri, cameraState = cameraState) }, isLocationTrackingEnabled = isLocationTrackingEnabled, - isTrackingBearing = bearingUpdate == BearingUpdate.TRACK_LOCATION, + isTrackingBearing = bearingUpdate == BearingUpdate.TRACK_AUTOMATIC, onToggleLocationTracking = { if (!locationAvailable) { scope.launch { snackbarHostState.showSnackbar(locationUnavailableMsg) } } else if (!isLocationTrackingEnabled) { // Off → Track with bearing - bearingUpdate = BearingUpdate.TRACK_LOCATION + bearingUpdate = BearingUpdate.TRACK_AUTOMATIC isLocationTrackingEnabled = true } else { when (bearingUpdate) { - BearingUpdate.TRACK_LOCATION -> { + BearingUpdate.TRACK_AUTOMATIC -> { // TrackBearing → TrackNorth bearingUpdate = BearingUpdate.ALWAYS_NORTH } - BearingUpdate.ALWAYS_NORTH -> { - // TrackNorth → Off - isLocationTrackingEnabled = false - } - - BearingUpdate.IGNORE -> { + else -> { + // TrackNorth (or any other) → Off isLocationTrackingEnabled = false } } diff --git a/feature/map/src/commonMain/kotlin/org/meshtastic/feature/map/component/InlineMap.kt b/feature/map/src/commonMain/kotlin/org/meshtastic/feature/map/component/InlineMap.kt index 5e4683823e..541bd6ec78 100644 --- a/feature/map/src/commonMain/kotlin/org/meshtastic/feature/map/component/InlineMap.kt +++ b/feature/map/src/commonMain/kotlin/org/meshtastic/feature/map/component/InlineMap.kt @@ -41,6 +41,7 @@ import org.maplibre.spatialk.geojson.Feature import org.maplibre.spatialk.geojson.FeatureCollection import org.maplibre.spatialk.geojson.Point import org.meshtastic.core.model.Node +import org.meshtastic.feature.map.mapOverlaysSupported import org.meshtastic.feature.map.model.MapStyle import org.meshtastic.feature.map.util.MARKER_STROKE_WIDTH import org.meshtastic.feature.map.util.NODE_MARKER_RADIUS @@ -84,6 +85,10 @@ fun InlineMap(node: Node, modifier: Modifier = Modifier) { options = MapOptions(gestureOptions = GestureOptions.AllDisabled, ornamentOptions = OrnamentOptions.AllDisabled), ) { + // Desktop (maplibre-compose 0.13.0) stubs all layers/sources; render base map only. See + // [mapOverlaysSupported]. + if (!mapOverlaysSupported) return@MaplibreMap + val source = rememberGeoJsonSource(data = GeoJsonData.Features(nodeFeature)) // Node marker dot diff --git a/feature/map/src/commonMain/kotlin/org/meshtastic/feature/map/component/MaplibreMapContent.kt b/feature/map/src/commonMain/kotlin/org/meshtastic/feature/map/component/MaplibreMapContent.kt index fadaa46f51..887e3003dd 100644 --- a/feature/map/src/commonMain/kotlin/org/meshtastic/feature/map/component/MaplibreMapContent.kt +++ b/feature/map/src/commonMain/kotlin/org/meshtastic/feature/map/component/MaplibreMapContent.kt @@ -79,6 +79,7 @@ import org.maplibre.compose.util.ClickResult import org.maplibre.spatialk.geojson.Point import org.meshtastic.core.model.DataPacket import org.meshtastic.core.model.Node +import org.meshtastic.feature.map.mapOverlaysSupported import org.meshtastic.feature.map.util.MARKER_STROKE_WIDTH import org.meshtastic.feature.map.util.NODE_MARKER_RADIUS import org.meshtastic.feature.map.util.PRECISION_CIRCLE_STROKE_ALPHA @@ -157,6 +158,10 @@ fun MaplibreMapContent( onMapLoadFinished = onMapLoad, onMapLoadFailed = onMapLoadFail, ) { + // MapLibre Compose layers/sources are stubbed on desktop (maplibre-compose 0.13.0); gate overlays off + // there so the base map still renders without throwing NotImplementedError. See [mapOverlaysSupported]. + if (!mapOverlaysSupported) return@MaplibreMap + // --- Terrain hillshade overlay --- if (showHillshade) { val demSource = rememberRasterDemSource(tiles = TERRAIN_TILES, encoding = RasterDemEncoding.Terrarium) @@ -181,7 +186,7 @@ fun MaplibreMapContent( if (locationState != null) { LocationPuck( idPrefix = "user-location", - locationState = locationState, + location = locationState.location, cameraState = cameraState, colors = LocationPuckDefaults.colors(), ) diff --git a/feature/map/src/commonMain/kotlin/org/meshtastic/feature/map/component/NodeTrackMap.kt b/feature/map/src/commonMain/kotlin/org/meshtastic/feature/map/component/NodeTrackMap.kt index 619245a9f3..0d2a42f237 100644 --- a/feature/map/src/commonMain/kotlin/org/meshtastic/feature/map/component/NodeTrackMap.kt +++ b/feature/map/src/commonMain/kotlin/org/meshtastic/feature/map/component/NodeTrackMap.kt @@ -28,6 +28,7 @@ import org.maplibre.compose.map.GestureOptions import org.maplibre.compose.map.MapOptions import org.maplibre.compose.map.MaplibreMap import org.maplibre.compose.map.OrnamentOptions +import org.meshtastic.feature.map.mapOverlaysSupported import org.meshtastic.feature.map.model.MapStyle import org.meshtastic.feature.map.util.computeBoundingBox import org.meshtastic.feature.map.util.toGeoPositionOrNull @@ -79,6 +80,9 @@ fun NodeTrackMap( options = MapOptions(gestureOptions = GestureOptions.RotationLocked, ornamentOptions = OrnamentOptions.AllEnabled), ) { + // Desktop (maplibre-compose 0.13.0) stubs all layers/sources; render base map only. See [mapOverlaysSupported]. + if (!mapOverlaysSupported) return@MaplibreMap + NodeTrackLayers( positions = positions, selectedPositionTime = selectedPositionTime, diff --git a/feature/map/src/commonMain/kotlin/org/meshtastic/feature/map/component/TracerouteMap.kt b/feature/map/src/commonMain/kotlin/org/meshtastic/feature/map/component/TracerouteMap.kt index f1523e757b..064bc33293 100644 --- a/feature/map/src/commonMain/kotlin/org/meshtastic/feature/map/component/TracerouteMap.kt +++ b/feature/map/src/commonMain/kotlin/org/meshtastic/feature/map/component/TracerouteMap.kt @@ -30,6 +30,7 @@ import org.maplibre.compose.map.MaplibreMap import org.maplibre.compose.map.OrnamentOptions import org.meshtastic.core.model.Node import org.meshtastic.core.model.TracerouteOverlay +import org.meshtastic.feature.map.mapOverlaysSupported import org.meshtastic.feature.map.model.MapStyle import org.meshtastic.feature.map.util.computeBoundingBox import org.meshtastic.feature.map.util.toGeoPositionOrNull @@ -87,6 +88,9 @@ fun TracerouteMap( options = MapOptions(gestureOptions = GestureOptions.RotationLocked, ornamentOptions = OrnamentOptions.AllEnabled), ) { + // Desktop (maplibre-compose 0.13.0) stubs all layers/sources; render base map only. See [mapOverlaysSupported]. + if (!mapOverlaysSupported) return@MaplibreMap + TracerouteLayers( overlay = tracerouteOverlay, nodePositions = tracerouteNodePositions, diff --git a/feature/map/src/iosMain/kotlin/org/meshtastic/feature/map/MapOverlaysSupport.kt b/feature/map/src/iosMain/kotlin/org/meshtastic/feature/map/MapOverlaysSupport.kt new file mode 100644 index 0000000000..2dfb87c2ea --- /dev/null +++ b/feature/map/src/iosMain/kotlin/org/meshtastic/feature/map/MapOverlaysSupport.kt @@ -0,0 +1,20 @@ +/* + * Copyright (c) 2026 Meshtastic LLC + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + */ +package org.meshtastic.feature.map + +/** iOS implements the full MapLibre Compose sources/layers API. */ +actual val mapOverlaysSupported: Boolean = true diff --git a/feature/map/src/jvmMain/kotlin/org/meshtastic/feature/map/MapOverlaysSupport.kt b/feature/map/src/jvmMain/kotlin/org/meshtastic/feature/map/MapOverlaysSupport.kt new file mode 100644 index 0000000000..4e1df79af6 --- /dev/null +++ b/feature/map/src/jvmMain/kotlin/org/meshtastic/feature/map/MapOverlaysSupport.kt @@ -0,0 +1,23 @@ +/* + * Copyright (c) 2026 Meshtastic LLC + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + */ +package org.meshtastic.feature.map + +/** + * Desktop (JVM) does NOT implement the MapLibre Compose sources/layers API in maplibre-compose 0.13.0 — every + * `Layer`/`Source` is stubbed with `TODO()`. Overlays are gated off so the base map renders without crashing. + */ +actual val mapOverlaysSupported: Boolean = false diff --git a/gradle/libs.versions.toml b/gradle/libs.versions.toml index 55c4eb56ef..b5d2d11e6f 100644 --- a/gradle/libs.versions.toml +++ b/gradle/libs.versions.toml @@ -47,14 +47,15 @@ compose-multiplatform-material3 = "1.11.0-alpha07" # AndroidCompose.kt's resolutionStrategy force-aligns these groups to *this* version # at resolution time, so it is the source of truth for the Android target. androidx-compose-bom-aligned = "1.11.2" -# `androidx-compose-material` (M2) is independent of CMP. Pinned because -# maps-compose-widgets requests `androidx.compose.material:material` without -# a version (relying on a BOM that we exclude). M2 is frozen at 1.7.8. +# `androidx-compose-material` (M2) is independent of CMP. Pinned because some +# transitive consumers request `androidx.compose.material:material` without a +# version (relying on a BOM that we exclude). M2 is frozen at 1.7.8. +# Consumed by build-logic AndroidCompose.kt to force-align the M2 material group. androidx-compose-material = "1.7.8" jetbrains-adaptive = "1.3.0-beta01" # MapLibre -maplibre-compose = "0.12.1" +maplibre-compose = "0.13.0" # ML Kit mlkit-barcode-scanning = "17.3.0"