From e739dd51f6f4495421ec9746c96b478db6de254d Mon Sep 17 00:00:00 2001 From: Masashi Katsumata Date: Sun, 8 Nov 2026 00:41:33 -0800 Subject: [PATCH 1/2] Remove style layers and sources by object so they can come back Overlays removed from a MapTiler map - a polygon, circle, raster layer, ground image or polyline taken out of the composition, or everything after clearOverlays() - did not come back when added again with the same id. MTStyle remembers every source and layer it added, by id, and refuses to add the same id again (SourceAlreadyExists / LayerAlreadyExists, which the renderers caught and logged). removeSourceById / removeLayerById only remove them from the page and leave that record behind; removeSource / removeLayer clear it too. Remove by object everywhere the renderers take a source or layer out, falling back to the id when MTStyle has no record of it (its add records asynchronously). The same applies to the background layer the vector-style mutation target adds and removes. A raster layer whose source changes is removed and re-added under the same ids, so that path was broken too. MapTilerStyleRemovalTest drives a real MTStyle through a recording command executor: removing by id leaves the id recorded; removing by object lets the same id be added again. --- .../maptiler/MapTilerStyleMutationTarget.kt | 9 ++- .../maptiler/MapTilerStyleRemoval.kt | 31 ++++++++ .../circle/MapTilerCircleOverlayRenderer.kt | 9 ++- .../MapTilerGroundImageOverlayRenderer.kt | 7 +- .../polygon/MapTilerPolygonOverlayRenderer.kt | 9 ++- .../MapTilerPolylineOverlayRenderer.kt | 7 +- .../MapTilerRasterLayerOverlayRenderer.kt | 8 +- .../maptiler/MapTilerStyleRemovalTest.kt | 73 +++++++++++++++++++ 8 files changed, 138 insertions(+), 15 deletions(-) create mode 100644 src/main/java/com/mapconductor/maptiler/MapTilerStyleRemoval.kt create mode 100644 src/test/java/com/mapconductor/maptiler/MapTilerStyleRemovalTest.kt diff --git a/src/main/java/com/mapconductor/maptiler/MapTilerStyleMutationTarget.kt b/src/main/java/com/mapconductor/maptiler/MapTilerStyleMutationTarget.kt index 06b6d51..31713fc 100644 --- a/src/main/java/com/mapconductor/maptiler/MapTilerStyleMutationTarget.kt +++ b/src/main/java/com/mapconductor/maptiler/MapTilerStyleMutationTarget.kt @@ -118,9 +118,12 @@ internal class MapTilerStyleMutationTarget( override fun removeLayer(layerId: String) { val target = style() ?: return - // `removeLayerById` reaches the page without being checked against - // anything, and removing an absent layer there is a no-op. - target.removeLayerById(layerId) + // Remove it from MTStyle's record too: `removeLayerById` alone leaves + // the id recorded, and adding the same background layer again throws + // LayerAlreadyExists. A layer MTStyle never recorded falls back to + // `removeLayerById`, which reaches the page unchecked (a no-op when the + // layer is absent). See MapTilerStyleRemoval.kt. + target.removeLayerAndForget(MTBackgroundLayer(layerId)) own(target) -= layerId } diff --git a/src/main/java/com/mapconductor/maptiler/MapTilerStyleRemoval.kt b/src/main/java/com/mapconductor/maptiler/MapTilerStyleRemoval.kt new file mode 100644 index 0000000..b5c26a3 --- /dev/null +++ b/src/main/java/com/mapconductor/maptiler/MapTilerStyleRemoval.kt @@ -0,0 +1,31 @@ +package com.mapconductor.maptiler + +import com.mapconductor.core.InternalMapConductorApi +import com.maptiler.maptilersdk.map.style.MTStyle +import com.maptiler.maptilersdk.map.style.layer.MTLayer +import com.maptiler.maptilersdk.map.style.source.MTSource + +/* + * MTStyle からレイヤ・ソースを外すときは、id ではなくオブジェクトで外す。 + * + * MTStyle は addLayer / addSource したものを id で覚えていて、同じ id をもう一度足そうとすると + * LayerAlreadyExists / SourceAlreadyExists を投げる。ところが removeLayerById / removeSourceById は + * ページ(MapLibre GL JS)から消すだけで、この記録を消さない。id で外したポリゴン・円・ラスター等を + * 同じ id(= 同じ state.id)で戻すと add が投げて、地図に戻らなかった。 + * removeLayer(layer) / removeSource(source) は記録も消してからページへ渡す。 + * + * 記録に無いとき(add の記録は SDK のコルーチンで遅れて入る)は NotFound を投げるので、 + * そのときは id でページから消す。 + */ + +@InternalMapConductorApi +internal fun MTStyle.removeLayerAndForget(layer: MTLayer) { + runCatching { removeLayer(layer) } + .onFailure { runCatching { removeLayerById(layer.identifier) } } +} + +@InternalMapConductorApi +internal fun MTStyle.removeSourceAndForget(source: MTSource) { + runCatching { removeSource(source) } + .onFailure { runCatching { removeSourceById(source.identifier) } } +} diff --git a/src/main/java/com/mapconductor/maptiler/circle/MapTilerCircleOverlayRenderer.kt b/src/main/java/com/mapconductor/maptiler/circle/MapTilerCircleOverlayRenderer.kt index b3d2db3..9949115 100644 --- a/src/main/java/com/mapconductor/maptiler/circle/MapTilerCircleOverlayRenderer.kt +++ b/src/main/java/com/mapconductor/maptiler/circle/MapTilerCircleOverlayRenderer.kt @@ -6,6 +6,8 @@ import com.mapconductor.core.circle.CircleOverlayRendererInterface import com.mapconductor.core.circle.CircleState import com.mapconductor.core.geometry.OverlayGeoJson import com.mapconductor.core.geometry.circleToRing +import com.mapconductor.maptiler.removeLayerAndForget +import com.mapconductor.maptiler.removeSourceAndForget import com.maptiler.maptilersdk.map.MTMapViewController import com.maptiler.maptilersdk.map.style.dsl.PropertyValue import com.maptiler.maptilersdk.map.style.layer.MTLayerVisibility @@ -141,9 +143,10 @@ class MapTilerCircleOverlayRenderer( private fun removeCircle(entity: CircleEntityInterface) { val style = mtController.style ?: return val handle = entity.circle - runCatching { style.removeLayerById(handle.lineLayerId) } - runCatching { style.removeLayerById(handle.fillLayerId) } - runCatching { style.removeSourceById(handle.sourceId) } + // id で外すと MTStyle の記録が残り、同じ id で戻せなくなる(MapTilerStyleRemoval.kt)。 + style.removeLayerAndForget(handle.lineLayer) + style.removeLayerAndForget(handle.fillLayer) + style.removeSourceAndForget(handle.source) } /** diff --git a/src/main/java/com/mapconductor/maptiler/groundimage/MapTilerGroundImageOverlayRenderer.kt b/src/main/java/com/mapconductor/maptiler/groundimage/MapTilerGroundImageOverlayRenderer.kt index 6df3d29..6764135 100644 --- a/src/main/java/com/mapconductor/maptiler/groundimage/MapTilerGroundImageOverlayRenderer.kt +++ b/src/main/java/com/mapconductor/maptiler/groundimage/MapTilerGroundImageOverlayRenderer.kt @@ -4,6 +4,8 @@ import com.mapconductor.core.features.GeoRectBounds import com.mapconductor.core.groundimage.GroundImageEntityInterface import com.mapconductor.core.groundimage.GroundImageOverlayRendererInterface import com.mapconductor.core.groundimage.GroundImageState +import com.mapconductor.maptiler.removeLayerAndForget +import com.mapconductor.maptiler.removeSourceAndForget import com.maptiler.maptilersdk.map.LngLat import com.maptiler.maptilersdk.map.MTMapViewController import com.maptiler.maptilersdk.map.style.dsl.PropertyValue @@ -132,8 +134,9 @@ class MapTilerGroundImageOverlayRenderer( private fun removeGroundImage(entity: GroundImageEntityInterface) { val style = mtController.style ?: return val handle = entity.groundImage - runCatching { style.removeLayerById(handle.layerId) } - runCatching { style.removeSourceById(handle.sourceId) } + // id で外すと MTStyle の記録が残り、同じ id で戻せなくなる(MapTilerStyleRemoval.kt)。 + style.removeLayerAndForget(handle.layer) + style.removeSourceAndForget(handle.source) } /** diff --git a/src/main/java/com/mapconductor/maptiler/polygon/MapTilerPolygonOverlayRenderer.kt b/src/main/java/com/mapconductor/maptiler/polygon/MapTilerPolygonOverlayRenderer.kt index 73435dd..5f7f499 100644 --- a/src/main/java/com/mapconductor/maptiler/polygon/MapTilerPolygonOverlayRenderer.kt +++ b/src/main/java/com/mapconductor/maptiler/polygon/MapTilerPolygonOverlayRenderer.kt @@ -7,6 +7,8 @@ import com.mapconductor.core.polygon.PolygonEntityInterface import com.mapconductor.core.polygon.PolygonOverlayRendererInterface import com.mapconductor.core.polygon.PolygonState import com.mapconductor.core.polygon.unionHoles +import com.mapconductor.maptiler.removeLayerAndForget +import com.mapconductor.maptiler.removeSourceAndForget import com.maptiler.maptilersdk.map.MTMapViewController import com.maptiler.maptilersdk.map.style.dsl.PropertyValue import com.maptiler.maptilersdk.map.style.layer.MTLayerVisibility @@ -164,9 +166,10 @@ class MapTilerPolygonOverlayRenderer( private fun removePolygon(entity: PolygonEntityInterface) { val style = mtController.style ?: return val handle = entity.polygon - runCatching { style.removeLayerById(handle.lineLayerId) } - runCatching { style.removeLayerById(handle.fillLayerId) } - runCatching { style.removeSourceById(handle.sourceId) } + // id で外すと MTStyle の記録が残り、同じ id で戻せなくなる(MapTilerStyleRemoval.kt)。 + style.removeLayerAndForget(handle.lineLayer) + style.removeLayerAndForget(handle.fillLayer) + style.removeSourceAndForget(handle.source) } /** diff --git a/src/main/java/com/mapconductor/maptiler/polyline/MapTilerPolylineOverlayRenderer.kt b/src/main/java/com/mapconductor/maptiler/polyline/MapTilerPolylineOverlayRenderer.kt index f4e226b..667549a 100644 --- a/src/main/java/com/mapconductor/maptiler/polyline/MapTilerPolylineOverlayRenderer.kt +++ b/src/main/java/com/mapconductor/maptiler/polyline/MapTilerPolylineOverlayRenderer.kt @@ -6,6 +6,8 @@ import com.mapconductor.core.geometry.buildUnwrappedPolylinePath import com.mapconductor.core.polyline.PolylineEntityInterface import com.mapconductor.core.polyline.PolylineOverlayRendererInterface import com.mapconductor.core.polyline.PolylineState +import com.mapconductor.maptiler.removeLayerAndForget +import com.mapconductor.maptiler.removeSourceAndForget import com.maptiler.maptilersdk.map.MTMapViewController import com.maptiler.maptilersdk.map.style.dsl.PropertyValue import com.maptiler.maptilersdk.map.style.layer.MTLayerVisibility @@ -141,8 +143,9 @@ class MapTilerPolylineOverlayRenderer( private fun removeLine(entity: PolylineEntityInterface) { val style = mtController.style ?: return val handle = entity.polyline - runCatching { style.removeLayerById(handle.layerId) } - runCatching { style.removeSourceById(handle.sourceId) } + // id で外すと MTStyle の記録が残り、同じ id で戻せなくなる(MapTilerStyleRemoval.kt)。 + style.removeLayerAndForget(handle.layer) + style.removeSourceAndForget(handle.source) } /** diff --git a/src/main/java/com/mapconductor/maptiler/raster/MapTilerRasterLayerOverlayRenderer.kt b/src/main/java/com/mapconductor/maptiler/raster/MapTilerRasterLayerOverlayRenderer.kt index a21306f..42e734e 100644 --- a/src/main/java/com/mapconductor/maptiler/raster/MapTilerRasterLayerOverlayRenderer.kt +++ b/src/main/java/com/mapconductor/maptiler/raster/MapTilerRasterLayerOverlayRenderer.kt @@ -6,6 +6,8 @@ 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.maptiler.removeLayerAndForget +import com.mapconductor.maptiler.removeSourceAndForget import com.maptiler.maptilersdk.map.MTMapViewController import com.maptiler.maptilersdk.map.style.MTTileScheme import com.maptiler.maptilersdk.map.style.dsl.PropertyValue @@ -121,8 +123,10 @@ class MapTilerRasterLayerOverlayRenderer( private fun removeLayer(entity: RasterLayerEntityInterface) { val style = mtController.style ?: return val handle = entity.layer - runCatching { style.removeLayerById(handle.layerId) } - runCatching { style.removeSourceById(handle.sourceId) } + // id で外すと MTStyle の記録が残り、同じ id で戻せなくなる(MapTilerStyleRemoval.kt)。 + // ソースを替えるとき(onChange)も同じ id で足し直すので、ここで記録まで消す。 + style.removeLayerAndForget(handle.layer) + style.removeSourceAndForget(handle.source) } private fun buildSource( diff --git a/src/test/java/com/mapconductor/maptiler/MapTilerStyleRemovalTest.kt b/src/test/java/com/mapconductor/maptiler/MapTilerStyleRemovalTest.kt new file mode 100644 index 0000000..b4ec7c5 --- /dev/null +++ b/src/test/java/com/mapconductor/maptiler/MapTilerStyleRemovalTest.kt @@ -0,0 +1,73 @@ +package com.mapconductor.maptiler + +import com.mapconductor.core.InternalMapConductorApi +import com.maptiler.maptilersdk.bridge.MTBridge +import com.maptiler.maptilersdk.bridge.MTBridgeReturnType +import com.maptiler.maptilersdk.bridge.MTCommand +import com.maptiler.maptilersdk.bridge.MTCommandExecutable +import com.maptiler.maptilersdk.map.style.MTMapReferenceStyle +import com.maptiler.maptilersdk.map.style.MTStyle +import com.maptiler.maptilersdk.map.style.MTStyleError +import com.maptiler.maptilersdk.map.style.source.MTGeoJSONSource +import kotlinx.coroutines.CoroutineScope +import kotlinx.coroutines.Dispatchers +import org.junit.Assert.assertEquals +import org.junit.Assert.assertThrows +import org.junit.Assert.assertTrue +import org.junit.Before +import org.junit.Test + +/** + * 外したソースを同じ id で足し直せること(ポリゴン・円・ラスター等が「一度外すと戻らない」不具合)。 + * + * 実物の [MTStyle] に、ページ(WebView)の代わりにコマンドを記録するだけの実行器をつないで確かめる。 + */ +@OptIn(InternalMapConductorApi::class) +class MapTilerStyleRemovalTest { + private val commands = mutableListOf() + private lateinit var style: MTStyle + + @Before + fun setUp() { + val executor = + object : MTCommandExecutable { + override suspend fun execute(command: MTCommand): MTBridgeReturnType { + commands += command.javaClass.simpleName + return MTBridgeReturnType.BoolValue(true) + } + } + style = MTStyle(MTMapReferenceStyle.STREETS) + // initWorker は SDK の internal。地図の ready で SDK 自身が呼ぶものを、ここでは反射で呼ぶ。 + MTStyle::class.java + .declaredMethods + .single { it.name.startsWith("initWorker") } + .apply { isAccessible = true } + .invoke(style, MTBridge(executor), CoroutineScope(Dispatchers.Unconfined)) + } + + private fun source() = MTGeoJSONSource.fromJsonString("polygon-source-PG", """{"type":"FeatureCollection","features":[]}""") + + /** 不具合の原因: id で外しても MTStyle の記録が残り、同じ id の add が投げる。 */ + @Test + fun removeSourceById_leavesTheIdRecorded() { + style.addSource(source()) + style.removeSourceById("polygon-source-PG") + assertThrows(MTStyleError.SourceAlreadyExists::class.java) { style.addSource(source()) } + } + + @Test + fun removeSourceAndForget_letsTheSameIdBeAddedAgain() { + val first = source() + style.addSource(first) + style.removeSourceAndForget(first) + style.addSource(source()) + assertEquals(listOf("AddSource", "RemoveSource", "AddSource"), commands.filter { it.contains("Source") }) + } + + /** 記録に無いもの(足していない・記録が遅れている)は id でページから消す。 */ + @Test + fun removeSourceAndForget_fallsBackToTheIdWhenNotRecorded() { + style.removeSourceAndForget(source()) + assertTrue(commands.toString(), commands.contains("RemoveSourceById")) + } +} From 8a7cecbfffb87f0820b4a6e6b65d10f92e5a6fc2 Mon Sep 17 00:00:00 2001 From: Masashi Katsumata Date: Sat, 7 Nov 2026 23:55:27 -0800 Subject: [PATCH 2/2] Stack overlays by band with moveLayer MTStyle.addLayer has no beforeId and always appends, so overlays were stacked in add order, and after a style reload in the reapply order (raster, polyline, polygon, circle, ground image). MapTilerOverlayStacking keeps the SDK layers' order per style and moves each new layer below the lowest SDK layer with a higher key. Reapply now runs in band order after resetting the record. The SDK cannot list the basemap's layers, so rasters sit above the basemap including its labels. MTStyle.addLayer does not reach the page right away: for anything but an image source it first waits for an isSourceLoaded round trip, and if the source is not loaded yet it holds the layer until the next idle. A moveLayer sent right after addLayer then arrives before the layer and does nothing (adding in reverse order left the raster on top, and a zIndex 0 polygon above a zIndex 1 one). So after an add, the recorded order is sent again on the next two idle events (onIdle, from onEventTriggered; launched once on the main dispatcher so it lands after the SDK flushes its queue). 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-maptiler.api | 13 +++ .../mapconductor/maptiler/MapTilerMapView.kt | 13 ++- .../maptiler/MapTilerOverlayStacking.kt | 108 ++++++++++++++++++ .../circle/MapTilerCircleOverlayRenderer.kt | 20 +++- .../MapTilerGroundImageOverlayRenderer.kt | 14 ++- .../polygon/MapTilerPolygonOverlayRenderer.kt | 20 +++- .../MapTilerPolylineOverlayRenderer.kt | 14 ++- .../MapTilerRasterLayerOverlayRenderer.kt | 19 ++- 8 files changed, 205 insertions(+), 16 deletions(-) create mode 100644 src/main/java/com/mapconductor/maptiler/MapTilerOverlayStacking.kt diff --git a/api/android-for-maptiler.api b/api/android-for-maptiler.api index 090f6a9..8cac437 100644 --- a/api/android-for-maptiler.api +++ b/api/android-for-maptiler.api @@ -165,6 +165,19 @@ final class com.mapconductor.maptiler.MapTilerMapViewSaver$Companion { public final class com.mapconductor.maptiler.MapTilerMapViewScope extends com.mapconductor.compose.MapViewScope { public com.mapconductor.maptiler.MapTilerMapViewScope(); } +public final class com.mapconductor.maptiler.MapTilerOverlayStacking { + public static final com.mapconductor.maptiler.MapTilerOverlayStacking INSTANCE; + 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 void add(com.maptiler.maptilersdk.map.style.MTStyle, com.maptiler.maptilersdk.map.style.layer.MTLayer, com.mapconductor.core.controller.OverlayStacking$Key); + public final void remove(com.maptiler.maptilersdk.map.style.MTStyle, java.lang.String); + public final void onIdle(com.maptiler.maptilersdk.map.style.MTStyle); + public final void reset(com.maptiler.maptilersdk.map.style.MTStyle); +} +final class com.mapconductor.maptiler.MapTilerOverlayStacking$Entry { + public final java.lang.String getId(); + public final com.mapconductor.core.controller.OverlayStacking$Key getKey(); +} public final class com.mapconductor.maptiler.MapTilerProjectedAnnotationKt { public static final void MapTilerProjectedAnnotation(com.maptiler.maptilersdk.map.MTMapViewController, com.maptiler.maptilersdk.map.LngLat, com.maptiler.maptilersdk.map.types.MTPoint, androidx.compose.ui.Alignment, androidx.compose.ui.Modifier, kotlin.jvm.functions.Function2, androidx.compose.runtime.Composer, int, int); } diff --git a/src/main/java/com/mapconductor/maptiler/MapTilerMapView.kt b/src/main/java/com/mapconductor/maptiler/MapTilerMapView.kt index 556e8fa..41b9924 100644 --- a/src/main/java/com/mapconductor/maptiler/MapTilerMapView.kt +++ b/src/main/java/com/mapconductor/maptiler/MapTilerMapView.kt @@ -316,11 +316,13 @@ fun MapTilerMapSurface( // 非 Compose ホストが `setMapDesignType` を直接呼んだ場合は // 調整が落ちたままになる。 mutations.onStyleLoaded() + // 重なり順の帯の順(下から)に足し直す。 + mtController.style?.let { MapTilerOverlayStacking.reset(it) } controller.reapplyRasterLayers() - controller.reapplyPolylines() + controller.reapplyGroundImages() controller.reapplyPolygons() controller.reapplyCircles() - controller.reapplyGroundImages() + controller.reapplyPolylines() controller.mapLoaded.value = true controller.dispatchInitialCameraToOverlays() onLoaded?.invoke(currentState) @@ -383,6 +385,13 @@ fun MapTilerMapSurface( onMoveEnd?.invoke(pos) } + MTEvent.ON_IDLE -> + // SDK はこのあとで待たせていた層を送る。1 度メインへ戻してから + // 並びを送り直す(MapTilerOverlayStacking.onIdle)。 + mtController.style?.let { style -> + coroutineScope.launch { MapTilerOverlayStacking.onIdle(style) } + } + else -> Unit } } diff --git a/src/main/java/com/mapconductor/maptiler/MapTilerOverlayStacking.kt b/src/main/java/com/mapconductor/maptiler/MapTilerOverlayStacking.kt new file mode 100644 index 0000000..41d5fe1 --- /dev/null +++ b/src/main/java/com/mapconductor/maptiler/MapTilerOverlayStacking.kt @@ -0,0 +1,108 @@ +package com.mapconductor.maptiler + +import com.mapconductor.core.controller.OverlayKind +import com.mapconductor.core.controller.OverlayStacking +import com.maptiler.maptilersdk.map.style.MTStyle +import com.maptiler.maptilersdk.map.style.layer.MTLayer +import java.util.WeakHashMap + +/** + * 重なり順([OverlayStacking]: ラスター < GroundImage < ポリゴン < サークル < ポリライン < マーカー)。 + * + * MapTiler SDK の `MTStyle.addLayer` は beforeId を取らず、常に一番上へ積む。並びを読む + * API も無い。そこで SDK が足した層の並びをここで持ち、足した直後に `moveLayer` で + * 「鍵が自分より大きい一番下の層」の下へ動かす。ベースマップの層は見えないので、 + * ラスターもベースマップ全体(ラベルを含む)の上に乗る(GL 系の他プロバイダはラベルの下)。 + * + * `MTStyle.addLayer` はすぐにはページへ届かない。画像ソース以外は `isSourceLoaded` の + * 往復を待ち、ソースが読み込み前なら次の `idle` まで SDK の中で待たされる。その前に + * `moveLayer` が届くと、まだ無い層を動かそうとして何も起きない(逆の順に足すとラスターが + * 一番上に残る)。そこで足したあとの `idle` で、記録した並びをもう一度ページへ送る + * ([onIdle]、MapTilerMapView の onEventTriggered)。 + * + * スタイルを読み直すと層は消える。[reset] で「今スタイルにある層」を空にしてから + * 帯の順に足し直す(MapTilerMapView の onMapViewInitialized)。 + */ +internal object MapTilerOverlayStacking { + private class Entry( + val id: String, + val key: OverlayStacking.Key, + ) + + /** スタイルにある SDK の層(下から上)。 */ + private val stacks = WeakHashMap>() + + /** 並びを送り直す残りの `idle` の回数。足すたびに [RESTACK_IDLES] へ戻す。 */ + private val pendingIdles = WeakHashMap() + + /** + * 足したあと何回の `idle` で並びを送り直すか。1 回目の `idle` で SDK が待たせていた層を + * ページへ送る。送り直し自体が描き直しを呼ぶので、2 回目で遅れて届いた層も拾って止まる。 + */ + private const val RESTACK_IDLES = 2 + + fun key( + kind: OverlayKind, + zIndex: Int = 0, + ) = OverlayStacking.Key(kind, zIndex) + + /** マーカーのタイル: マーカーの帯の一番下。 */ + val MARKER_TILE = key(OverlayKind.Marker, Int.MIN_VALUE) + + fun add( + style: MTStyle, + layer: MTLayer, + key: OverlayStacking.Key, + ) { + val id = layer.identifier + val before = + synchronized(stacks) { + val stack = stacks.getOrPut(style) { mutableListOf() } + stack.removeAll { it.id == id } + val b = OverlayStacking.insertBefore(stack, key) { it.key } + val at = if (b == null) stack.size else stack.indexOf(b) + stack.add(at, Entry(id, key)) + pendingIdles[style] = RESTACK_IDLES + b?.id + } + style.addLayer(layer) + if (before != null) runCatching { style.moveLayer(id, before) } + } + + fun remove( + style: MTStyle, + layerId: String, + ) { + synchronized(stacks) { stacks[style]?.removeAll { it.id == layerId } } + } + + /** + * 地図が `idle` になった。足したばかりの層があれば、記録した並び(下から上)を + * 上から順に `moveLayer` で送り直す。まだページに無い層の `moveLayer` は何もしない。 + * + * SDK は `idle` を委譲先へ渡した**あと**で、待たせていた層を送る。ここをそのまま + * 呼ぶと `moveLayer` が層より先に届くので、呼び出し側は 1 度メインスレッドへ戻して + * から呼ぶこと。 + */ + fun onIdle(style: MTStyle) { + val ids = + synchronized(stacks) { + val left = pendingIdles[style] ?: return + if (left <= 1) pendingIdles.remove(style) else pendingIdles[style] = left - 1 + stacks[style]?.map { it.id }.orEmpty() + } + if (ids.isEmpty()) return + runCatching { style.moveLayer(ids.last()) } + for (i in ids.size - 2 downTo 0) { + runCatching { style.moveLayer(ids[i], ids[i + 1]) } + } + } + + /** スタイルを読み直した(SDK の層が全部消えた)。 */ + fun reset(style: MTStyle) { + synchronized(stacks) { + stacks[style]?.clear() + pendingIdles.remove(style) + } + } +} diff --git a/src/main/java/com/mapconductor/maptiler/circle/MapTilerCircleOverlayRenderer.kt b/src/main/java/com/mapconductor/maptiler/circle/MapTilerCircleOverlayRenderer.kt index 9949115..c53364e 100644 --- a/src/main/java/com/mapconductor/maptiler/circle/MapTilerCircleOverlayRenderer.kt +++ b/src/main/java/com/mapconductor/maptiler/circle/MapTilerCircleOverlayRenderer.kt @@ -4,8 +4,10 @@ import androidx.compose.ui.graphics.toArgb import com.mapconductor.core.circle.CircleEntityInterface import com.mapconductor.core.circle.CircleOverlayRendererInterface import com.mapconductor.core.circle.CircleState +import com.mapconductor.core.controller.OverlayKind import com.mapconductor.core.geometry.OverlayGeoJson import com.mapconductor.core.geometry.circleToRing +import com.mapconductor.maptiler.MapTilerOverlayStacking import com.mapconductor.maptiler.removeLayerAndForget import com.mapconductor.maptiler.removeSourceAndForget import com.maptiler.maptilersdk.map.MTMapViewController @@ -68,16 +70,24 @@ class MapTilerCircleOverlayRenderer( val style = mtController.style ?: return handles.forEach { handle -> runCatching { style.addSource(handle.source) } - runCatching { style.addLayer(handle.fillLayer) } - runCatching { style.addLayer(handle.lineLayer) } + runCatching { MapTilerOverlayStacking.add(style, handle.fillLayer, stackKey(handle.fillLayer.identifier)) } + runCatching { MapTilerOverlayStacking.add(style, handle.lineLayer, stackKey(handle.lineLayer.identifier)) } } } + /** 層の id → zIndex(帯の中の順)。handle は公開型なので持たせずここで覚える。 */ + private val zIndexByLayer = java.util.concurrent.ConcurrentHashMap() + + /** サークルの帯。同じ鍵なら後から足したもの(輪郭)が上。 */ + private fun stackKey(layerId: String) = MapTilerOverlayStacking.key(OverlayKind.Circle, zIndexByLayer[layerId] ?: 0) + private fun addCircle(state: CircleState): MapTilerCircleHandle? { val json = buildGeoJson(state) ?: return null val sourceId = "circle-source-${state.id}" val fillLayerId = "circle-fill-${state.id}" val lineLayerId = "circle-line-${state.id}" + zIndexByLayer[fillLayerId] = state.zIndex ?: 0 + zIndexByLayer[lineLayerId] = state.zIndex ?: 0 val source = MTGeoJSONSource.fromJsonString(sourceId, json) val fillLayer = @@ -101,9 +111,9 @@ class MapTilerCircleOverlayRenderer( val style = mtController.style ?: return handle runCatching { style.addSource(source) } .onFailure { Log.w(TAG, "addSource failed: ${it.message}") } - runCatching { style.addLayer(fillLayer) } + runCatching { MapTilerOverlayStacking.add(style, fillLayer, stackKey(fillLayer.identifier)) } .onFailure { Log.w(TAG, "addLayer(fill) failed: ${it.message}") } - runCatching { style.addLayer(lineLayer) } + runCatching { MapTilerOverlayStacking.add(style, lineLayer, stackKey(lineLayer.identifier)) } .onFailure { Log.w(TAG, "addLayer(line) failed: ${it.message}") } return handle } @@ -144,7 +154,9 @@ class MapTilerCircleOverlayRenderer( val style = mtController.style ?: return val handle = entity.circle // id で外すと MTStyle の記録が残り、同じ id で戻せなくなる(MapTilerStyleRemoval.kt)。 + MapTilerOverlayStacking.remove(style, handle.lineLayer.identifier) style.removeLayerAndForget(handle.lineLayer) + MapTilerOverlayStacking.remove(style, handle.fillLayer.identifier) style.removeLayerAndForget(handle.fillLayer) style.removeSourceAndForget(handle.source) } diff --git a/src/main/java/com/mapconductor/maptiler/groundimage/MapTilerGroundImageOverlayRenderer.kt b/src/main/java/com/mapconductor/maptiler/groundimage/MapTilerGroundImageOverlayRenderer.kt index 6764135..3eadab9 100644 --- a/src/main/java/com/mapconductor/maptiler/groundimage/MapTilerGroundImageOverlayRenderer.kt +++ b/src/main/java/com/mapconductor/maptiler/groundimage/MapTilerGroundImageOverlayRenderer.kt @@ -1,9 +1,11 @@ package com.mapconductor.maptiler.groundimage +import com.mapconductor.core.controller.OverlayKind import com.mapconductor.core.features.GeoRectBounds import com.mapconductor.core.groundimage.GroundImageEntityInterface import com.mapconductor.core.groundimage.GroundImageOverlayRendererInterface import com.mapconductor.core.groundimage.GroundImageState +import com.mapconductor.maptiler.MapTilerOverlayStacking import com.mapconductor.maptiler.removeLayerAndForget import com.mapconductor.maptiler.removeSourceAndForget import com.maptiler.maptilersdk.map.LngLat @@ -58,14 +60,21 @@ class MapTilerGroundImageOverlayRenderer( val style = mtController.style ?: return handles.forEach { handle -> runCatching { style.addSource(handle.source) } - runCatching { style.addLayer(handle.layer) } + runCatching { MapTilerOverlayStacking.add(style, handle.layer, stackKey(handle.layer.identifier)) } } } + /** 層の id → zIndex(帯の中の順)。handle は公開型なので持たせずここで覚える。 */ + private val zIndexByLayer = java.util.concurrent.ConcurrentHashMap() + + /** GroundImage の帯(ラスターの上、ポリゴンの下)。 */ + private fun stackKey(layerId: String) = MapTilerOverlayStacking.key(OverlayKind.GroundImage, zIndexByLayer[layerId] ?: 0) + private fun addGroundImage(state: GroundImageState): MapTilerGroundImageHandle? { val coordinates = state.bounds.toImageCoordinates() ?: return null val sourceId = "groundimage-source-${state.id}" val layerId = "groundimage-layer-${state.id}" + zIndexByLayer[layerId] = 0 val source = MTImageSource(sourceId, imageUrl(state.image), coordinates) val layer = MTRasterLayer(layerId, sourceId).apply { @@ -87,7 +96,7 @@ class MapTilerGroundImageOverlayRenderer( val style = mtController.style ?: return handle runCatching { style.addSource(source) } .onFailure { Log.w(TAG, "addSource failed: ${it.message}") } - runCatching { style.addLayer(layer) } + runCatching { MapTilerOverlayStacking.add(style, layer, stackKey(layer.identifier)) } .onFailure { Log.w(TAG, "addLayer failed: ${it.message}") } return handle } @@ -135,6 +144,7 @@ class MapTilerGroundImageOverlayRenderer( val style = mtController.style ?: return val handle = entity.groundImage // id で外すと MTStyle の記録が残り、同じ id で戻せなくなる(MapTilerStyleRemoval.kt)。 + MapTilerOverlayStacking.remove(style, handle.layer.identifier) style.removeLayerAndForget(handle.layer) style.removeSourceAndForget(handle.source) } diff --git a/src/main/java/com/mapconductor/maptiler/polygon/MapTilerPolygonOverlayRenderer.kt b/src/main/java/com/mapconductor/maptiler/polygon/MapTilerPolygonOverlayRenderer.kt index 5f7f499..012b403 100644 --- a/src/main/java/com/mapconductor/maptiler/polygon/MapTilerPolygonOverlayRenderer.kt +++ b/src/main/java/com/mapconductor/maptiler/polygon/MapTilerPolygonOverlayRenderer.kt @@ -1,12 +1,14 @@ package com.mapconductor.maptiler.polygon import androidx.compose.ui.graphics.toArgb +import com.mapconductor.core.controller.OverlayKind import com.mapconductor.core.geometry.OverlayGeoJson import com.mapconductor.core.geometry.buildUnwrappedPolygonRings import com.mapconductor.core.polygon.PolygonEntityInterface import com.mapconductor.core.polygon.PolygonOverlayRendererInterface import com.mapconductor.core.polygon.PolygonState import com.mapconductor.core.polygon.unionHoles +import com.mapconductor.maptiler.MapTilerOverlayStacking import com.mapconductor.maptiler.removeLayerAndForget import com.mapconductor.maptiler.removeSourceAndForget import com.maptiler.maptilersdk.map.MTMapViewController @@ -73,16 +75,24 @@ class MapTilerPolygonOverlayRenderer( val style = mtController.style ?: return handles.forEach { handle -> runCatching { style.addSource(handle.source) } - runCatching { style.addLayer(handle.fillLayer) } - runCatching { style.addLayer(handle.lineLayer) } + runCatching { MapTilerOverlayStacking.add(style, handle.fillLayer, stackKey(handle.fillLayer.identifier)) } + runCatching { MapTilerOverlayStacking.add(style, handle.lineLayer, stackKey(handle.lineLayer.identifier)) } } } + /** 層の id → zIndex(帯の中の順)。handle は公開型なので持たせずここで覚える。 */ + private val zIndexByLayer = java.util.concurrent.ConcurrentHashMap() + + /** ポリゴンの帯。同じ鍵なら後から足したもの(輪郭)が上。 */ + private fun stackKey(layerId: String) = MapTilerOverlayStacking.key(OverlayKind.Polygon, zIndexByLayer[layerId] ?: 0) + private fun addPolygon(state: PolygonState): MapTilerPolygonHandle? { val json = buildGeoJson(state) ?: return null val sourceId = "polygon-source-${state.id}" val fillLayerId = "polygon-fill-${state.id}" val lineLayerId = "polygon-line-${state.id}" + zIndexByLayer[fillLayerId] = state.zIndex + zIndexByLayer[lineLayerId] = state.zIndex val source = MTGeoJSONSource.fromJsonString(sourceId, json) val fillLayer = @@ -107,9 +117,9 @@ class MapTilerPolygonOverlayRenderer( // ソース → 塗り → 輪郭の順に追加(輪郭を塗りの上に重ねる)。 runCatching { style.addSource(source) } .onFailure { Log.w(TAG, "addSource failed: ${it.message}") } - runCatching { style.addLayer(fillLayer) } + runCatching { MapTilerOverlayStacking.add(style, fillLayer, stackKey(fillLayer.identifier)) } .onFailure { Log.w(TAG, "addLayer(fill) failed: ${it.message}") } - runCatching { style.addLayer(lineLayer) } + runCatching { MapTilerOverlayStacking.add(style, lineLayer, stackKey(lineLayer.identifier)) } .onFailure { Log.w(TAG, "addLayer(line) failed: ${it.message}") } return handle } @@ -167,7 +177,9 @@ class MapTilerPolygonOverlayRenderer( val style = mtController.style ?: return val handle = entity.polygon // id で外すと MTStyle の記録が残り、同じ id で戻せなくなる(MapTilerStyleRemoval.kt)。 + MapTilerOverlayStacking.remove(style, handle.lineLayer.identifier) style.removeLayerAndForget(handle.lineLayer) + MapTilerOverlayStacking.remove(style, handle.fillLayer.identifier) style.removeLayerAndForget(handle.fillLayer) style.removeSourceAndForget(handle.source) } diff --git a/src/main/java/com/mapconductor/maptiler/polyline/MapTilerPolylineOverlayRenderer.kt b/src/main/java/com/mapconductor/maptiler/polyline/MapTilerPolylineOverlayRenderer.kt index 667549a..a7049c2 100644 --- a/src/main/java/com/mapconductor/maptiler/polyline/MapTilerPolylineOverlayRenderer.kt +++ b/src/main/java/com/mapconductor/maptiler/polyline/MapTilerPolylineOverlayRenderer.kt @@ -1,11 +1,13 @@ package com.mapconductor.maptiler.polyline import androidx.compose.ui.graphics.toArgb +import com.mapconductor.core.controller.OverlayKind import com.mapconductor.core.geometry.OverlayGeoJson import com.mapconductor.core.geometry.buildUnwrappedPolylinePath import com.mapconductor.core.polyline.PolylineEntityInterface import com.mapconductor.core.polyline.PolylineOverlayRendererInterface import com.mapconductor.core.polyline.PolylineState +import com.mapconductor.maptiler.MapTilerOverlayStacking import com.mapconductor.maptiler.removeLayerAndForget import com.mapconductor.maptiler.removeSourceAndForget import com.maptiler.maptilersdk.map.MTMapViewController @@ -67,14 +69,21 @@ class MapTilerPolylineOverlayRenderer( val style = mtController.style ?: return handles.forEach { handle -> runCatching { style.addSource(handle.source) } - runCatching { style.addLayer(handle.layer) } + runCatching { MapTilerOverlayStacking.add(style, handle.layer, stackKey(handle.layer.identifier)) } } } + /** 層の id → zIndex(帯の中の順)。handle は公開型なので持たせずここで覚える。 */ + private val zIndexByLayer = java.util.concurrent.ConcurrentHashMap() + + /** ポリラインの帯。同じ鍵なら後から足したもの(輪郭)が上。 */ + private fun stackKey(layerId: String) = MapTilerOverlayStacking.key(OverlayKind.Polyline, zIndexByLayer[layerId] ?: 0) + private fun addLine(state: PolylineState): MapTilerPolylineHandle? { val json = buildGeoJson(state) ?: return null val sourceId = "polyline-source-${state.id}" val layerId = "polyline-layer-${state.id}" + zIndexByLayer[layerId] = state.zIndex val source = MTGeoJSONSource.fromJsonString(sourceId, json) // 色の alpha は line-opacity で表現し、line-color は不透明 RGB とする(二重適用を避ける)。 val opaqueColor = state.strokeColor.toArgb() or ALPHA_MASK @@ -93,7 +102,7 @@ class MapTilerPolylineOverlayRenderer( val style = mtController.style ?: return handle runCatching { style.addSource(source) } .onFailure { Log.w(TAG, "addSource failed: ${it.message}") } - runCatching { style.addLayer(layer) } + runCatching { MapTilerOverlayStacking.add(style, layer, stackKey(layer.identifier)) } .onFailure { Log.w(TAG, "addLayer failed: ${it.message}") } return handle } @@ -144,6 +153,7 @@ class MapTilerPolylineOverlayRenderer( val style = mtController.style ?: return val handle = entity.polyline // id で外すと MTStyle の記録が残り、同じ id で戻せなくなる(MapTilerStyleRemoval.kt)。 + MapTilerOverlayStacking.remove(style, handle.layer.identifier) style.removeLayerAndForget(handle.layer) style.removeSourceAndForget(handle.source) } diff --git a/src/main/java/com/mapconductor/maptiler/raster/MapTilerRasterLayerOverlayRenderer.kt b/src/main/java/com/mapconductor/maptiler/raster/MapTilerRasterLayerOverlayRenderer.kt index 42e734e..41740a5 100644 --- a/src/main/java/com/mapconductor/maptiler/raster/MapTilerRasterLayerOverlayRenderer.kt +++ b/src/main/java/com/mapconductor/maptiler/raster/MapTilerRasterLayerOverlayRenderer.kt @@ -1,11 +1,13 @@ package com.mapconductor.maptiler.raster +import com.mapconductor.core.controller.OverlayKind import com.mapconductor.core.raster.RasterHeaderRuleSet 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.maptiler.MapTilerOverlayStacking import com.mapconductor.maptiler.removeLayerAndForget import com.mapconductor.maptiler.removeSourceAndForget import com.maptiler.maptilersdk.map.MTMapViewController @@ -74,13 +76,25 @@ class MapTilerRasterLayerOverlayRenderer( val style = mtController.style ?: return handles.forEach { handle -> runCatching { style.addSource(handle.source) } - runCatching { style.addLayer(handle.layer) } + runCatching { MapTilerOverlayStacking.add(style, handle.layer, stackKey(handle.layer.identifier)) } } } + /** 層の id → zIndex(帯の中の順)。handle は公開型なので持たせずここで覚える。 */ + private val zIndexByLayer = java.util.concurrent.ConcurrentHashMap() + + /** ラスターの帯(一番下)。マーカーのタイルはマーカーの帯の一番下。 */ + private fun stackKey(layerId: String) = + if (layerId.startsWith("raster-layer-marker-tile-")) { + MapTilerOverlayStacking.MARKER_TILE + } else { + MapTilerOverlayStacking.key(OverlayKind.RasterLayer, zIndexByLayer[layerId] ?: 0) + } + private fun addLayer(state: RasterLayerState): MapTilerRasterLayerHandle? { val sourceId = "raster-source-${state.id}" val layerId = "raster-layer-${state.id}" + zIndexByLayer[layerId] = state.zIndex val source = buildSource(sourceId, state.source) ?: return null val layer = @@ -99,7 +113,7 @@ class MapTilerRasterLayerOverlayRenderer( // ソース → レイヤの順に追加する。既存の場合の例外は無視する。 runCatching { style.addSource(source) } .onFailure { Log.w(TAG, "addSource failed: ${it.message}") } - runCatching { style.addLayer(layer) } + runCatching { MapTilerOverlayStacking.add(style, layer, stackKey(layer.identifier)) } .onFailure { Log.w(TAG, "addLayer failed: ${it.message}") } return handle } @@ -125,6 +139,7 @@ class MapTilerRasterLayerOverlayRenderer( val handle = entity.layer // id で外すと MTStyle の記録が残り、同じ id で戻せなくなる(MapTilerStyleRemoval.kt)。 // ソースを替えるとき(onChange)も同じ id で足し直すので、ここで記録まで消す。 + MapTilerOverlayStacking.remove(style, handle.layer.identifier) style.removeLayerAndForget(handle.layer) style.removeSourceAndForget(handle.source) }