From 51eef7ef065d94d394862037b7dfdb3689abd741 Mon Sep 17 00:00:00 2001 From: Masashi Katsumata Date: Sat, 7 Nov 2026 23:09:56 -0800 Subject: [PATCH] Let clearOverlays() clear the strategy marker controllers too Strategy marker controllers (clustering and other MarkerRenderingStrategy users) are not owned by a provider: extension modules such as android-marker-clustering's MarkerRenderingGroup create them and hand them to registerOverlayController. Every provider's clearOverlays() clears only its own fields, so markers drawn through a strategy stayed on the map. BaseMapViewController.clearStrategyMarkerControllers() clears every registered StrategyMarkerController (with the onRemove -> clear -> onPostProcess order from the previous commit). Providers call it from clearOverlays(). Controllers that only listen to the camera (heatmap) are left alone. ClearStrategyMarkerControllersTest fails when the helper does nothing and passes now. Public API: one protected method added. --- api/android-sdk-core.api | 1 + .../core/controller/BaseMapViewController.kt | 16 ++ .../ClearStrategyMarkerControllersTest.kt | 167 ++++++++++++++++++ 3 files changed, 184 insertions(+) create mode 100644 src/test/java/com/mapconductor/core/controller/ClearStrategyMarkerControllersTest.kt diff --git a/api/android-sdk-core.api b/api/android-sdk-core.api index df5319a..69a64a5 100644 --- a/api/android-sdk-core.api +++ b/api/android-sdk-core.api @@ -259,6 +259,7 @@ public abstract class com.mapconductor.core.controller.BaseMapViewController imp public final void setMapClickListener(kotlin.jvm.functions.Function1); public final void setMapLongClickListener(kotlin.jvm.functions.Function1); public void registerOverlayController(com.mapconductor.core.controller.OverlayControllerInterface); + protected final java.lang.Object clearStrategyMarkerControllers(kotlin.coroutines.Continuation); protected java.util.List getOverlayCascadeOrder(); protected boolean dispatchMarkerTap(com.mapconductor.core.features.GeoPointInterface); public final boolean dispatchOverlayTap(com.mapconductor.core.features.GeoPointInterface); diff --git a/src/main/java/com/mapconductor/core/controller/BaseMapViewController.kt b/src/main/java/com/mapconductor/core/controller/BaseMapViewController.kt index c989e3a..02ef930 100644 --- a/src/main/java/com/mapconductor/core/controller/BaseMapViewController.kt +++ b/src/main/java/com/mapconductor/core/controller/BaseMapViewController.kt @@ -19,6 +19,7 @@ import com.mapconductor.core.marker.MarkerAnimationOverlayHost import com.mapconductor.core.marker.MarkerCapableInterface import com.mapconductor.core.marker.MarkerState import com.mapconductor.core.marker.OnMarkerEventHandler +import com.mapconductor.core.marker.StrategyMarkerController import com.mapconductor.core.polygon.OnPolygonEventHandler import com.mapconductor.core.polygon.PolygonCapableInterface import com.mapconductor.core.polygon.PolygonState @@ -158,6 +159,21 @@ abstract class BaseMapViewController : } } + /** + * 登録されている [StrategyMarkerController](クラスタリングなどの Strategy で描くマーカー)を + * すべて clear する。各プロバイダの `clearOverlays()` から呼ぶ。 + * + * Strategy のコントローラはプロバイダが持つものではなく、拡張モジュール + * (android-marker-clustering の MarkerRenderingGroup など)が + * [registerOverlayController] で登録していくもの。プロバイダの `clearOverlays()` は + * 自分のフィールドのコントローラしか知らないので、ここで登録簿から拾う。 + * + * 他の種類(ヒートマップのようにカメラだけ購読するもの)は消さない。 + */ + protected suspend fun clearStrategyMarkerControllers() { + overlayControllers.filterIsInstance>().forEach { it.clear() } + } + // ── タップのカスケード ────────────────────────────────────────────── // // marker → circle → groundImage → polyline → polygon → map の一本道。 diff --git a/src/test/java/com/mapconductor/core/controller/ClearStrategyMarkerControllersTest.kt b/src/test/java/com/mapconductor/core/controller/ClearStrategyMarkerControllersTest.kt new file mode 100644 index 0000000..f20fb69 --- /dev/null +++ b/src/test/java/com/mapconductor/core/controller/ClearStrategyMarkerControllersTest.kt @@ -0,0 +1,167 @@ +package com.mapconductor.core.controller + +import com.mapconductor.core.features.GeoPoint +import com.mapconductor.core.features.GeoPointInterface +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 com.mapconductor.core.marker.MarkerEntity +import com.mapconductor.core.marker.MarkerEntityInterface +import com.mapconductor.core.marker.MarkerManager +import com.mapconductor.core.marker.MarkerOverlayRendererInterface +import com.mapconductor.core.marker.MarkerRenderingStrategyInterface +import com.mapconductor.core.marker.MarkerState +import com.mapconductor.core.marker.OnMarkerEventHandler +import com.mapconductor.core.marker.StrategyMarkerController +import kotlinx.coroutines.CoroutineScope +import kotlinx.coroutines.Dispatchers +import kotlinx.coroutines.test.runTest +import org.junit.Assert.assertEquals +import org.junit.Test + +/** + * `clearOverlays()` が、拡張モジュールが [BaseMapViewController.registerOverlayController] で + * 登録した [StrategyMarkerController](クラスタリングなど)も消すこと。 + * + * プロバイダの `clearOverlays()` は自分のフィールドのコントローラしか clear しておらず、 + * Strategy で描いたマーカーは clearOverlays() の後も地図に残っていた。 + */ +class ClearStrategyMarkerControllersTest { + 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 系の描画担当: onPostProcess で manager の全部を描き直す。 */ + private class GlRenderer( + private val manager: MarkerManager, + ) : MarkerOverlayRendererInterface { + var drawn: List = emptyList() + 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>) = Unit + + override suspend fun onAnimate(entity: MarkerEntityInterface) = Unit + + override suspend fun onPostProcess() { + drawn = manager.allEntities().map { it.state.id }.sorted() + } + } + + private class DrawAllStrategy : MarkerRenderingStrategyInterface { + override val markerManager: MarkerManager = MarkerManager.defaultManager() + + override fun clear() = 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 + } + + /** カメラだけ購読する拡張(android-heatmap のような)。clearOverlays で触られてはいけない。 */ + private class CameraOnlyController : OverlayControllerInterface { + var cleared = 0 + override val zIndex = 0 + + override suspend fun add(data: List) = Unit + + override suspend fun update(state: Unit) = Unit + + override suspend fun clear() { + cleared++ + } + + override fun find(position: GeoPointInterface): Unit? = null + + override fun destroy() = Unit + } + + /** プロバイダの clearOverlays() と同じ形: 自分のコントローラを消してから共通部分を呼ぶ。 */ + private class Driver : BaseMapViewController() { + override val holder get() = throw UnsupportedOperationException() + override val defaultCoroutine = CoroutineScope(Dispatchers.Unconfined) + override val mainCoroutine = defaultCoroutine + + override fun moveCamera(position: MapCameraPosition) = Unit + + override fun animateCamera( + position: MapCameraPosition, + duration: Long, + ) = Unit + + override fun fitBounds( + bounds: GeoRectBounds, + padding: Int, + ) = Unit + + override suspend fun clearOverlays() { + clearStrategyMarkerControllers() + } + } + + private fun markers() = + listOf( + MarkerState(position = GeoPoint(35.0, 139.0), id = "m1"), + MarkerState(position = GeoPoint(35.1, 139.1), id = "m2"), + ) + + @Test + fun `clearOverlays は登録された Strategy のマーカーを消し、他の登録物には触らない`() = + runTest { + val driver = Driver() + val groups = + (0..1).map { + val strategy = DrawAllStrategy() + val renderer = GlRenderer(strategy.markerManager) + val controller = StrategyMarkerController(strategy, renderer) + driver.registerOverlayController(controller) + controller.onCameraChanged(camera) + controller.add(markers()) + Triple(strategy, renderer, controller) + } + val other = CameraOnlyController() + driver.registerOverlayController(other) + groups.forEach { assertEquals(listOf("m1", "m2"), it.second.drawn) } + + driver.clearOverlays() + + groups.forEach { (strategy, renderer, _) -> + assertEquals(emptyList(), renderer.drawn) + assertEquals(0, strategy.markerManager.allEntities().size) + } + assertEquals(0, other.cleared) + + // 送り直せばまた描かれる。 + groups.forEach { it.third.add(markers()) } + groups.forEach { assertEquals(listOf("m1", "m2"), it.second.drawn) } + } +}