From e772e01050e971e1266ed710a5067debaa3a6f8d Mon Sep 17 00:00:00 2001 From: Masashi Katsumata Date: Sat, 7 Nov 2026 22:16:11 -0800 Subject: [PATCH] Clear StrategyMarkerController in the same order as the other controllers clear() only called strategy.clear(), which empties the manager. It never called onRemove or onPostProcess, unlike AbstractMarkerController and the five shape controllers. The GL renderers (MapLibre, Mapbox, Mappls, OMM) leave the map alone in onRemove and redraw the manager in onPostProcess, so markers drawn through a strategy stayed on the map after clear(), and renderer-side resources were never released. clear() now runs onRemove(everything), strategy.clear() (so the strategy's own reset, such as MarkerClusterStrategy's cluster state, is kept), then onPostProcess, inside the same permit as add/update. Markers queued before the first camera are dropped as well; they would otherwise be drawn on the first camera event after clear(). Nothing in the SDK calls this clear() today: clearOverlays() clears the providers' own controllers, not the strategy controllers that MarkerRenderingGroup registers. StrategyMarkerClearOrderTest fails on the old clear() (markers still drawn; queued markers drawn after clear) and passes now. Same test in js-sdk-core and ios-sdk-core. --- .../core/marker/StrategyMarkerController.kt | 20 ++- .../marker/StrategyMarkerClearOrderTest.kt | 138 ++++++++++++++++++ 2 files changed, 157 insertions(+), 1 deletion(-) create mode 100644 src/test/java/com/mapconductor/core/marker/StrategyMarkerClearOrderTest.kt diff --git a/src/main/java/com/mapconductor/core/marker/StrategyMarkerController.kt b/src/main/java/com/mapconductor/core/marker/StrategyMarkerController.kt index a52c6a7..e9ad09d 100644 --- a/src/main/java/com/mapconductor/core/marker/StrategyMarkerController.kt +++ b/src/main/java/com/mapconductor/core/marker/StrategyMarkerController.kt @@ -105,8 +105,26 @@ class StrategyMarkerController( } } + /** + * 他の 5 種類・[AbstractMarkerController] の clear() と同じく + * 「onRemove(全部) → 空にする → onPostProcess」の順に呼ぶ。 + * + * 以前は strategy.clear() で manager を空にするだけだった。GL 系の描画担当は + * onRemove では地図に触らず onPostProcess で manager から描き直すので、 + * 呼ばないと clear() の後もマーカーが地図に残る。onRemove はアイコン等の + * 描画側の資源を返すため。空にするのは strategy.clear() のままにして、 + * ストラテジ固有の後片付け(クラスタの状態など)を保つ。 + * add / update と同じ permit の中で行い、ストラテジの処理と交錯させない。 + */ override suspend fun clear() { - strategy.clear() + semaphore.withPermit { + val entities = markerManager.allEntities() + if (entities.isNotEmpty()) renderer.onRemove(entities) + strategy.clear() + // 最初のカメラ待ちで保留していたものも消す。残すと次のカメラで描かれてしまう。 + pendingStates = null + renderer.onPostProcess() + } } override fun getEntity(id: String): MarkerEntityInterface? = strategy.markerManager.getEntity(id) diff --git a/src/test/java/com/mapconductor/core/marker/StrategyMarkerClearOrderTest.kt b/src/test/java/com/mapconductor/core/marker/StrategyMarkerClearOrderTest.kt new file mode 100644 index 0000000..027c594 --- /dev/null +++ b/src/test/java/com/mapconductor/core/marker/StrategyMarkerClearOrderTest.kt @@ -0,0 +1,138 @@ +package com.mapconductor.core.marker + +import com.mapconductor.core.features.GeoPoint +import com.mapconductor.core.features.GeoRectBounds +import com.mapconductor.core.map.MapCameraPosition +import com.mapconductor.core.map.MapViewHolderInterface +import com.mapconductor.core.map.VisibleRegion +import kotlinx.coroutines.test.runTest +import org.junit.Assert.assertEquals +import org.junit.Test + +/** + * [StrategyMarkerController.clear] は他のコントローラの clear() と同じく + * 「onRemove(全部) → 空にする → onPostProcess」で、何も描かれていない状態で終わること。 + * + * 以前は strategy.clear() で manager を空にするだけで、onRemove も onPostProcess も + * 呼ばなかった。GL 系の描画担当は onPostProcess で manager から描き直すので、 + * clear() の後もマーカーが地図に残る。js-sdk-core / ios-sdk-core の同名テストと同じ。 + */ +class StrategyMarkerClearOrderTest { + private val bounds = GeoRectBounds(GeoPoint(34.0, 138.0), GeoPoint(36.0, 140.0)) + private val camera = + MapCameraPosition( + position = GeoPoint(35.0, 139.0), + zoom = 10.0, + visibleRegion = VisibleRegion(bounds, null, null, null, null), + ) + + /** GL 系の描画担当: onRemove では何もせず、onPostProcess で manager の全部を描き直す。 */ + private class GlRenderer( + private val manager: MarkerManager, + ) : MarkerOverlayRendererInterface { + var drawn: List = emptyList() + val removed = mutableListOf() + override var animateStartListener: OnMarkerEventHandler? = null + override var animateEndListener: OnMarkerEventHandler? = null + override val holder: MapViewHolderInterface<*, *> + get() = throw UnsupportedOperationException("not used") + + override suspend fun onAdd(data: List) = data.map { it.state.id } + + override suspend fun onChange(data: List>) = + data.map { it.current.state.id } + + override suspend fun onRemove(data: List>) { + removed += data.map { it.state.id } + } + + override suspend fun onAnimate(entity: MarkerEntityInterface) = Unit + + override suspend fun onPostProcess() { + drawn = manager.allEntities().map { it.state.id }.sorted() + } + } + + /** + * 渡されたマーカーを全部描くストラテジ。既定アイコン(AbstractMarkerRenderingStrategy が持つ)は + * Android の Bitmap を焼くので素の JVM では使えない。描いた状態は manager へ直接登録する。 + */ + private class DrawAllStrategy : MarkerRenderingStrategyInterface { + override val markerManager: MarkerManager = MarkerManager.defaultManager() + var cleared = 0 + + override fun clear() { + cleared++ + markerManager.clear() + } + + override suspend fun onUpdate( + state: MarkerState, + viewport: GeoRectBounds, + renderer: MarkerOverlayRendererInterface, + ): Boolean = true + + override suspend fun onAdd( + data: List, + viewport: GeoRectBounds, + renderer: MarkerOverlayRendererInterface, + ): Boolean { + data.forEach { markerManager.registerEntity(MarkerEntity(marker = it.id, state = it, isRendered = true)) } + renderer.onPostProcess() + return true + } + + override suspend fun onCameraChanged( + cameraPosition: MapCameraPosition, + renderer: MarkerOverlayRendererInterface, + ) = Unit + } + + private fun markers() = + listOf( + MarkerState(position = GeoPoint(35.0, 139.0), id = "m1"), + MarkerState(position = GeoPoint(35.1, 139.1), id = "m2"), + ) + + @Test + fun `clear 後は何も描かれず、ストラテジ自身の後片付けも走る`() = + runTest { + val strategy = DrawAllStrategy() + val renderer = GlRenderer(strategy.markerManager) + val controller = StrategyMarkerController(strategy, renderer) + controller.onCameraChanged(camera) + controller.add(markers()) + assertEquals(listOf("m1", "m2"), renderer.drawn) + + controller.clear() + + assertEquals(listOf("m1", "m2"), renderer.removed.sorted()) + assertEquals(1, strategy.cleared) + assertEquals(emptyList(), renderer.drawn) + } + + @Test + fun `最初のカメラ待ちのマーカーも clear で捨てる`() = + runTest { + val strategy = DrawAllStrategy() + val renderer = GlRenderer(strategy.markerManager) + val controller = StrategyMarkerController(strategy, renderer) + controller.add(markers()) // カメラ前なので保留 + controller.clear() + controller.onCameraChanged(camera) + assertEquals(emptyList(), renderer.drawn) + } + + @Test + fun `clear 後に送り直せばまた描かれる`() = + runTest { + val strategy = DrawAllStrategy() + val renderer = GlRenderer(strategy.markerManager) + val controller = StrategyMarkerController(strategy, renderer) + controller.onCameraChanged(camera) + controller.add(markers()) + controller.clear() + controller.add(markers()) + assertEquals(listOf("m1", "m2"), renderer.drawn) + } +}