From 301d9c947c59b4df2f114bad05e0d65b37697c6e Mon Sep 17 00:00:00 2001 From: Masashi Katsumata Date: Tue, 10 Nov 2026 16:16:50 -0800 Subject: [PATCH] Support bulk Circles with raster tiles --- .../MapLibreCircleTilingDeviceTest.kt | 71 +++++++++++++++++++ .../maplibre/MapLibreOverlayStacking.kt | 3 + .../maplibre/MapLibreViewController.kt | 15 ++++ .../circle/MapLibreCircleOverlayRenderer.kt | 2 +- .../MapLibreRasterLayerOverlayRenderer.kt | 7 +- 5 files changed, 96 insertions(+), 2 deletions(-) create mode 100644 src/androidTest/java/com/mapconductor/maplibre/MapLibreCircleTilingDeviceTest.kt diff --git a/src/androidTest/java/com/mapconductor/maplibre/MapLibreCircleTilingDeviceTest.kt b/src/androidTest/java/com/mapconductor/maplibre/MapLibreCircleTilingDeviceTest.kt new file mode 100644 index 0000000..83cae1d --- /dev/null +++ b/src/androidTest/java/com/mapconductor/maplibre/MapLibreCircleTilingDeviceTest.kt @@ -0,0 +1,71 @@ +package com.mapconductor.maplibre + +import androidx.compose.foundation.layout.fillMaxSize +import androidx.compose.runtime.getValue +import androidx.compose.runtime.mutableStateOf +import androidx.compose.runtime.setValue +import androidx.compose.ui.Modifier +import androidx.compose.ui.test.junit4.createComposeRule +import androidx.test.ext.junit.runners.AndroidJUnit4 +import androidx.test.platform.app.InstrumentationRegistry +import com.mapconductor.compose.circle.Circles +import com.mapconductor.core.circle.CIRCLE_TILE_LAYER_ID +import com.mapconductor.core.circle.CircleState +import com.mapconductor.core.features.GeoPoint +import java.io.File +import org.junit.Assert.assertTrue +import org.junit.Rule +import org.junit.Test +import org.junit.runner.RunWith + +/** Exercises bulk registration through the real map host, not just the core tiler. */ +@RunWith(AndroidJUnit4::class) +class MapLibreCircleTilingDeviceTest { + @get:Rule val compose = createComposeRule() + + @Test + fun circlesSwitchBetweenTilesAndNativeAndSurviveStyleReload() { + val context = InstrumentationRegistry.getInstrumentation().targetContext + fun design(name: String): MapLibreDesign { + val file = File(context.cacheDir, "circle-device-$name.json") + file.writeText("""{"version":8,"name":"$name","sources":{},"layers":[{"id":"background","type":"background"}]}""") + return MapLibreDesign(id = name, styleJsonURL = "file://${file.absolutePath}") + } + val first = design("first") + val second = design("second") + val state = MapLibreViewState(mapDesignType = first, id = "circle-device") + val many = (0..2000).map { i -> + CircleState(center = GeoPoint(35.6 + i * 0.00001, 139.7), radiusMeters = 20.0, id = "circle-$i") + } + var circles by mutableStateOf(many) + compose.setContent { + MapLibreMapView(state = state, modifier = Modifier.fillMaxSize()) { Circles(circles) } + } + fun tiled(design: String): Boolean { + var result = false + InstrumentationRegistry.getInstrumentation().runOnMainSync { + val style = state.getMapViewHolder()?.map?.style + result = style?.isFullyLoaded == true && style.uri == (if (design == "first") first.styleJsonURL else second.styleJsonURL) && + style.layers.any { it.id.contains(CIRCLE_TILE_LAYER_ID) } + } + return result + } + compose.waitUntil(30_000) { tiled("first") } + compose.runOnUiThread { state.mapDesignType = second } + compose.waitUntil(30_000) { tiled("second") } + compose.runOnUiThread { circles = many.take(1) } + compose.waitUntil(30_000) { + var native = false + InstrumentationRegistry.getInstrumentation().runOnMainSync { + val style = state.getMapViewHolder()?.map?.style + native = style?.isFullyLoaded == true && style.layers.none { it.id.contains(CIRCLE_TILE_LAYER_ID) } + } + native + } + compose.runOnUiThread { circles = many } + compose.waitUntil(30_000) { tiled("second") } + assertTrue(tiled("second")) + compose.runOnUiThread { circles = emptyList() } + compose.waitUntil(30_000) { !tiled("second") } + } +} diff --git a/src/main/java/com/mapconductor/maplibre/MapLibreOverlayStacking.kt b/src/main/java/com/mapconductor/maplibre/MapLibreOverlayStacking.kt index 3d763eb..8181e44 100644 --- a/src/main/java/com/mapconductor/maplibre/MapLibreOverlayStacking.kt +++ b/src/main/java/com/mapconductor/maplibre/MapLibreOverlayStacking.kt @@ -39,6 +39,9 @@ internal object MapLibreOverlayStacking { /** ポリゴンのタイル(ラスター): ポリゴンの帯の一番下。 */ val POLYGON_TILE = key(OverlayKind.Polygon, Int.MIN_VALUE) + /** 円のタイル(ラスター): サークルの帯の一番下(ポリゴンの上、ネイティブの円とポリラインのタイルの下)。 */ + val CIRCLE_TILE = key(OverlayKind.Circle, Int.MIN_VALUE) + /** ポリラインのタイル(ラスター): ポリラインの帯の一番下(サークルの上、ネイティブのポリラインの下)。 */ val POLYLINE_TILE = key(OverlayKind.Polyline, Int.MIN_VALUE) diff --git a/src/main/java/com/mapconductor/maplibre/MapLibreViewController.kt b/src/main/java/com/mapconductor/maplibre/MapLibreViewController.kt index 4dae837..9e9af8e 100644 --- a/src/main/java/com/mapconductor/maplibre/MapLibreViewController.kt +++ b/src/main/java/com/mapconductor/maplibre/MapLibreViewController.kt @@ -1,5 +1,8 @@ package com.mapconductor.maplibre +import com.mapconductor.core.circle.CIRCLE_TILE_LAYER_ID +import com.mapconductor.core.circle.CircleTiler +import com.mapconductor.core.circle.circleTileRasterLayerState import com.mapconductor.core.circle.OnCircleEventHandler import com.mapconductor.core.controller.BaseMapViewController import com.mapconductor.core.features.GeoPointInterface @@ -124,6 +127,18 @@ class MapLibreViewController( } } + // Many circles too: the same raster layer path, in the circle band. + circleController.tiler = + CircleTiler().apply { + onTemplate = { template, tileSize -> + if (template != null) { + rasterLayerController.upsert(circleTileRasterLayerState(template, tileSize)) + } else { + rasterLayerController.removeById(CIRCLE_TILE_LAYER_ID) + } + } + } + // Many polylines too: the same raster layer path, in the polyline band. polylineController.tiler = PolylineTiler().apply { diff --git a/src/main/java/com/mapconductor/maplibre/circle/MapLibreCircleOverlayRenderer.kt b/src/main/java/com/mapconductor/maplibre/circle/MapLibreCircleOverlayRenderer.kt index 6315989..b36ed33 100644 --- a/src/main/java/com/mapconductor/maplibre/circle/MapLibreCircleOverlayRenderer.kt +++ b/src/main/java/com/mapconductor/maplibre/circle/MapLibreCircleOverlayRenderer.kt @@ -42,7 +42,7 @@ class MapLibreCircleOverlayRenderer( /** Reapply the current composition after a MapLibre style is created or replaced. */ fun redraw() { - val circles = circleManager.allEntities() + val circles = circleManager.nativeEntities().filter { (it.circle as MapLibreActualCircle?) != null } holder.map.style?.let { style -> coroutine.launch { layer.draw(circles, style) } } diff --git a/src/main/java/com/mapconductor/maplibre/raster/MapLibreRasterLayerOverlayRenderer.kt b/src/main/java/com/mapconductor/maplibre/raster/MapLibreRasterLayerOverlayRenderer.kt index 697d665..6f16208 100644 --- a/src/main/java/com/mapconductor/maplibre/raster/MapLibreRasterLayerOverlayRenderer.kt +++ b/src/main/java/com/mapconductor/maplibre/raster/MapLibreRasterLayerOverlayRenderer.kt @@ -1,5 +1,6 @@ package com.mapconductor.maplibre.raster +import com.mapconductor.core.circle.CIRCLE_TILE_LAYER_ID import com.mapconductor.core.controller.OverlayKind import com.mapconductor.core.polygon.POLYGON_TILE_LAYER_ID import com.mapconductor.core.polyline.POLYLINE_TILE_LAYER_ID @@ -32,6 +33,8 @@ class MapLibreRasterLayerOverlayRenderer( private fun isPolylineTileRaster(state: RasterLayerState): Boolean = state.id == POLYLINE_TILE_LAYER_ID + private fun isCircleTileRaster(state: RasterLayerState): Boolean = state.id == CIRCLE_TILE_LAYER_ID + override suspend fun onAdd( data: List, ): List = @@ -120,6 +123,8 @@ class MapLibreRasterLayerOverlayRenderer( MapLibreOverlayStacking.MARKER_TILE } else if (isPolygonTileRaster(state)) { MapLibreOverlayStacking.POLYGON_TILE + } else if (isCircleTileRaster(state)) { + MapLibreOverlayStacking.CIRCLE_TILE } else if (isPolylineTileRaster(state)) { MapLibreOverlayStacking.POLYLINE_TILE } else { @@ -157,7 +162,7 @@ class MapLibreRasterLayerOverlayRenderer( val ordered = stateById.values .asSequence() - .filter { !isMarkerTileRaster(it) && !isPolygonTileRaster(it) && !isPolylineTileRaster(it) } + .filter { !isMarkerTileRaster(it) && !isPolygonTileRaster(it) && !isPolylineTileRaster(it) && !isCircleTileRaster(it) } .sortedBy { it.zIndex } .mapNotNull { state -> handleById[state.id]?.let { handle -> state to handle } } .toList()