From e739dd51f6f4495421ec9746c96b478db6de254d Mon Sep 17 00:00:00 2001 From: Masashi Katsumata Date: Sun, 8 Nov 2026 00:41:33 -0800 Subject: [PATCH] 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")) + } +}