From 4b7363cc2c3cd4659965ffad553914eceb822f10 Mon Sep 17 00:00:00 2001 From: Masashi Katsumata Date: Sat, 7 Nov 2026 18:42:49 -0800 Subject: [PATCH 1/2] Clear overlays in the order remove, empty the manager, then post-process GL-style renderers (MapLibre, Mapbox, Mappls, OpenMobileMaps) ignore onRemove and redraw everything the manager holds in onPostProcess. Polyline, GroundImage and RasterLayer called onPostProcess before emptying the manager, so clear() redrew every overlay it was meant to remove; Polygon did not call it at all. All five now run onRemove, manager.clear(), onPostProcess, as CircleController already did. OverlayClearOrderTest pins this with a GL-style fake renderer for each kind. --- .../core/circle/CircleController.kt | 2 + .../core/groundimage/GroundImageController.kt | 4 +- .../core/polygon/PolygonController.kt | 3 + .../core/polyline/PolylineController.kt | 4 +- .../core/raster/RasterLayerController.kt | 4 +- .../core/OverlayClearOrderTest.kt | 248 ++++++++++++++++++ 6 files changed, 262 insertions(+), 3 deletions(-) create mode 100644 src/test/java/com/mapconductor/core/OverlayClearOrderTest.kt diff --git a/src/main/java/com/mapconductor/core/circle/CircleController.kt b/src/main/java/com/mapconductor/core/circle/CircleController.kt index 2075365..63a2a56 100644 --- a/src/main/java/com/mapconductor/core/circle/CircleController.kt +++ b/src/main/java/com/mapconductor/core/circle/CircleController.kt @@ -150,6 +150,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 b51818a..9d3a8ba 100644 --- a/src/main/java/com/mapconductor/core/groundimage/GroundImageController.kt +++ b/src/main/java/com/mapconductor/core/groundimage/GroundImageController.kt @@ -147,8 +147,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/polygon/PolygonController.kt b/src/main/java/com/mapconductor/core/polygon/PolygonController.kt index 96eb44f..f7a5f4b 100644 --- a/src/main/java/com/mapconductor/core/polygon/PolygonController.kt +++ b/src/main/java/com/mapconductor/core/polygon/PolygonController.kt @@ -153,7 +153,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 d5080cd..f7a21a4 100644 --- a/src/main/java/com/mapconductor/core/polyline/PolylineController.kt +++ b/src/main/java/com/mapconductor/core/polyline/PolylineController.kt @@ -153,8 +153,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 4b3d831..85a3e8e 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..eb9d364 --- /dev/null +++ b/src/test/java/com/mapconductor/core/OverlayClearOrderTest.kt @@ -0,0 +1,248 @@ +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.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) + } +} From 8bf797a30e69fceb987044acfae05bc626ee5edf Mon Sep 17 00:00:00 2001 From: Masashi Katsumata Date: Sat, 7 Nov 2026 19:49:26 -0800 Subject: [PATCH 2/2] Post-process after clearing markers too AbstractMarkerController.clear() emptied the manager without calling onPostProcess. GL-style marker renderers only drop icon references in onRemove and redraw their source from the manager in onPostProcess, so the markers stayed on the map. Clear now ends the same way as the other five overlay kinds: onRemove, manager.clear(), onPostProcess. OverlayClearOrderTest gains the marker case. --- .../core/marker/AbstractMarkerController.kt | 3 ++ .../core/OverlayClearOrderTest.kt | 54 +++++++++++++++++++ 2 files changed, 57 insertions(+) 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/test/java/com/mapconductor/core/OverlayClearOrderTest.kt b/src/test/java/com/mapconductor/core/OverlayClearOrderTest.kt index eb9d364..f26fed2 100644 --- a/src/test/java/com/mapconductor/core/OverlayClearOrderTest.kt +++ b/src/test/java/com/mapconductor/core/OverlayClearOrderTest.kt @@ -12,6 +12,14 @@ 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 @@ -243,6 +251,52 @@ class OverlayClearOrderTest { 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) } }