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) } + } +}