From 18f5187ae2f09fa16c3bd7f6809537b6cec2434c 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-mappls.api | 2 ++ .../mappls/MapplsViewController.kt | 16 +++++++++ .../mappls/polygon/MapplsPolygonConductor.kt | 33 +++++++++++++++++++ .../MapplsRasterLayerOverlayRenderer.kt | 31 ++++++++++++++++- 4 files changed, 81 insertions(+), 1 deletion(-) diff --git a/api/android-for-mappls.api b/api/android-for-mappls.api index 31bc400..80ab732 100644 --- a/api/android-for-mappls.api +++ b/api/android-for-mappls.api @@ -370,6 +370,8 @@ public final class com.mapconductor.mappls.polygon.MapplsPolygonConductor implem public final com.mapconductor.mappls.polygon.MapplsPolygonOverlayRenderer getPolygonOverlay(); public final com.mapconductor.mappls.polyline.MapplsPolylineOverlayRenderer getPolylineOverlay(); public int getZIndex(); + public final com.mapconductor.core.polygon.PolygonTiler getTiler(); + public final void setTiler(com.mapconductor.core.polygon.PolygonTiler); public final kotlin.jvm.functions.Function1 getClickListener(); public final void setClickListener(kotlin.jvm.functions.Function1); public java.lang.Object add(java.util.List, kotlin.coroutines.Continuation); diff --git a/src/main/java/com/mapconductor/mappls/MapplsViewController.kt b/src/main/java/com/mapconductor/mappls/MapplsViewController.kt index e14a612..708c16c 100644 --- a/src/main/java/com/mapconductor/mappls/MapplsViewController.kt +++ b/src/main/java/com/mapconductor/mappls/MapplsViewController.kt @@ -17,7 +17,10 @@ import com.mapconductor.core.marker.OnMarkerEventHandler import com.mapconductor.core.marker.StrategyMarkerController import com.mapconductor.core.marker.dispatchGeoMarkerClick import com.mapconductor.core.polygon.OnPolygonEventHandler +import com.mapconductor.core.polygon.POLYGON_TILE_LAYER_ID import com.mapconductor.core.polygon.PolygonState +import com.mapconductor.core.polygon.PolygonTiler +import com.mapconductor.core.polygon.polygonTileRasterLayerState import com.mapconductor.core.polyline.OnPolylineEventHandler import com.mapconductor.mappls.circle.MapplsCircleController import com.mapconductor.mappls.groundimage.MapplsGroundImageController @@ -105,6 +108,19 @@ class MapplsViewController( 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) + } + } + } } fun setupListeners() { diff --git a/src/main/java/com/mapconductor/mappls/polygon/MapplsPolygonConductor.kt b/src/main/java/com/mapconductor/mappls/polygon/MapplsPolygonConductor.kt index 25bb1a9..25c5bfd 100644 --- a/src/main/java/com/mapconductor/mappls/polygon/MapplsPolygonConductor.kt +++ b/src/main/java/com/mapconductor/mappls/polygon/MapplsPolygonConductor.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.mappls.polyline.MapplsPolylineOverlayRenderer +import kotlinx.coroutines.Dispatchers +import kotlinx.coroutines.withContext class MapplsPolygonConductor( val polygonOverlay: MapplsPolygonOverlayRenderer, @@ -21,6 +24,13 @@ class MapplsPolygonConductor( > { 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 + var clickListener: ((PolygonEvent) -> Unit)? = null override suspend fun add(data: List) { @@ -38,7 +48,19 @@ class MapplsPolygonConductor( 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 = @@ -64,6 +86,13 @@ class MapplsPolygonConductor( } 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( @@ -97,6 +126,7 @@ class MapplsPolygonConductor( polygonOverlay.polygonManager.find(position) as? PolygonEntityInterface override suspend fun clear() { + tiler?.clear() polygonOverlay.polygonManager.clear() polylineOverlay.polylineManager.clear() polygonOverlay.onPostProcess() @@ -134,6 +164,9 @@ class MapplsPolygonConductor( } } +/** 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/mappls/raster/MapplsRasterLayerOverlayRenderer.kt b/src/main/java/com/mapconductor/mappls/raster/MapplsRasterLayerOverlayRenderer.kt index 60512e3..ad7c352 100644 --- a/src/main/java/com/mapconductor/mappls/raster/MapplsRasterLayerOverlayRenderer.kt +++ b/src/main/java/com/mapconductor/mappls/raster/MapplsRasterLayerOverlayRenderer.kt @@ -1,5 +1,6 @@ package com.mapconductor.mappls.raster +import com.mapconductor.core.polygon.POLYGON_TILE_LAYER_ID import com.mapconductor.core.raster.RasterLayerEntityInterface import com.mapconductor.core.raster.RasterLayerOverlayRendererInterface import com.mapconductor.core.raster.RasterLayerSource @@ -24,6 +25,8 @@ class MapplsRasterLayerOverlayRenderer( 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 + override suspend fun onAdd( data: List, ): List = @@ -96,6 +99,8 @@ class MapplsRasterLayerOverlayRenderer( try { if (isMarkerTileRaster(state)) { addLayerForMarkerTile(style, layer) + } else if (isPolygonTileRaster(state)) { + addLayerForPolygonTile(style, layer) } else { addBelowBasemapLabels(style, layer) } @@ -105,6 +110,26 @@ class MapplsRasterLayerOverlayRenderer( return handle } + /** + * 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.mappls.sdk.maps.Style, + layer: com.mappls.sdk.maps.style.layers.Layer, + ) { + val anchor = style.layers.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.mappls.sdk.maps.Style, layer: RasterLayer, @@ -159,7 +184,7 @@ class MapplsRasterLayerOverlayRenderer( 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() @@ -262,6 +287,10 @@ class MapplsRasterLayerOverlayRenderer( 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 f02f6416c93db2e44e3c98006463d8f81fa8c376 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/mappls/polygon/MapplsPolygonConductor.kt | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/src/main/java/com/mapconductor/mappls/polygon/MapplsPolygonConductor.kt b/src/main/java/com/mapconductor/mappls/polygon/MapplsPolygonConductor.kt index 25c5bfd..5ebf667 100644 --- a/src/main/java/com/mapconductor/mappls/polygon/MapplsPolygonConductor.kt +++ b/src/main/java/com/mapconductor/mappls/polygon/MapplsPolygonConductor.kt @@ -160,7 +160,10 @@ class MapplsPolygonConductor( } 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 eafdc6e19466f551c0c8839318437d21951953ff 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. --- .../mappls/polygon/MapplsPolygonConductor.kt | 13 ++++++++++--- 1 file changed, 10 insertions(+), 3 deletions(-) diff --git a/src/main/java/com/mapconductor/mappls/polygon/MapplsPolygonConductor.kt b/src/main/java/com/mapconductor/mappls/polygon/MapplsPolygonConductor.kt index 5ebf667..87f5881 100644 --- a/src/main/java/com/mapconductor/mappls/polygon/MapplsPolygonConductor.kt +++ b/src/main/java/com/mapconductor/mappls/polygon/MapplsPolygonConductor.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 @@ -88,9 +89,15 @@ class MapplsPolygonConductor( 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 1d52e9d1f0043aea748b8ee3822dcf636550714f 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-mappls.api | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/api/android-for-mappls.api b/api/android-for-mappls.api index c052daa..4db1462 100644 --- a/api/android-for-mappls.api +++ b/api/android-for-mappls.api @@ -113,6 +113,7 @@ public final class com.mapconductor.mappls.MapplsOverlayStacking { public final com.mapconductor.core.controller.OverlayLayerStack stackOf(com.mappls.sdk.maps.MapplsMap); 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.mappls.sdk.maps.MapplsMap, com.mappls.sdk.maps.Style, com.mappls.sdk.maps.style.layers.Layer, com.mapconductor.core.controller.OverlayStacking$Key); public final void remove(com.mappls.sdk.maps.MapplsMap, com.mappls.sdk.maps.Style, java.lang.String); @@ -312,6 +313,9 @@ public final class com.mapconductor.mappls.marker.MapplsMarkerController extends public void destroy(); public java.lang.Object update(java.lang.Object, kotlin.coroutines.Continuation); } +final class com.mapconductor.mappls.marker.MapplsMarkerController$Companion { + public final long getTILE_REFRESH_COALESCE-UwyO8pc(); +} public final class com.mapconductor.mappls.marker.MapplsMarkerOverlayRenderer extends com.mapconductor.core.marker.AbstractMarkerOverlayRenderer { public com.mapconductor.mappls.marker.MapplsMarkerOverlayRenderer(com.mapconductor.mappls.MapplsMapViewHolderInterface, com.mapconductor.core.marker.MarkerManager, com.mapconductor.mappls.marker.MarkerLayer, com.mapconductor.mappls.marker.MarkerDragLayer, kotlinx.coroutines.CoroutineScope); public com.mapconductor.mappls.marker.MapplsMarkerOverlayRenderer(com.mapconductor.mappls.MapplsMapViewHolderInterface, com.mapconductor.core.marker.MarkerManager, com.mapconductor.mappls.marker.MarkerLayer, com.mapconductor.mappls.marker.MarkerDragLayer, kotlinx.coroutines.CoroutineScope, int, kotlin.jvm.internal.DefaultConstructorMarker); @@ -389,6 +393,7 @@ public final class com.mapconductor.mappls.polygon.MapplsPolygonController exten public com.mapconductor.mappls.polygon.MapplsPolygonController(com.mapconductor.mappls.polygon.MapplsPolygonCompositeRenderer); public com.mapconductor.mappls.polygon.MapplsPolygonCompositeRenderer getRenderer(); public int getZIndex(); + public void destroy(); public com.mapconductor.core.polygon.PolygonOverlayRendererInterface getRenderer(); } public final class com.mapconductor.mappls.polygon.MapplsPolygonLayer {