From 81525c40ad0c331b80c748fab57f47400b5a37e0 Mon Sep 17 00:00:00 2001 From: Masashi Katsumata Date: Sat, 7 Nov 2026 23:55:27 -0800 Subject: [PATCH] Stack overlays by band, independent of add order and style reloads Before: circles, then polylines were appended, polygon z-layers went below the polyline layer (above circles) and ground images directly below the polyline layer (above polygons). With the Standard style the basemap is an import and has no label layer in styleLayers, so rasters were appended on top of every overlay. MapboxOverlayStacking records the band and zIndex of every layer the SDK adds and inserts each one below the lowest SDK layer with a higher key. Rasters stay below the basemap's first label layer when the style has one. Stacking order (all providers, all platforms), bottom to top: raster layers < ground images < polygons < circles < polylines < markers. Needs android-sdk-core with OverlayStacking (fix/overlay-stacking-order). --- api/android-for-mapbox.api | 9 ++ .../mapbox/MapboxMapViewControllerStyle.kt | 53 +++----- .../mapbox/MapboxOverlayStacking.kt | 86 +++++++++++++ .../MapboxGroundImageOverlayRenderer.kt | 25 ++-- .../MapboxRasterLayerOverlayRenderer.kt | 118 ++++-------------- 5 files changed, 143 insertions(+), 148 deletions(-) create mode 100644 src/main/java/com/mapconductor/mapbox/MapboxOverlayStacking.kt diff --git a/api/android-for-mapbox.api b/api/android-for-mapbox.api index 7e1e793..86164e0 100644 --- a/api/android-for-mapbox.api +++ b/api/android-for-mapbox.api @@ -211,6 +211,15 @@ public final class com.mapconductor.mapbox.MapboxMapViewSaver extends com.mapcon public final class com.mapconductor.mapbox.MapboxMapViewScope extends com.mapconductor.compose.MapViewScope { public com.mapconductor.mapbox.MapboxMapViewScope(); } +public final class com.mapconductor.mapbox.MapboxOverlayStacking { + public static final com.mapconductor.mapbox.MapboxOverlayStacking INSTANCE; + public final com.mapconductor.core.controller.OverlayLayerStack stackOf(com.mapbox.maps.MapboxMap); + public final com.mapconductor.core.controller.OverlayStacking$Key key(com.mapconductor.core.controller.OverlayKind, int); + public final com.mapconductor.core.controller.OverlayStacking$Key getMARKER_TILE(); + public final com.mapconductor.core.controller.OverlayStacking$Key getMARKER_DRAG(); + public final void add(com.mapbox.maps.MapboxMap, com.mapbox.maps.Style, com.mapbox.maps.extension.style.layers.Layer, com.mapconductor.core.controller.OverlayStacking$Key); + public final void remove(com.mapbox.maps.MapboxMap, com.mapbox.maps.Style, java.lang.String); +} public final class com.mapconductor.mapbox.MapboxPaddings extends com.mapconductor.core.map.MapPaddings implements com.mapconductor.mapbox.IMapboxPaddingsInterface { public static final com.mapconductor.mapbox.MapboxPaddings$Companion Companion; public com.mapconductor.mapbox.MapboxPaddings(double, double, double, double); diff --git a/src/main/java/com/mapconductor/mapbox/MapboxMapViewControllerStyle.kt b/src/main/java/com/mapconductor/mapbox/MapboxMapViewControllerStyle.kt index cbbc2d0..3f08602 100644 --- a/src/main/java/com/mapconductor/mapbox/MapboxMapViewControllerStyle.kt +++ b/src/main/java/com/mapconductor/mapbox/MapboxMapViewControllerStyle.kt @@ -1,9 +1,7 @@ package com.mapconductor.mapbox -import com.mapbox.maps.extension.style.layers.addLayer -import com.mapbox.maps.extension.style.layers.addLayerAbove -import com.mapbox.maps.extension.style.layers.addLayerBelow import com.mapbox.maps.extension.style.sources.addSource +import com.mapconductor.core.controller.OverlayKind import com.mapconductor.mapbox.marker.MapboxMarkerOverlayRenderer /** @@ -21,18 +19,12 @@ internal fun MapboxMapViewController.attachMarkerLayers( style.addSource(renderer.markerLayer.source) } catch (_: Exception) { } - try { - style.addLayer(renderer.markerLayer.layer) - } catch (_: Exception) { - } + MapboxOverlayStacking.add(holder.map, style, renderer.markerLayer.layer, MapboxOverlayStacking.key(OverlayKind.Marker)) try { style.addSource(renderer.dragLayer.source) } catch (_: Exception) { } - try { - style.addLayer(renderer.dragLayer.layer) - } catch (_: Exception) { - } + MapboxOverlayStacking.add(holder.map, style, renderer.dragLayer.layer, MapboxOverlayStacking.MARKER_DRAG) } internal fun MapboxMapViewController.attachOverlaySourcesAndLayers(style: com.mapbox.maps.Style) { @@ -42,14 +34,22 @@ internal fun MapboxMapViewController.attachOverlaySourcesAndLayers(style: com.ma // Circle fill (fill pass) then stroke (line pass) — both below general polyline style.addSource(circleController.renderer.layer.source) - style.addLayer(circleController.renderer.layer.fillLayer) - style.addLayer(circleController.renderer.layer.strokeLayer) + // 層の位置は MapboxOverlayStacking が帯の順で決める(追加の順に依らない)。 + // 線は塗りの直後(同じ鍵なら後から入れたものが上)。 + val circleKey = MapboxOverlayStacking.key(OverlayKind.Circle) + MapboxOverlayStacking.add(holder.map, style, circleController.renderer.layer.fillLayer, circleKey) + MapboxOverlayStacking.add(holder.map, style, circleController.renderer.layer.strokeLayer, circleKey) // Polyline (general) style.addSource(polylineController.renderer.layer.source) - style.addLayer(polylineController.renderer.layer.layer) + MapboxOverlayStacking.add( + holder.map, + style, + polylineController.renderer.layer.layer, + MapboxOverlayStacking.key(OverlayKind.Polyline), + ) - // Add z-indexed polygon layers below general polylines + // Polygon z-layers (polygon band: above ground images, below circles) ensurePolygonZLayers(style) // Marker + drag layers @@ -76,14 +76,8 @@ internal fun MapboxMapViewController.ensurePolygonZLayers(style: com.mapbox.maps toRemove.forEach { z -> val fillId = "polygon-fill-layer-$z" val outlineId = "polygon-outline-layer-$z" - try { - style.removeStyleLayer(outlineId) - } catch (_: Exception) { - } - try { - style.removeStyleLayer(fillId) - } catch (_: Exception) { - } + MapboxOverlayStacking.remove(holder.map, style, outlineId) + MapboxOverlayStacking.remove(holder.map, style, fillId) } val zList = zSet.toList().sorted() @@ -108,11 +102,7 @@ internal fun MapboxMapViewController.ensurePolygonZLayers(style: com.mapbox.maps .get("fillColor"), ) } - try { - style.addLayerBelow(layer, anchorId) - } catch (_: Exception) { - style.addLayer(layer) - } + MapboxOverlayStacking.add(holder.map, style, layer, MapboxOverlayStacking.key(OverlayKind.Polygon, z)) } // Outline layer above its fill @@ -138,11 +128,8 @@ internal fun MapboxMapViewController.ensurePolygonZLayers(style: com.mapbox.maps .get("strokeWidth"), ) } - try { - style.addLayerAbove(layer, fillId) - } catch (_: Exception) { - style.addLayer(layer) - } + // 塗りの直後(同じ鍵なら後から入れたものが上)。 + MapboxOverlayStacking.add(holder.map, style, layer, MapboxOverlayStacking.key(OverlayKind.Polygon, z)) } } // Update tracked set diff --git a/src/main/java/com/mapconductor/mapbox/MapboxOverlayStacking.kt b/src/main/java/com/mapconductor/mapbox/MapboxOverlayStacking.kt new file mode 100644 index 0000000..34757f5 --- /dev/null +++ b/src/main/java/com/mapconductor/mapbox/MapboxOverlayStacking.kt @@ -0,0 +1,86 @@ +package com.mapconductor.mapbox + +import android.util.Log +import com.mapbox.maps.MapboxMap +import com.mapbox.maps.Style +import com.mapbox.maps.extension.style.layers.Layer +import com.mapbox.maps.extension.style.layers.addLayer +import com.mapbox.maps.extension.style.layers.addLayerBelow +import com.mapconductor.core.controller.OverlayKind +import com.mapconductor.core.controller.OverlayLayerStack +import com.mapconductor.core.controller.OverlayStacking +import java.util.WeakHashMap + +/** + * オーバーレイの層をスタイルのどこへ入れるか([OverlayStacking] の帯の順)。 + * + * 下から: ラスター < GroundImage < ポリゴン < サークル < ポリライン < マーカー + * (マーカーのタイルはマーカーの帯の一番下、ドラッグ中の層は一番上)。 + * + * 位置は毎回スタイルの今の並びから決めるので、追加した順・送り直し・ + * スタイルの読み直し(デザイン変更)に左右されない。 + * + * ラスターだけはベースマップのラベル(SDK の層でない最初の symbol 層)の下に入れる。 + * それ以外の帯はラベルの上に乗る(従来どおり)。Standard スタイルのように + * ベースマップが import で、その層が `styleLayers` に出てこない場合は、 + * ラスターも含めてベースマップ全体の上に乗る。 + */ +internal object MapboxOverlayStacking { + private val stacks = WeakHashMap() + + fun stackOf(map: MapboxMap): OverlayLayerStack = synchronized(stacks) { stacks.getOrPut(map) { OverlayLayerStack() } } + + fun key( + kind: OverlayKind, + zIndex: Int = 0, + ) = OverlayStacking.Key(kind, zIndex) + + /** マーカーのタイル(ラスター): マーカーの帯の一番下。 */ + val MARKER_TILE = key(OverlayKind.Marker, Int.MIN_VALUE) + + /** ドラッグ中のマーカーの層: マーカーの帯の一番上。 */ + val MARKER_DRAG = key(OverlayKind.Marker, Int.MAX_VALUE) + + /** [layer] を [key] の位置へ足す。同じ id の層が既にあれば何もしない。 */ + fun add( + map: MapboxMap, + style: Style, + layer: Layer, + key: OverlayStacking.Key, + ) { + val stack = stackOf(map) + stack.put(layer.layerId, key) + if (style.styleLayerExists(layer.layerId)) return + val layers = style.styleLayers + val ids = layers.map { it.id } + var before = stack.beforeId(ids, key) + if (key.kind == OverlayKind.RasterLayer) { + val label = layers.firstOrNull { it.type == "symbol" && stack.keyOf(it.id) == null }?.id + if (label != null && (before == null || ids.indexOf(label) < ids.indexOf(before))) { + before = label + } + } + try { + if (before != null) style.addLayerBelow(layer, before) else style.addLayer(layer) + } catch (e: Exception) { + if (style.styleLayerExists(layer.layerId)) return + try { + style.addLayer(layer) + } catch (e2: Exception) { + Log.w("Mapbox", "Failed to add layer: ${layer.layerId} (${e2.message})") + } + } + } + + fun remove( + map: MapboxMap, + style: Style?, + layerId: String, + ) { + stackOf(map).remove(layerId) + try { + style?.removeStyleLayer(layerId) + } catch (_: Exception) { + } + } +} diff --git a/src/main/java/com/mapconductor/mapbox/groundimage/MapboxGroundImageOverlayRenderer.kt b/src/main/java/com/mapconductor/mapbox/groundimage/MapboxGroundImageOverlayRenderer.kt index 34b8679..2a5c186 100644 --- a/src/main/java/com/mapconductor/mapbox/groundimage/MapboxGroundImageOverlayRenderer.kt +++ b/src/main/java/com/mapconductor/mapbox/groundimage/MapboxGroundImageOverlayRenderer.kt @@ -3,13 +3,12 @@ package com.mapconductor.mapbox.groundimage import com.mapbox.bindgen.DataRef import com.mapbox.maps.Image import com.mapbox.maps.Style -import com.mapbox.maps.extension.style.layers.addLayer -import com.mapbox.maps.extension.style.layers.addLayerBelow import com.mapbox.maps.extension.style.layers.generated.rasterLayer import com.mapbox.maps.extension.style.layers.properties.generated.Visibility import com.mapbox.maps.extension.style.sources.addSource import com.mapbox.maps.extension.style.sources.generated.imageSource import com.mapbox.maps.extension.style.utils.TypeUtils +import com.mapconductor.core.controller.OverlayKind import com.mapconductor.core.features.GeoRectBounds import com.mapconductor.core.groundimage.AbstractGroundImageOverlayRenderer import com.mapconductor.core.groundimage.GroundImageEntityInterface @@ -17,6 +16,7 @@ import com.mapconductor.core.groundimage.GroundImageFingerPrint import com.mapconductor.core.groundimage.GroundImageState import com.mapconductor.mapbox.MapboxActualGroundImage import com.mapconductor.mapbox.MapboxMapViewHolder +import com.mapconductor.mapbox.MapboxOverlayStacking import java.nio.ByteBuffer import android.annotation.SuppressLint import android.graphics.Bitmap @@ -134,25 +134,15 @@ class MapboxGroundImageOverlayRenderer( Log.w("Mapbox", "Failed to add ground image source: ${e.message}") } - try { - style.addLayerBelow(layer, BELOW_LAYER_ID) - } catch (_: Exception) { - try { - style.addLayer(layer) - } catch (e: Exception) { - Log.w("Mapbox", "Failed to add ground image layer: ${e.message}") - } - } + // GroundImage の帯(ラスターの上、ポリゴンの下)。送り直しても同じ位置。 + MapboxOverlayStacking.add(holder.map, style, layer, MapboxOverlayStacking.key(OverlayKind.GroundImage)) } private fun removeSourceAndLayerIfExists( style: Style, handle: MapboxGroundImageHandle, ) { - try { - style.removeStyleLayer(handle.layerId) - } catch (_: Exception) { - } + MapboxOverlayStacking.remove(holder.map, style, handle.layerId) try { style.removeStyleSource(handle.sourceId) } catch (_: Exception) { @@ -224,7 +214,6 @@ class MapboxGroundImageOverlayRenderer( opacity = opacity, ) - companion object { - private const val BELOW_LAYER_ID = "polyline-layer" - } + // 公開 API(Companion)の互換のために残している。 + companion object } diff --git a/src/main/java/com/mapconductor/mapbox/raster/MapboxRasterLayerOverlayRenderer.kt b/src/main/java/com/mapconductor/mapbox/raster/MapboxRasterLayerOverlayRenderer.kt index 54c73a1..9c36fdc 100644 --- a/src/main/java/com/mapconductor/mapbox/raster/MapboxRasterLayerOverlayRenderer.kt +++ b/src/main/java/com/mapconductor/mapbox/raster/MapboxRasterLayerOverlayRenderer.kt @@ -1,11 +1,9 @@ package com.mapconductor.mapbox.raster -import com.mapbox.maps.extension.style.layers.addLayer -import com.mapbox.maps.extension.style.layers.addLayerAbove -import com.mapbox.maps.extension.style.layers.addLayerBelow import com.mapbox.maps.extension.style.layers.generated.rasterLayer import com.mapbox.maps.extension.style.sources.addSource import com.mapbox.maps.extension.style.sources.generated.rasterSource +import com.mapconductor.core.controller.OverlayKind import com.mapconductor.core.raster.RasterHeaderRuleSet import com.mapconductor.core.raster.RasterLayerEntityInterface import com.mapconductor.core.raster.RasterLayerOverlayRendererInterface @@ -13,6 +11,7 @@ import com.mapconductor.core.raster.RasterLayerSource import com.mapconductor.core.raster.RasterLayerState import com.mapconductor.core.raster.TileScheme import com.mapconductor.mapbox.MapboxMapViewHolder +import com.mapconductor.mapbox.MapboxOverlayStacking import android.util.Log import kotlinx.coroutines.CoroutineScope import kotlinx.coroutines.Dispatchers @@ -99,15 +98,7 @@ class MapboxRasterLayerOverlayRenderer( } catch (e: Exception) { Log.w("Mapbox", "Failed to add raster source: ${e.message}") } - try { - if (isMarkerTileRaster(state)) { - addLayerForMarkerTile(style, layer) - } else { - addBelowBasemapLabels(style, layer) - } - } catch (e: Exception) { - Log.w("Mapbox", "Failed to add raster layer: ${e.message}") - } + addToStack(style, layer, state) return handle } @@ -116,10 +107,7 @@ class MapboxRasterLayerOverlayRenderer( state: RasterLayerState, ) { val style = holder.map.style ?: return - try { - style.removeStyleLayer(handle.layerId) - } catch (_: Exception) { - } + MapboxOverlayStacking.remove(holder.map, style, handle.layerId) val opacity = if (state.visible) { state.opacity.coerceIn(0.0f, 1.0f).toDouble() @@ -130,23 +118,13 @@ class MapboxRasterLayerOverlayRenderer( rasterLayer(handle.layerId, handle.sourceId) { rasterOpacity(opacity) } - try { - if (isMarkerTileRaster(state)) { - addLayerForMarkerTile(style, layer) - } else { - addBelowBasemapLabels(style, layer) - } - } catch (_: Exception) { - } + addToStack(style, layer, state) } private fun removeLayer(entity: RasterLayerEntityInterface) { val style = holder.map.style ?: return val handle = entity.layer - try { - style.removeStyleLayer(handle.layerId) - } catch (_: Exception) { - } + MapboxOverlayStacking.remove(holder.map, style, handle.layerId) try { style.removeStyleSource(handle.sourceId) } catch (_: Exception) { @@ -182,26 +160,22 @@ class MapboxRasterLayerOverlayRenderer( } } - private fun addLayerForMarkerTile( + /** + * 帯の順([MapboxOverlayStacking])で足す。アプリのラスターはラスターの帯 + * (一番下、ベースマップのラベルの下)、マーカーのタイルはマーカーの帯の一番下。 + */ + private fun addToStack( style: com.mapbox.maps.Style, layer: com.mapbox.maps.extension.style.layers.Layer, + state: RasterLayerState, ) { - // Insert the raster tiles below the marker symbol layer so they don't cover markers, - // but remain above vector overlays that are anchored below markers (polyline/circle/etc). - try { - style.addLayerBelow(layer, MARKERS_LAYER_ID) - return - } catch (_: Exception) { - } - - // Best-effort fallback: place above polylines if marker layer isn't present yet. - try { - style.addLayerAbove(layer, POLYLINE_LAYER_ID) - return - } catch (_: Exception) { - } - - style.addLayer(layer) + val key = + if (isMarkerTileRaster(state)) { + MapboxOverlayStacking.MARKER_TILE + } else { + MapboxOverlayStacking.key(OverlayKind.RasterLayer, state.zIndex) + } + MapboxOverlayStacking.add(holder.map, style, layer, key) } private fun rebuildNonMarkerRasterLayers(style: com.mapbox.maps.Style) { @@ -215,10 +189,7 @@ class MapboxRasterLayerOverlayRenderer( // Remove and re-add non-marker raster layers in zIndex order to ensure deterministic stacking. ordered.forEach { (_, handle) -> - try { - style.removeStyleLayer(handle.layerId) - } catch (_: Exception) { - } + MapboxOverlayStacking.remove(holder.map, style, handle.layerId) } ordered.forEach { (state, handle) -> @@ -232,59 +203,12 @@ class MapboxRasterLayerOverlayRenderer( rasterLayer(handle.layerId, handle.sourceId) { rasterOpacity(opacity) } - try { - addBelowBasemapLabels(style, layer) - } catch (_: Exception) { - } + addToStack(style, layer, state) } } - /** - * Puts a raster layer above the basemap's geometry but **below its labels**. - * - * Appended at the top of the style instead, a raster overlay covers the - * place names, road names and shields the backend draws -- a vector tile - * layer's own roads run straight through them, which is what "the labels - * are under the lines" looks like. Every raster overlay we add has the - * same problem, so the rule lives here rather than in each of them. - * - * Inserting successive layers below the same anchor keeps their order: - * each one lands directly below the anchor, which is directly above the - * one inserted before it. - * - * Anything of ours is skipped when looking for the anchor. Markers are a - * symbol layer too, and anchoring to them would put the raster back above - * the labels -- with the added twist of only doing so once a marker exists. - */ - private fun addBelowBasemapLabels( - style: com.mapbox.maps.Style, - layer: com.mapbox.maps.extension.style.layers.Layer, - ) { - val anchor = - style.styleLayers.firstOrNull { it.type == "symbol" && !isOursById(it.id) } - if (anchor == null) { - style.addLayer(layer) - return - } - try { - style.addLayerBelow(layer, anchor.id) - } catch (_: Exception) { - style.addLayer(layer) - } - } - - /** True for layers this SDK adds, as opposed to the design's own. */ - private fun isOursById(id: String): Boolean = - id.startsWith("raster-layer-") || - id.startsWith(MARKERS_LAYER_ID) || - id.startsWith("marker-drag-layer") || - id.startsWith(POLYLINE_LAYER_ID) || - id.startsWith("circle-layer") - private companion object { private const val MARKER_TILE_RASTER_ID_PREFIX = "marker-tile-" - private const val MARKERS_LAYER_ID = "markers-layer" - private const val POLYLINE_LAYER_ID = "polyline-layer" } }