diff --git a/src/main/java/com/mapconductor/core/circle/CircleController.kt b/src/main/java/com/mapconductor/core/circle/CircleController.kt index 87ec272..04c07e3 100644 --- a/src/main/java/com/mapconductor/core/circle/CircleController.kt +++ b/src/main/java/com/mapconductor/core/circle/CircleController.kt @@ -151,6 +151,8 @@ abstract class CircleController( semaphore.withPermit { val entities: List> = circleManager.allEntities() renderer.onRemove(entities) + // manager を空にしてから onPostProcess を呼ぶ。GL 系の描画担当は remove では何もせず、 + // onPostProcess で manager の全部を描き直すので、逆の順だと clear() 後も地図に残る。 circleManager.clear() renderer.onPostProcess() } diff --git a/src/main/java/com/mapconductor/core/groundimage/GroundImageController.kt b/src/main/java/com/mapconductor/core/groundimage/GroundImageController.kt index 1c5cbc9..c5c70c5 100644 --- a/src/main/java/com/mapconductor/core/groundimage/GroundImageController.kt +++ b/src/main/java/com/mapconductor/core/groundimage/GroundImageController.kt @@ -148,8 +148,10 @@ abstract class GroundImageController( semaphore.withPermit { val entities: List> = groundImageManager.allEntities() renderer.onRemove(entities) - renderer.onPostProcess() + // manager を空にしてから onPostProcess を呼ぶ。GL 系の描画担当は remove では何もせず、 + // onPostProcess で manager の全部を描き直すので、逆の順だと clear() 後も地図に残る。 groundImageManager.clear() + renderer.onPostProcess() } } diff --git a/src/main/java/com/mapconductor/core/marker/AbstractMarkerController.kt b/src/main/java/com/mapconductor/core/marker/AbstractMarkerController.kt index 4f05cc5..5c74161 100644 --- a/src/main/java/com/mapconductor/core/marker/AbstractMarkerController.kt +++ b/src/main/java/com/mapconductor/core/marker/AbstractMarkerController.kt @@ -316,6 +316,9 @@ abstract class AbstractMarkerController( renderer.onRemove(entities) markerManager.clear() draggingMarkerIds.clear() + // 他の 5 種類と同じく、manager を空にしてから onPostProcess を呼ぶ。GL 系の描画担当は + // onRemove では地図に触らず、onPostProcess で manager の中身から描き直す。 + renderer.onPostProcess() } } diff --git a/src/main/java/com/mapconductor/core/polygon/PolygonController.kt b/src/main/java/com/mapconductor/core/polygon/PolygonController.kt index c9ad5c9..b1061e9 100644 --- a/src/main/java/com/mapconductor/core/polygon/PolygonController.kt +++ b/src/main/java/com/mapconductor/core/polygon/PolygonController.kt @@ -154,7 +154,10 @@ abstract class PolygonController( semaphore.withPermit { val entities: List> = polygonManager.allEntities() renderer.onRemove(entities) + // manager を空にしてから onPostProcess を呼ぶ。GL 系の描画担当は remove では何もせず、 + // onPostProcess で manager の全部を描き直すので、逆の順だと clear() 後も地図に残る。 polygonManager.clear() + renderer.onPostProcess() } } diff --git a/src/main/java/com/mapconductor/core/polyline/PolylineController.kt b/src/main/java/com/mapconductor/core/polyline/PolylineController.kt index 361cda6..0b585b6 100644 --- a/src/main/java/com/mapconductor/core/polyline/PolylineController.kt +++ b/src/main/java/com/mapconductor/core/polyline/PolylineController.kt @@ -154,8 +154,10 @@ abstract class PolylineController( semaphore.withPermit { val entities: List> = polylineManager.allEntities() renderer.onRemove(entities) - renderer.onPostProcess() + // manager を空にしてから onPostProcess を呼ぶ。GL 系の描画担当は remove では何もせず、 + // onPostProcess で manager の全部を描き直すので、逆の順だと clear() 後も地図に残る。 polylineManager.clear() + renderer.onPostProcess() } } diff --git a/src/main/java/com/mapconductor/core/raster/RasterLayerController.kt b/src/main/java/com/mapconductor/core/raster/RasterLayerController.kt index f6b3631..1e27d46 100644 --- a/src/main/java/com/mapconductor/core/raster/RasterLayerController.kt +++ b/src/main/java/com/mapconductor/core/raster/RasterLayerController.kt @@ -236,8 +236,10 @@ abstract class RasterLayerController( upsertedIds.clear() val entities: List> = rasterLayerManager.allEntities() renderer.onRemove(entities) - renderer.onPostProcess() + // manager を空にしてから onPostProcess を呼ぶ。GL 系の描画担当は remove では何もせず、 + // onPostProcess で manager の全部を描き直すので、逆の順だと clear() 後も地図に残る。 rasterLayerManager.clear() + renderer.onPostProcess() } } } diff --git a/src/test/java/com/mapconductor/core/OverlayClearOrderTest.kt b/src/test/java/com/mapconductor/core/OverlayClearOrderTest.kt new file mode 100644 index 0000000..f26fed2 --- /dev/null +++ b/src/test/java/com/mapconductor/core/OverlayClearOrderTest.kt @@ -0,0 +1,302 @@ +package com.mapconductor.core + +import com.mapconductor.core.circle.CircleController +import com.mapconductor.core.circle.CircleEntityInterface +import com.mapconductor.core.circle.CircleManager +import com.mapconductor.core.circle.CircleOverlayRendererInterface +import com.mapconductor.core.circle.CircleState +import com.mapconductor.core.features.GeoPoint +import com.mapconductor.core.features.GeoRectBounds +import com.mapconductor.core.groundimage.GroundImageController +import com.mapconductor.core.groundimage.GroundImageEntityInterface +import com.mapconductor.core.groundimage.GroundImageManager +import com.mapconductor.core.groundimage.GroundImageOverlayRendererInterface +import com.mapconductor.core.groundimage.GroundImageState +import com.mapconductor.core.map.MapViewHolderInterface +import com.mapconductor.core.marker.AbstractMarkerController +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.MarkerState +import com.mapconductor.core.marker.OnMarkerEventHandler +import com.mapconductor.core.polygon.PolygonController +import com.mapconductor.core.polygon.PolygonEntityInterface +import com.mapconductor.core.polygon.PolygonManager +import com.mapconductor.core.polygon.PolygonOverlayRendererInterface +import com.mapconductor.core.polygon.PolygonState +import com.mapconductor.core.polyline.PolylineController +import com.mapconductor.core.polyline.PolylineEntityInterface +import com.mapconductor.core.polyline.PolylineManager +import com.mapconductor.core.polyline.PolylineOverlayRendererInterface +import com.mapconductor.core.polyline.PolylineState +import com.mapconductor.core.raster.RasterLayerController +import com.mapconductor.core.raster.RasterLayerEntityInterface +import com.mapconductor.core.raster.RasterLayerManager +import com.mapconductor.core.raster.RasterLayerOverlayRendererInterface +import com.mapconductor.core.raster.RasterLayerSource +import com.mapconductor.core.raster.RasterLayerState +import kotlinx.coroutines.CoroutineScope +import kotlinx.coroutines.test.StandardTestDispatcher +import kotlinx.coroutines.test.runTest +import org.junit.Assert.assertEquals +import org.junit.Test + +/** + * clear() の後、地図に何も残らないこと。 + * + * MapLibre / Mapbox / Mappls / OMM などの GL 系の描画担当は、remove では何もせず、 + * onPostProcess で manager の全部を描き直す。ここの偽レンダラも同じ作りにしてあり、 + * `drawn` は最後の onPostProcess で描いたもの(=地図に見えているもの)を表す。 + * + * clear() が manager を空にする前に onPostProcess を呼ぶと、全部を描き直して残る。 + * 呼ばなければ、前に描いたものが残る。 + */ +class OverlayClearOrderTest { + private val a = GeoPoint(35.0, 139.0) + private val b = GeoPoint(35.1, 139.1) + private val c = GeoPoint(35.0, 139.1) + + /** GL 系の描画担当の作り: onPostProcess で [all] の全部を描き直す。 */ + private class Drawn( + private val all: () -> List, + ) { + var ids: List = emptyList() + private set + + fun redraw() { + ids = all().sorted() + } + } + + // ── Polygon ───────────────────────────────────────────────────────── + + private class PolygonGl( + manager: PolygonManager, + ) : PolygonOverlayRendererInterface { + val drawn = Drawn { manager.allEntities().map { it.state.id } } + + 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 onPostProcess() = drawn.redraw() + } + + @Test + fun `polygon の clear 後は何も描かれていない`() = + runTest { + val manager = PolygonManager() + val renderer = PolygonGl(manager) + val controller = object : PolygonController(manager, renderer) {} + controller.add(listOf(PolygonState(points = listOf(a, b, c), id = "p1"), PolygonState(points = listOf(a, b, c), id = "p2"))) + assertEquals(listOf("p1", "p2"), renderer.drawn.ids) + + controller.clear() + + assertEquals(emptyList(), renderer.drawn.ids) + } + + // ── Polyline ──────────────────────────────────────────────────────── + + private class PolylineGl( + manager: PolylineManager, + ) : PolylineOverlayRendererInterface { + val drawn = Drawn { manager.allEntities().map { it.state.id } } + + 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 onPostProcess() = drawn.redraw() + } + + @Test + fun `polyline の clear 後は何も描かれていない`() = + runTest { + val manager = PolylineManager() + val renderer = PolylineGl(manager) + val controller = object : PolylineController(manager, renderer) {} + controller.add(listOf(PolylineState(points = listOf(a, b), id = "l1"), PolylineState(points = listOf(b, c), id = "l2"))) + assertEquals(listOf("l1", "l2"), renderer.drawn.ids) + + controller.clear() + + assertEquals(emptyList(), renderer.drawn.ids) + } + + // ── Circle ────────────────────────────────────────────────────────── + + private class CircleGl( + manager: CircleManager, + ) : CircleOverlayRendererInterface { + val drawn = Drawn { manager.allEntities().map { it.state.id } } + + 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 onPostProcess() = drawn.redraw() + } + + @Test + fun `circle の clear 後は何も描かれていない`() = + runTest { + val manager = CircleManager() + val renderer = CircleGl(manager) + val controller = object : CircleController(manager, renderer) {} + controller.add( + listOf(CircleState(center = a, radiusMeters = 100.0, id = "c1"), CircleState(center = b, radiusMeters = 50.0, id = "c2")), + ) + assertEquals(listOf("c1", "c2"), renderer.drawn.ids) + + controller.clear() + + assertEquals(emptyList(), renderer.drawn.ids) + } + + // ── GroundImage ───────────────────────────────────────────────────── + + private class GroundImageGl( + manager: GroundImageManager, + ) : GroundImageOverlayRendererInterface { + val drawn = Drawn { manager.allEntities().map { it.state.id } } + + 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 onPostProcess() = drawn.redraw() + } + + /** GroundImageState は Drawable を要求するが、ここでは中身を使わない。 */ + private class FakeDrawable : android.graphics.drawable.Drawable() { + override fun draw(canvas: android.graphics.Canvas) = Unit + + override fun setAlpha(alpha: Int) = Unit + + override fun setColorFilter(colorFilter: android.graphics.ColorFilter?) = Unit + + @Deprecated("Deprecated in Java") + override fun getOpacity(): Int = android.graphics.PixelFormat.OPAQUE + } + + @Test + fun `groundImage の clear 後は何も描かれていない`() = + runTest { + val manager = GroundImageManager() + val renderer = GroundImageGl(manager) + val controller = object : GroundImageController(manager, renderer) {} + val bounds = + GeoRectBounds().apply { + extend(a) + extend(b) + } + controller.add( + listOf( + GroundImageState(bounds = bounds, image = FakeDrawable(), id = "g1"), + GroundImageState(bounds = bounds, image = FakeDrawable(), id = "g2"), + ), + ) + assertEquals(listOf("g1", "g2"), renderer.drawn.ids) + + controller.clear() + + assertEquals(emptyList(), renderer.drawn.ids) + } + + // ── RasterLayer ───────────────────────────────────────────────────── + + private class RasterGl( + manager: RasterLayerManager, + override val coroutine: CoroutineScope, + ) : RasterLayerOverlayRendererInterface { + val drawn = Drawn { manager.allEntities().map { it.state.id } } + + 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 onPostProcess() = drawn.redraw() + } + + @Test + fun `rasterLayer の clear 後は何も描かれていない`() = + runTest { + val manager = RasterLayerManager() + val renderer = RasterGl(manager, CoroutineScope(StandardTestDispatcher(testScheduler))) + val controller = object : RasterLayerController(manager, renderer) {} + controller.add( + listOf( + RasterLayerState(source = RasterLayerSource.UrlTemplate("https://example.com/a/{z}/{x}/{y}.png"), id = "r1"), + RasterLayerState(source = RasterLayerSource.UrlTemplate("https://example.com/b/{z}/{x}/{y}.png"), id = "r2"), + ), + ) + assertEquals(listOf("r1", "r2"), renderer.drawn.ids) + + controller.clear() + + assertEquals(emptyList(), renderer.drawn.ids) + } + + // ── Marker ────────────────────────────────────────────────────────── + + /** + * MapLibre / Mapbox / Mappls のマーカーの描画担当と同じ作り: onRemove はアイコンの参照数を + * 減らすだけで、地図のソースは onPostProcess が manager の中身から描き直す。 + */ + private class MarkerGl( + manager: MarkerManager, + ) : MarkerOverlayRendererInterface { + val drawn = Drawn { manager.allEntities().map { it.state.id } } + 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.redraw() + } + + @Test + fun `marker の clear 後は何も描かれていない`() = + runTest { + val manager = MarkerManager.defaultManager() + val renderer = MarkerGl(manager) + val controller = object : AbstractMarkerController(manager, renderer) {} + // add() は既定アイコン(Android の Bitmap)を焼くので素の JVM では呼べない。 + // 描かれた状態は manager へ直接登録して作る。 + listOf("m1", "m2").forEach { id -> + manager.registerEntity(MarkerEntity(marker = id, state = MarkerState(position = a, id = id), isRendered = true)) + } + renderer.onPostProcess() + assertEquals(listOf("m1", "m2"), renderer.drawn.ids) + + controller.clear() + + assertEquals(emptyList(), renderer.drawn.ids) + } +}