From 5e594cd07e4531217a91fd3138d3221671a536fb Mon Sep 17 00:00:00 2001 From: Masashi Katsumata Date: Sat, 7 Nov 2026 05:40:54 -0800 Subject: [PATCH 1/4] Draw large polygon sets as raster tiles (PolygonTiler) The polygon controller gets a PolygonTiler, whose tiles are shown through the raster layer path the marker tiles use (upsert / removeById, outside the app's RasterLayer composition). Tiled polygons stay registered for taps and give the map SDK nothing. The polygon tile layer goes where the SDK's polygon layers do: over the basemap, under polylines and markers. --- api/android-for-mapbox.api | 2 ++ .../mapbox/MapboxMapViewController.kt | 16 +++++++++ .../mapbox/polygon/MapboxPolygonConductor.kt | 33 +++++++++++++++++++ .../MapboxRasterLayerOverlayRenderer.kt | 33 ++++++++++++++++++- 4 files changed, 83 insertions(+), 1 deletion(-) diff --git a/api/android-for-mapbox.api b/api/android-for-mapbox.api index 3527d83..fe46994 100644 --- a/api/android-for-mapbox.api +++ b/api/android-for-mapbox.api @@ -405,6 +405,8 @@ public final class com.mapconductor.mapbox.polygon.MapboxPolygonConductor implem public final com.mapconductor.mapbox.polygon.MapboxPolygonOverlayRenderer getPolygonOverlay(); public final com.mapconductor.mapbox.polyline.MapboxPolylineOverlayRenderer getPolylineOverlay(); public int getZIndex(); + public final com.mapconductor.core.polygon.PolygonTiler getTiler(); + public final void setTiler(com.mapconductor.core.polygon.PolygonTiler); public java.lang.Object add(java.util.List, kotlin.coroutines.Continuation); public java.lang.Object update(com.mapconductor.core.polygon.PolygonState, kotlin.coroutines.Continuation); public final void dispatchClick(com.mapconductor.core.polygon.PolygonEvent); diff --git a/src/main/java/com/mapconductor/mapbox/MapboxMapViewController.kt b/src/main/java/com/mapconductor/mapbox/MapboxMapViewController.kt index e10bc14..075c5fa 100644 --- a/src/main/java/com/mapconductor/mapbox/MapboxMapViewController.kt +++ b/src/main/java/com/mapconductor/mapbox/MapboxMapViewController.kt @@ -1,5 +1,8 @@ package com.mapconductor.mapbox +import com.mapconductor.core.polygon.POLYGON_TILE_LAYER_ID +import com.mapconductor.core.polygon.PolygonTiler +import com.mapconductor.core.polygon.polygonTileRasterLayerState import MapboxMapViewControllerInterface import com.mapbox.android.gestures.MoveGestureDetector import com.mapbox.geojson.Point @@ -116,6 +119,19 @@ class MapboxMapViewController internal constructor( markerTileLayers.forEach { entity -> rasterLayerController.removeById(entity.state.id) } } } + + // Many polygons are drawn as raster tiles, shown through the same RasterLayer + // controller as the marker tiles (upsert / removeById, never the app's layers). + polygonController.tiler = + PolygonTiler().apply { + onTemplate = { template, tileSize -> + if (template != null) { + rasterLayerController.upsert(polygonTileRasterLayerState(template, tileSize)) + } else { + rasterLayerController.removeById(POLYGON_TILE_LAYER_ID) + } + } + } } override fun moveCamera(position: MapCameraPosition) = handleMoveCamera(position) diff --git a/src/main/java/com/mapconductor/mapbox/polygon/MapboxPolygonConductor.kt b/src/main/java/com/mapconductor/mapbox/polygon/MapboxPolygonConductor.kt index d797101..8038ca5 100644 --- a/src/main/java/com/mapconductor/mapbox/polygon/MapboxPolygonConductor.kt +++ b/src/main/java/com/mapconductor/mapbox/polygon/MapboxPolygonConductor.kt @@ -8,9 +8,12 @@ import com.mapconductor.core.polygon.PolygonEntity import com.mapconductor.core.polygon.PolygonEntityInterface import com.mapconductor.core.polygon.PolygonEvent import com.mapconductor.core.polygon.PolygonState +import com.mapconductor.core.polygon.PolygonTiler import com.mapconductor.core.polyline.PolylineEntity import com.mapconductor.core.polyline.PolylineState import com.mapconductor.mapbox.polyline.MapboxPolylineOverlayRenderer +import kotlinx.coroutines.Dispatchers +import kotlinx.coroutines.withContext class MapboxPolygonConductor( val polygonOverlay: MapboxPolygonOverlayRenderer, @@ -21,6 +24,13 @@ class MapboxPolygonConductor( > { override val zIndex: Int = 2 + /** + * Set by the view controller to draw many polygons as tiles (see + * [PolygonTiler]). Tiled polygons stay in the manager, for taps, and give + * the GeoJSON layers nothing. + */ + var tiler: PolygonTiler? = null + override suspend fun add(data: List) { val nextIds = data.asSequence().map { it.id }.toSet() val prevIds = @@ -36,7 +46,19 @@ class MapboxPolygonConductor( polylineOverlay.polylineManager.removeEntity("outline-$id") } + val tiled = tiler?.route(data) ?: emptySet() + if (tiled.isNotEmpty()) { + // Still registered, so taps resolve against the original shape. + withContext(Dispatchers.Default) { + for (polygonState in data) { + if (polygonState.id !in tiled) continue + polygonOverlay.polygonManager.registerEntity(PolygonEntity(polygon = IN_TILES, state = polygonState)) + polylineOverlay.polylineManager.removeEntity("outline-${polygonState.id}") + } + } + } data.forEach { polygonState -> + if (polygonState.id in tiled) return@forEach polygonOverlay.createPolygon(polygonState)?.let { polygon -> val polygonEntity = PolygonEntity( @@ -61,6 +83,13 @@ class MapboxPolygonConductor( } override suspend fun update(state: PolygonState) { + tiler?.let { tiler -> + if (tiler.isTiled(state.id)) { + polygonOverlay.polygonManager.registerEntity(PolygonEntity(polygon = IN_TILES, state = state)) + tiler.update(state) + return + } + } polygonOverlay.createPolygon(state)?.let { polygon -> val polygonEntity = PolygonEntity( @@ -96,6 +125,7 @@ class MapboxPolygonConductor( polygonOverlay.polygonManager.find(position) as? PolygonEntityInterface override suspend fun clear() { + tiler?.clear() polygonOverlay.polygonManager.clear() polylineOverlay.polylineManager.clear() polygonOverlay.onPostProcess() @@ -133,6 +163,9 @@ class MapboxPolygonConductor( } } +/** What a tiled polygon hands the GeoJSON layers: nothing. It is in the tiles. */ +private val IN_TILES: List = emptyList() + private fun PolygonState.toPolylineState(): PolylineState { val closedPoints = if (points.first() != points.last()) { diff --git a/src/main/java/com/mapconductor/mapbox/raster/MapboxRasterLayerOverlayRenderer.kt b/src/main/java/com/mapconductor/mapbox/raster/MapboxRasterLayerOverlayRenderer.kt index 54c73a1..1509a07 100644 --- a/src/main/java/com/mapconductor/mapbox/raster/MapboxRasterLayerOverlayRenderer.kt +++ b/src/main/java/com/mapconductor/mapbox/raster/MapboxRasterLayerOverlayRenderer.kt @@ -6,6 +6,7 @@ import com.mapbox.maps.extension.style.layers.addLayerBelow import com.mapbox.maps.extension.style.layers.generated.rasterLayer import com.mapbox.maps.extension.style.sources.addSource import com.mapbox.maps.extension.style.sources.generated.rasterSource +import com.mapconductor.core.polygon.POLYGON_TILE_LAYER_ID import com.mapconductor.core.raster.RasterHeaderRuleSet import com.mapconductor.core.raster.RasterLayerEntityInterface import com.mapconductor.core.raster.RasterLayerOverlayRendererInterface @@ -26,6 +27,8 @@ class MapboxRasterLayerOverlayRenderer( private fun isMarkerTileRaster(state: RasterLayerState): Boolean = state.id.startsWith(MARKER_TILE_RASTER_ID_PREFIX) + private fun isPolygonTileRaster(state: RasterLayerState): Boolean = state.id == POLYGON_TILE_LAYER_ID + /** Mapbox Android はタイル要求を書き換える公開 API を持たない。 */ override suspend fun onAdd( data: List, @@ -102,6 +105,8 @@ class MapboxRasterLayerOverlayRenderer( try { if (isMarkerTileRaster(state)) { addLayerForMarkerTile(style, layer) + } else if (isPolygonTileRaster(state)) { + addLayerForPolygonTile(style, layer) } else { addBelowBasemapLabels(style, layer) } @@ -133,6 +138,8 @@ class MapboxRasterLayerOverlayRenderer( try { if (isMarkerTileRaster(state)) { addLayerForMarkerTile(style, layer) + } else if (isPolygonTileRaster(state)) { + addLayerForPolygonTile(style, layer) } else { addBelowBasemapLabels(style, layer) } @@ -182,6 +189,26 @@ class MapboxRasterLayerOverlayRenderer( } } + /** + * The polygon tiles are polygons drawn as images, not imagery: they go + * where the SDK's own polygon layers do, over the basemap and under + * polylines and markers -- below the first of those in the style. + */ + private fun addLayerForPolygonTile( + style: com.mapbox.maps.Style, + layer: com.mapbox.maps.extension.style.layers.Layer, + ) { + val anchor = style.styleLayers.map { it.id }.firstOrNull { id -> POLYGON_TILE_ANCHOR_PREFIXES.any { id.startsWith(it) } } + if (anchor != null) { + try { + style.addLayerBelow(layer, anchor) + return + } catch (_: Exception) { + } + } + style.addLayer(layer) + } + private fun addLayerForMarkerTile( style: com.mapbox.maps.Style, layer: com.mapbox.maps.extension.style.layers.Layer, @@ -208,7 +235,7 @@ class MapboxRasterLayerOverlayRenderer( val ordered = stateById.values .asSequence() - .filter { !isMarkerTileRaster(it) } + .filter { !isMarkerTileRaster(it) && !isPolygonTileRaster(it) } .sortedBy { it.zIndex } .mapNotNull { state -> handleById[state.id]?.let { handle -> state to handle } } .toList() @@ -285,6 +312,10 @@ class MapboxRasterLayerOverlayRenderer( private const val MARKER_TILE_RASTER_ID_PREFIX = "marker-tile-" private const val MARKERS_LAYER_ID = "markers-layer" private const val POLYLINE_LAYER_ID = "polyline-layer" + + /** The SDK's own layers the polygon tiles go below, whichever comes first in the style. */ + private val POLYGON_TILE_ANCHOR_PREFIXES = + listOf("polygon-fill-layer", "polygon-outline-layer", POLYLINE_LAYER_ID, "raster-layer-marker-tile-", MARKERS_LAYER_ID) } } From c8abaee18b57a2654eba694cd58bf8f76b5a8733 Mon Sep 17 00:00:00 2001 From: Masashi Katsumata Date: Sat, 7 Nov 2026 13:33:12 -0800 Subject: [PATCH 2/4] Dispose the polygon tiler when the conductor is destroyed core's PolygonController.destroy() releases its tiler (02369fa), but this provider's own polygon conductor does not extend it and its destroy() was empty, so the tile server kept every destroyed map's tiled polygons. --- .../mapconductor/mapbox/polygon/MapboxPolygonConductor.kt | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/src/main/java/com/mapconductor/mapbox/polygon/MapboxPolygonConductor.kt b/src/main/java/com/mapconductor/mapbox/polygon/MapboxPolygonConductor.kt index 8038ca5..eb6f713 100644 --- a/src/main/java/com/mapconductor/mapbox/polygon/MapboxPolygonConductor.kt +++ b/src/main/java/com/mapconductor/mapbox/polygon/MapboxPolygonConductor.kt @@ -159,7 +159,10 @@ class MapboxPolygonConductor( } override fun destroy() { - // No native resources to clean up for polygons + // The tile server outlives the map; without this its route keeps every + // tiled polygon (and the renderer's copies of them) alive. Same as + // core's PolygonController.destroy(), which this conductor does not extend. + tiler?.dispose() } } From 6aacb1b71ad1c85735eed0471eb0fcd3efff6935 Mon Sep 17 00:00:00 2001 From: Masashi Katsumata Date: Sat, 7 Nov 2026 20:07:32 -0800 Subject: [PATCH 3/4] Polygon conductor: move a tiled polygon to the GeoJSON layers when a change makes it untileable Setting geodesic on a tiled polygon in place, or giving it points that span half the world or fewer than three, kept it in the tiles. update() now checks canTile and, when it fails, takes the polygon out of the tiles and draws it natively. It stays on the map, so this is not an add. --- .../mapbox/polygon/MapboxPolygonConductor.kt | 13 ++++++++++--- 1 file changed, 10 insertions(+), 3 deletions(-) diff --git a/src/main/java/com/mapconductor/mapbox/polygon/MapboxPolygonConductor.kt b/src/main/java/com/mapconductor/mapbox/polygon/MapboxPolygonConductor.kt index eb6f713..5858c51 100644 --- a/src/main/java/com/mapconductor/mapbox/polygon/MapboxPolygonConductor.kt +++ b/src/main/java/com/mapconductor/mapbox/polygon/MapboxPolygonConductor.kt @@ -8,6 +8,7 @@ import com.mapconductor.core.polygon.PolygonEntity import com.mapconductor.core.polygon.PolygonEntityInterface import com.mapconductor.core.polygon.PolygonEvent import com.mapconductor.core.polygon.PolygonState +import com.mapconductor.core.polygon.PolygonTileSet import com.mapconductor.core.polygon.PolygonTiler import com.mapconductor.core.polyline.PolylineEntity import com.mapconductor.core.polyline.PolylineState @@ -85,9 +86,15 @@ class MapboxPolygonConductor( override suspend fun update(state: PolygonState) { tiler?.let { tiler -> if (tiler.isTiled(state.id)) { - polygonOverlay.polygonManager.registerEntity(PolygonEntity(polygon = IN_TILES, state = state)) - tiler.update(state) - return + if (PolygonTileSet.canTile(state)) { + polygonOverlay.polygonManager.registerEntity(PolygonEntity(polygon = IN_TILES, state = state)) + tiler.update(state) + return + } + // No longer drawable in a tile (geodesic, half the world wide, or + // under 3 points): the GeoJSON layers draw it from now on. Still + // on the map, so this is not an add. + tiler.remove(state.id) } } polygonOverlay.createPolygon(state)?.let { polygon -> From 99ef8c3cc2d4b0f4c071247613ac1ce891b3b68c Mon Sep 17 00:00:00 2001 From: Masashi Katsumata Date: Mon, 9 Nov 2026 06:55:03 -0800 Subject: [PATCH 4/4] Update the API baseline Additions only: the polygon tiles' stacking key, and declarations develop added without running apiDump (TILE_REFRESH_COALESCE, and on ArcGIS the marker controller's constructor that takes a boolean). --- api/android-for-mapbox.api | 2 ++ 1 file changed, 2 insertions(+) diff --git a/api/android-for-mapbox.api b/api/android-for-mapbox.api index 86164e0..9092445 100644 --- a/api/android-for-mapbox.api +++ b/api/android-for-mapbox.api @@ -216,6 +216,7 @@ public final class com.mapconductor.mapbox.MapboxOverlayStacking { public final com.mapconductor.core.controller.OverlayLayerStack stackOf(com.mapbox.maps.MapboxMap); public final com.mapconductor.core.controller.OverlayStacking$Key key(com.mapconductor.core.controller.OverlayKind, int); public final com.mapconductor.core.controller.OverlayStacking$Key getMARKER_TILE(); + public final com.mapconductor.core.controller.OverlayStacking$Key getPOLYGON_TILE(); public final com.mapconductor.core.controller.OverlayStacking$Key getMARKER_DRAG(); public final void add(com.mapbox.maps.MapboxMap, com.mapbox.maps.Style, com.mapbox.maps.extension.style.layers.Layer, com.mapconductor.core.controller.OverlayStacking$Key); public final void remove(com.mapbox.maps.MapboxMap, com.mapbox.maps.Style, java.lang.String); @@ -424,6 +425,7 @@ public final class com.mapconductor.mapbox.polygon.MapboxPolygonController exten public com.mapconductor.mapbox.polygon.MapboxPolygonController(com.mapconductor.mapbox.polygon.MapboxPolygonCompositeRenderer); public com.mapconductor.mapbox.polygon.MapboxPolygonCompositeRenderer getRenderer(); public int getZIndex(); + public void destroy(); public com.mapconductor.core.polygon.PolygonOverlayRendererInterface getRenderer(); } public final class com.mapconductor.mapbox.polygon.MapboxPolygonLayer {