From 53d00f31c01052f2a3dfbfff144a501297f194f1 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 were added first (bottom), polygon z-layers were inserted below the polyline layer (so above circles), ground images went directly below the polyline layer (above polygons, and above every polygon added earlier), and a raster added after the style was set went to the very top until the next rebuild put it under the labels. MapplsOverlayStacking records the band and zIndex of every layer the SDK adds and inserts each one below the lowest SDK layer with a higher key, reading the style's current layer list every time. Rasters additionally stay below the basemap's first label layer (as before). Marker tiles go to the bottom of the marker band, the drag layer to the top. 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-mappls.api | 9 ++ .../mappls/MapplsOverlayStacking.kt | 86 ++++++++++++++ .../mappls/MapplsViewController.kt | 17 +-- .../mappls/MapplsViewControllerStyle.kt | 97 +++++----------- .../MapplsGroundImageOverlayRenderer.kt | 28 ++--- .../MapplsRasterLayerOverlayRenderer.kt | 107 ++++-------------- 6 files changed, 161 insertions(+), 183 deletions(-) create mode 100644 src/main/java/com/mapconductor/mappls/MapplsOverlayStacking.kt diff --git a/api/android-for-mappls.api b/api/android-for-mappls.api index 8fe3338..c052daa 100644 --- a/api/android-for-mappls.api +++ b/api/android-for-mappls.api @@ -108,6 +108,15 @@ public final class com.mapconductor.mappls.MapplsMapViewSaver extends com.mapcon public final class com.mapconductor.mappls.MapplsMapViewScope extends com.mapconductor.compose.MapViewScope { public com.mapconductor.mappls.MapplsMapViewScope(); } +public final class com.mapconductor.mappls.MapplsOverlayStacking { + public static final com.mapconductor.mappls.MapplsOverlayStacking INSTANCE; + public final com.mapconductor.core.controller.OverlayLayerStack stackOf(com.mappls.sdk.maps.MapplsMap); + 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.mappls.sdk.maps.MapplsMap, com.mappls.sdk.maps.Style, com.mappls.sdk.maps.style.layers.Layer, com.mapconductor.core.controller.OverlayStacking$Key); + public final void remove(com.mappls.sdk.maps.MapplsMap, com.mappls.sdk.maps.Style, java.lang.String); +} public final class com.mapconductor.mappls.MapplsPolyUtilsKt { public static final java.util.List createMapplsLines-rXlOB7A(java.lang.String, java.util.List, boolean, long, float, int); public static final java.lang.String toMapplsColorString-8_81llA(long); diff --git a/src/main/java/com/mapconductor/mappls/MapplsOverlayStacking.kt b/src/main/java/com/mapconductor/mappls/MapplsOverlayStacking.kt new file mode 100644 index 0000000..047d039 --- /dev/null +++ b/src/main/java/com/mapconductor/mappls/MapplsOverlayStacking.kt @@ -0,0 +1,86 @@ +package com.mapconductor.mappls + +import android.util.Log +import com.mapconductor.core.controller.OverlayKind +import com.mapconductor.core.controller.OverlayLayerStack +import com.mapconductor.core.controller.OverlayStacking +import com.mappls.sdk.maps.MapplsMap +import com.mappls.sdk.maps.Style +import com.mappls.sdk.maps.style.layers.Layer +import com.mappls.sdk.maps.style.layers.SymbolLayer +import java.util.WeakHashMap + +/** + * オーバーレイの層をスタイルのどこへ入れるか([OverlayStacking] の帯の順)。 + * + * 下から: ラスター < GroundImage < ポリゴン < サークル < ポリライン < マーカー + * (マーカーのタイルはマーカーの帯の一番下、ドラッグ中の層は一番上)。 + * + * 位置は毎回スタイルの今の並びから決めるので、追加した順・送り直し・ + * スタイルの読み直し(デザイン変更)に左右されない。 + * + * ラスターだけはベースマップのラベル(SDK の層でない最初の symbol 層)の下に入れる。 + * それ以外の帯はラベルの上に乗る(従来どおり)。 + */ +internal object MapplsOverlayStacking { + private val stacks = WeakHashMap() + + fun stackOf(map: MapplsMap): 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 の層が既にあれば何もしない + * (位置を変えたいときは先に [remove] する)。 + */ + fun add( + map: MapplsMap, + style: Style, + layer: Layer, + key: OverlayStacking.Key, + ) { + val stack = stackOf(map) + stack.put(layer.id, key) + if (style.getLayer(layer.id) != null) return + val layers = style.layers + val ids = layers.map { it.id } + var before = stack.beforeId(ids, key) + if (key.kind == OverlayKind.RasterLayer) { + val label = layers.firstOrNull { it is SymbolLayer && 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.getLayer(layer.id) != null) return + try { + style.addLayer(layer) + } catch (e2: Exception) { + Log.w("Mappls", "Failed to add layer: ${layer.id} (${e2.message})") + } + } + } + + fun remove( + map: MapplsMap, + style: Style?, + layerId: String, + ) { + stackOf(map).remove(layerId) + try { + style?.removeLayer(layerId) + } catch (_: Exception) { + } + } +} diff --git a/src/main/java/com/mapconductor/mappls/MapplsViewController.kt b/src/main/java/com/mapconductor/mappls/MapplsViewController.kt index c7791b2..f05951c 100644 --- a/src/main/java/com/mapconductor/mappls/MapplsViewController.kt +++ b/src/main/java/com/mapconductor/mappls/MapplsViewController.kt @@ -337,19 +337,14 @@ class MapplsViewController( styleInstance?.let { style -> renderer.ensureDefaultIcon(style) ensureGeoJsonSource(style, renderer.markerLayer.sourceId) - addLayerAboveSafely( - style = style, - layer = renderer.markerLayer.layer, - layerId = renderer.markerLayer.layerId, - aboveId = polylineController.renderer.layer.layerId, + MapplsOverlayStacking.add( + holder.map, + style, + renderer.markerLayer.layer, + MapplsOverlayStacking.key(com.mapconductor.core.controller.OverlayKind.Marker), ) ensureGeoJsonSource(style, renderer.dragLayer.sourceId) - addLayerAboveSafely( - style = style, - layer = renderer.dragLayer.layer, - layerId = renderer.dragLayer.layerId, - aboveId = renderer.markerLayer.layerId, - ) + MapplsOverlayStacking.add(holder.map, style, renderer.dragLayer.layer, MapplsOverlayStacking.MARKER_DRAG) renderer.redraw() renderer.drawDragLayer() } diff --git a/src/main/java/com/mapconductor/mappls/MapplsViewControllerStyle.kt b/src/main/java/com/mapconductor/mappls/MapplsViewControllerStyle.kt index 74b8d03..177c03f 100644 --- a/src/main/java/com/mapconductor/mappls/MapplsViewControllerStyle.kt +++ b/src/main/java/com/mapconductor/mappls/MapplsViewControllerStyle.kt @@ -1,5 +1,6 @@ package com.mapconductor.mappls +import com.mapconductor.core.controller.OverlayKind import com.mapconductor.mappls.marker.MapplsMarkerOverlayRenderer import com.mappls.sdk.maps.Style import com.mappls.sdk.maps.style.expressions.Expression @@ -17,7 +18,9 @@ import kotlinx.coroutines.launch * * Mappls はスタイルが読み込み直されるたびにレイヤーが全部消えるため、ここは * 何度呼ばれても同じ状態になるように書いてある(`ensureGeoJsonSource` / - * `addLayerSafely` はいずれも既存があれば作らない)。 + * [MapplsOverlayStacking.add] はいずれも既存があれば作らない)。層の位置は + * [MapplsOverlayStacking] が帯の順(ラスター < GroundImage < ポリゴン < サークル < + * ポリライン < マーカー)で決める。 * * ポリゴンの z 順は**レイヤーを z 値ごとに用意して**表現する。Mappls には * フィーチャー単位の z 指定が無く、レイヤーの並び順だけが順序を決めるため。 @@ -34,6 +37,7 @@ internal fun MapplsViewController.ensureGeoJsonSource( } } +// 層の位置は MapplsOverlayStacking が決める。以下 2 つは公開 API の互換のために残している(SDK 内では未使用)。 internal fun MapplsViewController.addLayerSafely( style: Style, layer: Layer, @@ -84,51 +88,28 @@ internal fun MapplsViewController.setupStyle(style: Style) { ensureGeoJsonSource(style, polygonController.renderer.outline.layer.sourceId) ensureGeoJsonSource(style, polygonController.renderer.fill.layer.sourceId) - // Circle cts as anchor above polygons + // 層の位置は MapplsOverlayStacking が帯の順で決める(追加の順に依らない)。 + val stacking = MapplsOverlayStacking + val map = holder.map + val markerRenderer = markerController.renderer as MapplsMarkerOverlayRenderer + ensureGeoJsonSource(style, circleController.renderer.layer.sourceId) - addLayerSafely( - style = style, - layer = circleController.renderer.layer.layer, - layerId = circleController.renderer.layer.layerId, - ) - // Circle stroke (LineLayer) directly above the circle fill - addLayerAboveSafely( - style = style, - layer = circleController.renderer.layer.strokeLayer, - layerId = circleController.renderer.layer.strokeLayerId, - aboveId = circleController.renderer.layer.layerId, - ) + stacking.add(map, style, circleController.renderer.layer.layer, stacking.key(OverlayKind.Circle)) + // 線は塗りの直後(同じ鍵なら後から入れたものが上)。 + stacking.add(map, style, circleController.renderer.layer.strokeLayer, stacking.key(OverlayKind.Circle)) - // Polyline (general) acts as anchor above circles ensureGeoJsonSource(style, polylineController.renderer.layer.sourceId) - addLayerSafely( - style = style, - layer = polylineController.renderer.layer.layer, - layerId = polylineController.renderer.layer.layerId, - ) + stacking.add(map, style, polylineController.renderer.layer.layer, stacking.key(OverlayKind.Polyline)) - // Add z-indexed polygon layers below general polylines ensurePolygonZLayers(style) - // Marker - add source and layer at the top - ensureGeoJsonSource(style, (markerController.renderer as MapplsMarkerOverlayRenderer).markerLayer.sourceId) - addLayerAboveSafely( - style = style, - layer = (markerController.renderer as MapplsMarkerOverlayRenderer).markerLayer.layer, - layerId = (markerController.renderer as MapplsMarkerOverlayRenderer).markerLayer.layerId, - aboveId = polylineController.renderer.layer.layerId, - ) - (markerController.renderer as MapplsMarkerOverlayRenderer).redraw() + ensureGeoJsonSource(style, markerRenderer.markerLayer.sourceId) + stacking.add(map, style, markerRenderer.markerLayer.layer, stacking.key(OverlayKind.Marker)) + markerRenderer.redraw() - // Drag layer above marker layer - ensureGeoJsonSource(style, (markerController.renderer as MapplsMarkerOverlayRenderer).dragLayer.sourceId) - addLayerAboveSafely( - style = style, - layer = (markerController.renderer as MapplsMarkerOverlayRenderer).dragLayer.layer, - layerId = (markerController.renderer as MapplsMarkerOverlayRenderer).dragLayer.layerId, - aboveId = (markerController.renderer as MapplsMarkerOverlayRenderer).markerLayer.layerId, - ) - (markerController.renderer as MapplsMarkerOverlayRenderer).redraw() + ensureGeoJsonSource(style, markerRenderer.dragLayer.sourceId) + stacking.add(map, style, markerRenderer.dragLayer.layer, stacking.MARKER_DRAG) + markerRenderer.redraw() markerEventControllers .map { it.renderer as MapplsMarkerOverlayRenderer } @@ -136,19 +117,9 @@ internal fun MapplsViewController.setupStyle(style: Style) { .forEach { renderer -> renderer.ensureDefaultIcon(style) ensureGeoJsonSource(style, renderer.markerLayer.sourceId) - addLayerAboveSafely( - style = style, - layer = renderer.markerLayer.layer, - layerId = renderer.markerLayer.layerId, - aboveId = polylineController.renderer.layer.layerId, - ) + stacking.add(map, style, renderer.markerLayer.layer, stacking.key(OverlayKind.Marker)) ensureGeoJsonSource(style, renderer.dragLayer.sourceId) - addLayerAboveSafely( - style = style, - layer = renderer.dragLayer.layer, - layerId = renderer.dragLayer.layerId, - aboveId = renderer.markerLayer.layerId, - ) + stacking.add(map, style, renderer.dragLayer.layer, stacking.MARKER_DRAG) renderer.redraw() renderer.drawDragLayer() } @@ -167,7 +138,6 @@ internal fun MapplsViewController.setupStyle(style: Style) { internal fun MapplsViewController.ensurePolygonZLayers(style: Style) { val fillSourceId = polygonController.renderer.fill.layer.sourceId val outlineSourceId = polygonController.renderer.outline.layer.sourceId - val anchorId = polylineController.renderer.layer.layerId val zSet = polygonController.polygonManager @@ -180,14 +150,8 @@ internal fun MapplsViewController.ensurePolygonZLayers(style: Style) { toRemove.forEach { z -> val fillId = "polygon-fill-layer-$z" val outlineId = "polygon-outline-layer-$z" - try { - style.removeLayer(outlineId) - } catch (_: Exception) { - } - try { - style.removeLayer(fillId) - } catch (_: Exception) { - } + MapplsOverlayStacking.remove(holder.map, style, outlineId) + MapplsOverlayStacking.remove(holder.map, style, fillId) } val zList = zSet.toList().sorted() @@ -213,11 +177,7 @@ internal fun MapplsViewController.ensurePolygonZLayers(style: Style) { ), ) } - try { - style.addLayerBelow(fill, anchorId) - } catch (_: Exception) { - style.addLayer(fill) - } + MapplsOverlayStacking.add(holder.map, style, fill, MapplsOverlayStacking.key(OverlayKind.Polygon, z)) } if (style.getLayer(outlineId) == null) { @@ -246,11 +206,8 @@ internal fun MapplsViewController.ensurePolygonZLayers(style: Style) { ), ) } - try { - style.addLayerAbove(outline, fillId) - } catch (_: Exception) { - style.addLayer(outline) - } + // 塗りの直後(同じ鍵なら後から入れたものが上)。 + MapplsOverlayStacking.add(holder.map, style, outline, MapplsOverlayStacking.key(OverlayKind.Polygon, z)) } } polygonZLayers.clear() diff --git a/src/main/java/com/mapconductor/mappls/groundimage/MapplsGroundImageOverlayRenderer.kt b/src/main/java/com/mapconductor/mappls/groundimage/MapplsGroundImageOverlayRenderer.kt index 0dcc9eb..1b7059e 100644 --- a/src/main/java/com/mapconductor/mappls/groundimage/MapplsGroundImageOverlayRenderer.kt +++ b/src/main/java/com/mapconductor/mappls/groundimage/MapplsGroundImageOverlayRenderer.kt @@ -1,5 +1,6 @@ package com.mapconductor.mappls.groundimage +import com.mapconductor.core.controller.OverlayKind import com.mapconductor.core.features.GeoRectBounds import com.mapconductor.core.groundimage.AbstractGroundImageOverlayRenderer import com.mapconductor.core.groundimage.GroundImageEntityInterface @@ -7,6 +8,7 @@ import com.mapconductor.core.groundimage.GroundImageFingerPrint import com.mapconductor.core.groundimage.GroundImageState import com.mapconductor.mappls.MapplsActualGroundImage import com.mapconductor.mappls.MapplsMapViewHolderInterface +import com.mapconductor.mappls.MapplsOverlayStacking import com.mappls.sdk.maps.Style import com.mappls.sdk.maps.geometry.LatLng import com.mappls.sdk.maps.geometry.LatLngQuad @@ -104,25 +106,20 @@ class MapplsGroundImageOverlayRenderer( Log.w("Mappls", "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("Mappls", "Failed to add ground image layer: ${e.message}") - } - } + // GroundImage の帯(ラスターの上、ポリゴンの下)。送り直しても同じ位置。 + MapplsOverlayStacking.add( + holder.map, + style, + layer, + MapplsOverlayStacking.key(OverlayKind.GroundImage), + ) } private fun removeSourceAndLayerIfExists( style: Style, handle: MapplsGroundImageHandle, ) { - try { - style.removeLayer(handle.layerId) - } catch (_: Exception) { - } + MapplsOverlayStacking.remove(holder.map, style, handle.layerId) try { style.removeSource(handle.sourceId) } catch (_: Exception) { @@ -185,7 +182,6 @@ class MapplsGroundImageOverlayRenderer( opacity = opacity, ) - companion object { - private const val BELOW_LAYER_ID = "polyline-layer" - } + // 公開 API(Companion)の互換のために残している。 + companion object } diff --git a/src/main/java/com/mapconductor/mappls/raster/MapplsRasterLayerOverlayRenderer.kt b/src/main/java/com/mapconductor/mappls/raster/MapplsRasterLayerOverlayRenderer.kt index 60512e3..fc2c8ac 100644 --- a/src/main/java/com/mapconductor/mappls/raster/MapplsRasterLayerOverlayRenderer.kt +++ b/src/main/java/com/mapconductor/mappls/raster/MapplsRasterLayerOverlayRenderer.kt @@ -1,11 +1,13 @@ package com.mapconductor.mappls.raster +import com.mapconductor.core.controller.OverlayKind import com.mapconductor.core.raster.RasterLayerEntityInterface import com.mapconductor.core.raster.RasterLayerOverlayRendererInterface import com.mapconductor.core.raster.RasterLayerSource import com.mapconductor.core.raster.RasterLayerState import com.mapconductor.core.raster.TileScheme import com.mapconductor.mappls.MapplsMapViewHolderInterface +import com.mapconductor.mappls.MapplsOverlayStacking import com.mappls.sdk.maps.style.layers.Property import com.mappls.sdk.maps.style.layers.PropertyFactory import com.mappls.sdk.maps.style.layers.RasterLayer @@ -93,38 +95,26 @@ class MapplsRasterLayerOverlayRenderer( } catch (e: Exception) { Log.w("Mappls", "Failed to add raster source: ${e.message}") } - try { - if (isMarkerTileRaster(state)) { - addLayerForMarkerTile(style, layer) - } else { - addBelowBasemapLabels(style, layer) - } - } catch (e: Exception) { - Log.w("Mappls", "Failed to add raster layer: ${e.message}") - } + addToStack(style, layer, state) return handle } - private fun addLayerForMarkerTile( + /** + * 帯の順([MapplsOverlayStacking])で足す。アプリのラスターはラスターの帯 + * (一番下、ベースマップのラベルの下)、マーカーのタイルはマーカーの帯の一番下。 + */ + private fun addToStack( style: com.mappls.sdk.maps.Style, layer: RasterLayer, + 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)) { + MapplsOverlayStacking.MARKER_TILE + } else { + MapplsOverlayStacking.key(OverlayKind.RasterLayer, state.zIndex) + } + MapplsOverlayStacking.add(holder.map, style, layer, key) } private fun updateLayer( @@ -145,10 +135,7 @@ class MapplsRasterLayerOverlayRenderer( private fun removeLayer(entity: RasterLayerEntityInterface) { val style = holder.map.style ?: return val handle = entity.layer - try { - style.removeLayer(handle.layerId) - } catch (_: Exception) { - } + MapplsOverlayStacking.remove(holder.map, style, handle.layerId) try { style.removeSource(handle.sourceId) } catch (_: Exception) { @@ -164,11 +151,10 @@ class MapplsRasterLayerOverlayRenderer( .mapNotNull { state -> handleById[state.id]?.let { handle -> state to handle } } .toList() + // zIndex が変わったときのために、いったん外して zIndex の順に入れ直す + // (同じ zIndex なら渡された順)。位置は帯の順で決まる。 ordered.forEach { (_, handle) -> - try { - style.removeLayer(handle.layerId) - } catch (_: Exception) { - } + MapplsOverlayStacking.remove(holder.map, style, handle.layerId) } ordered.forEach { (state, handle) -> @@ -180,59 +166,10 @@ class MapplsRasterLayerOverlayRenderer( if (state.visible) Property.VISIBLE else Property.NONE, ), ) - try { - addBelowBasemapLabels(style, layer) - } catch (_: Exception) { - } - } - } - - /** - * 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.mappls.sdk.maps.Style, - layer: com.mappls.sdk.maps.style.layers.Layer, - ) { - val anchor = - style.layers.firstOrNull { - it is com.mappls.sdk.maps.style.layers.SymbolLayer && !isOursById(it.id) - } - if (anchor == null) { - style.addLayer(layer) - return - } - try { - style.addLayerBelow(layer, anchor.id) - } catch (_: Exception) { - style.addLayer(layer) + addToStack(style, layer, state) } } - /** 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("org.maplibre.annotations") || - id.startsWith("com.mappls.annotations") || - id.startsWith(MARKERS_LAYER_ID) || - id.startsWith("marker-drag-layer") || - id.startsWith(POLYLINE_LAYER_ID) || - id.startsWith("circle-layer") - private fun buildSource( sourceId: String, source: RasterLayerSource, @@ -260,8 +197,6 @@ class MapplsRasterLayerOverlayRenderer( 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" } }