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/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..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,12 @@ 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 import com.maptiler.maptilersdk.map.style.dsl.PropertyValue import com.maptiler.maptilersdk.map.style.layer.MTLayerVisibility @@ -66,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 = @@ -99,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 } @@ -141,9 +153,12 @@ 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)。 + 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 6df3d29..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,13 @@ 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 import com.maptiler.maptilersdk.map.MTMapViewController import com.maptiler.maptilersdk.map.style.dsl.PropertyValue @@ -56,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 { @@ -85,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 } @@ -132,8 +143,10 @@ 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)。 + 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 73435dd..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,16 @@ 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 import com.maptiler.maptilersdk.map.style.dsl.PropertyValue import com.maptiler.maptilersdk.map.style.layer.MTLayerVisibility @@ -71,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 = @@ -105,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 } @@ -164,9 +176,12 @@ 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)。 + 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 f4e226b..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,15 @@ 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 import com.maptiler.maptilersdk.map.style.dsl.PropertyValue import com.maptiler.maptilersdk.map.style.layer.MTLayerVisibility @@ -65,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 @@ -91,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 } @@ -141,8 +152,10 @@ 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)。 + 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 a21306f..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,15 @@ 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 import com.maptiler.maptilersdk.map.style.MTTileScheme import com.maptiler.maptilersdk.map.style.dsl.PropertyValue @@ -72,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 = @@ -97,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 } @@ -121,8 +137,11 @@ 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 で足し直すので、ここで記録まで消す。 + MapTilerOverlayStacking.remove(style, handle.layer.identifier) + 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")) + } +}