From d8c0942cac0488a40ebd3b3cf2ddda525b723dd0 Mon Sep 17 00:00:00 2001 From: Masashi Katsumata Date: Sat, 7 Nov 2026 17:05:15 -0800 Subject: [PATCH] Extend core PolygonController for Mapbox polygons MapboxPolygonConductor implemented SlottedOverlayController by hand because it drives two renderers (fill + a polyline per outline, since a fill layer cannot draw a stroke wider than 1 px). Every change to core's PolygonController had to be copied over, and it was missed twice. Replace it with MapboxPolygonController, which extends core's PolygonController, and MapboxPolygonCompositeRenderer, which presents the existing fill and outline renderers as one renderer. The fill renderer's onRemove now redraws without the removed polygons, since core's clear() does not call onPostProcess. --- api/android-for-mapbox.api | 41 ++-- .../MapboxPolygonCompositeRenderer.kt.md | 37 +++ docs/polygon/MapboxPolygonConductor.kt.md | 221 ------------------ docs/polygon/MapboxPolygonController.kt.md | 30 +++ .../com/mapconductor/mapbox/MapboxMapView.kt | 18 +- .../mapbox/MapboxMapViewController.kt | 6 +- .../mapbox/MapboxMapViewControllerStyle.kt | 10 +- .../polygon/MapboxPolygonCompositeRenderer.kt | 78 +++++++ .../mapbox/polygon/MapboxPolygonConductor.kt | 152 ------------ .../mapbox/polygon/MapboxPolygonController.kt | 17 ++ .../polygon/MapboxPolygonOverlayRenderer.kt | 10 + 11 files changed, 209 insertions(+), 411 deletions(-) create mode 100644 docs/polygon/MapboxPolygonCompositeRenderer.kt.md delete mode 100644 docs/polygon/MapboxPolygonConductor.kt.md create mode 100644 docs/polygon/MapboxPolygonController.kt.md create mode 100644 src/main/java/com/mapconductor/mapbox/polygon/MapboxPolygonCompositeRenderer.kt delete mode 100644 src/main/java/com/mapconductor/mapbox/polygon/MapboxPolygonConductor.kt create mode 100644 src/main/java/com/mapconductor/mapbox/polygon/MapboxPolygonController.kt diff --git a/api/android-for-mapbox.api b/api/android-for-mapbox.api index 3527d83..7e1e793 100644 --- a/api/android-for-mapbox.api +++ b/api/android-for-mapbox.api @@ -107,8 +107,8 @@ public final class com.mapconductor.mapbox.MapboxMapDesignKt { public static final com.mapconductor.core.map.MapDesignTypeInterface toMapDesignType(com.mapbox.maps.Style); } public final class com.mapconductor.mapbox.MapboxMapViewController extends com.mapconductor.core.controller.BaseMapViewController implements MapboxMapViewControllerInterface,com.mapbox.maps.plugin.gestures.OnMapClickListener,com.mapbox.maps.plugin.gestures.OnMapLongClickListener,com.mapbox.maps.plugin.gestures.OnMoveListener { - public com.mapconductor.mapbox.MapboxMapViewController(com.mapconductor.mapbox.MapboxMapViewHolder, com.mapconductor.core.map.MapProjection, com.mapconductor.mapbox.marker.MapboxMarkerController, com.mapconductor.mapbox.polyline.MapboxPolylineController, com.mapconductor.mapbox.polygon.MapboxPolygonConductor, com.mapconductor.mapbox.groundimage.MapboxGroundImageController, com.mapconductor.mapbox.circle.MapboxCircleController, com.mapconductor.mapbox.raster.MapboxRasterLayerController, kotlinx.coroutines.CoroutineScope, kotlinx.coroutines.CoroutineScope); - public com.mapconductor.mapbox.MapboxMapViewController(com.mapconductor.mapbox.MapboxMapViewHolder, com.mapconductor.core.map.MapProjection, com.mapconductor.mapbox.marker.MapboxMarkerController, com.mapconductor.mapbox.polyline.MapboxPolylineController, com.mapconductor.mapbox.polygon.MapboxPolygonConductor, com.mapconductor.mapbox.groundimage.MapboxGroundImageController, com.mapconductor.mapbox.circle.MapboxCircleController, com.mapconductor.mapbox.raster.MapboxRasterLayerController, kotlinx.coroutines.CoroutineScope, kotlinx.coroutines.CoroutineScope, int, kotlin.jvm.internal.DefaultConstructorMarker); + public com.mapconductor.mapbox.MapboxMapViewController(com.mapconductor.mapbox.MapboxMapViewHolder, com.mapconductor.core.map.MapProjection, com.mapconductor.mapbox.marker.MapboxMarkerController, com.mapconductor.mapbox.polyline.MapboxPolylineController, com.mapconductor.mapbox.polygon.MapboxPolygonController, com.mapconductor.mapbox.groundimage.MapboxGroundImageController, com.mapconductor.mapbox.circle.MapboxCircleController, com.mapconductor.mapbox.raster.MapboxRasterLayerController, kotlinx.coroutines.CoroutineScope, kotlinx.coroutines.CoroutineScope); + public com.mapconductor.mapbox.MapboxMapViewController(com.mapconductor.mapbox.MapboxMapViewHolder, com.mapconductor.core.map.MapProjection, com.mapconductor.mapbox.marker.MapboxMarkerController, com.mapconductor.mapbox.polyline.MapboxPolylineController, com.mapconductor.mapbox.polygon.MapboxPolygonController, com.mapconductor.mapbox.groundimage.MapboxGroundImageController, com.mapconductor.mapbox.circle.MapboxCircleController, com.mapconductor.mapbox.raster.MapboxRasterLayerController, kotlinx.coroutines.CoroutineScope, kotlinx.coroutines.CoroutineScope, int, kotlin.jvm.internal.DefaultConstructorMarker); public com.mapconductor.mapbox.MapboxMapViewHolder getHolder(); public kotlinx.coroutines.CoroutineScope getMainCoroutine(); public kotlinx.coroutines.CoroutineScope getDefaultCoroutine(); @@ -191,7 +191,7 @@ public final class com.mapconductor.mapbox.MapboxMapViewHolder implements com.ma public final class com.mapconductor.mapbox.MapboxMapViewKt { public static final void MapboxMapView(com.mapconductor.mapbox.MapboxViewState, androidx.compose.ui.Modifier, com.mapconductor.core.marker.MarkerTilingOptions, com.mapconductor.core.map.CameraRestriction, kotlin.jvm.functions.Function2, ? extends java.lang.Object>, kotlin.jvm.functions.Function1, kotlin.Unit>, kotlin.jvm.functions.Function1, kotlin.jvm.functions.Function1, kotlin.jvm.functions.Function1, kotlin.jvm.functions.Function1, kotlin.jvm.functions.Function1, com.mapconductor.core.map.MapProjection, com.mapconductor.core.map.MapViewStyle, kotlin.jvm.functions.Function1, kotlin.Unit>, kotlin.jvm.functions.Function3, androidx.compose.runtime.Composer, int, int, int); public static final com.mapconductor.mapbox.MapboxMapViewController createMapboxViewController(com.mapconductor.mapbox.MapboxMapViewHolder, com.mapconductor.core.marker.MarkerTilingOptions, com.mapconductor.core.map.MapProjection, com.mapconductor.core.map.MutableMapServiceRegistry); - public static final com.mapconductor.mapbox.polygon.MapboxPolygonConductor getPolygonController(com.mapconductor.mapbox.MapboxMapViewHolder); + public static final com.mapconductor.mapbox.polygon.MapboxPolygonController getPolygonController(com.mapconductor.mapbox.MapboxMapViewHolder); public static final com.mapconductor.mapbox.circle.MapboxCircleController getCircleController(com.mapconductor.mapbox.MapboxMapViewHolder); public static final com.mapconductor.mapbox.polyline.MapboxPolylineController getPolylineController(com.mapconductor.mapbox.MapboxMapViewHolder); public static final com.mapconductor.mapbox.marker.MapboxMarkerController getMarkerController(com.mapconductor.mapbox.MapboxMapViewHolder, com.mapconductor.core.marker.MarkerTilingOptions); @@ -400,27 +400,22 @@ public class com.mapconductor.mapbox.marker.MarkerLayer { public final boolean wouldSkipDraw(java.util.List>); public final void draw(java.util.List>); } -public final class com.mapconductor.mapbox.polygon.MapboxPolygonConductor implements com.mapconductor.core.controller.SlottedOverlayController> { - public com.mapconductor.mapbox.polygon.MapboxPolygonConductor(com.mapconductor.mapbox.polygon.MapboxPolygonOverlayRenderer, com.mapconductor.mapbox.polyline.MapboxPolylineOverlayRenderer); - public final com.mapconductor.mapbox.polygon.MapboxPolygonOverlayRenderer getPolygonOverlay(); - public final com.mapconductor.mapbox.polyline.MapboxPolylineOverlayRenderer getPolylineOverlay(); - public int getZIndex(); - 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); - public final kotlin.jvm.functions.Function1 getClickListener(); - public final void setClickListener(kotlin.jvm.functions.Function1); - public com.mapconductor.core.polygon.PolygonEntityInterface find(com.mapconductor.core.features.GeoPointInterface); - public java.lang.Object clear(kotlin.coroutines.Continuation); - public com.mapconductor.core.controller.OverlayKind getKind(); - public boolean has(java.lang.String); - public void setClickListenerAny(java.lang.Object); - public com.mapconductor.core.controller.OverlayHit resolveTap(com.mapconductor.core.features.GeoPointInterface); - public void destroy(); - public java.lang.Object update(java.lang.Object, kotlin.coroutines.Continuation); - public java.lang.Object find(com.mapconductor.core.features.GeoPointInterface); +public final class com.mapconductor.mapbox.polygon.MapboxPolygonCompositeRenderer implements com.mapconductor.core.polygon.PolygonOverlayRendererInterface> { + public com.mapconductor.mapbox.polygon.MapboxPolygonCompositeRenderer(com.mapconductor.mapbox.polygon.MapboxPolygonOverlayRenderer, com.mapconductor.mapbox.polyline.MapboxPolylineOverlayRenderer); + public final com.mapconductor.mapbox.polygon.MapboxPolygonOverlayRenderer getFill(); + public final com.mapconductor.mapbox.polyline.MapboxPolylineOverlayRenderer getOutline(); + public java.lang.Object onAdd(java.util.List, kotlin.coroutines.Continuation>>); + public java.lang.Object onChange(java.util.List>>, kotlin.coroutines.Continuation>>); + public java.lang.Object onRemove(java.util.List>>, kotlin.coroutines.Continuation); + public java.lang.Object onPostProcess(kotlin.coroutines.Continuation); } -public final class com.mapconductor.mapbox.polygon.MapboxPolygonConductorKt { +public final class com.mapconductor.mapbox.polygon.MapboxPolygonCompositeRendererKt { +} +public final class com.mapconductor.mapbox.polygon.MapboxPolygonController extends com.mapconductor.core.polygon.PolygonController> { + public com.mapconductor.mapbox.polygon.MapboxPolygonController(com.mapconductor.mapbox.polygon.MapboxPolygonCompositeRenderer); + public com.mapconductor.mapbox.polygon.MapboxPolygonCompositeRenderer getRenderer(); + public int getZIndex(); + public com.mapconductor.core.polygon.PolygonOverlayRendererInterface getRenderer(); } public final class com.mapconductor.mapbox.polygon.MapboxPolygonLayer { public com.mapconductor.mapbox.polygon.MapboxPolygonLayer(java.lang.String, java.lang.String); diff --git a/docs/polygon/MapboxPolygonCompositeRenderer.kt.md b/docs/polygon/MapboxPolygonCompositeRenderer.kt.md new file mode 100644 index 0000000..4542f73 --- /dev/null +++ b/docs/polygon/MapboxPolygonCompositeRenderer.kt.md @@ -0,0 +1,37 @@ +# MapboxPolygonCompositeRenderer + +## Signature + +```kotlin +class MapboxPolygonCompositeRenderer( + val fill: MapboxPolygonOverlayRenderer, + val outline: MapboxPolylineOverlayRenderer, +) : PolygonOverlayRendererInterface +``` + +## Description + +Presents the fill renderer and the outline renderer to the core `PolygonController` as a single +renderer. + +A Mapbox fill layer can only draw a 1 px outline with no width, so to honour `strokeWidth` each +polygon's outer ring is also drawn as a closed polyline with the id `outline-` through +`outline`. Every call the controller makes on this renderer is forwarded to `fill` and mirrored on +`outline`: + +- `onAdd` / `onChange`: the fill is created or rebuilt; when that succeeds, the outline polyline is + (re)created with the polygon's `strokeColor`, `strokeWidth`, `geodesic`, `zIndex` and `extra`. +- `onRemove`: the outline polylines are dropped and both sources are redrawn without the removed + polygons (the controller's `clear()` does not call `onPostProcess`). +- `onPostProcess`: both sources are redrawn. + +Holes are not outlined. + +## Constructor Parameters + +- `fill` + - Type: `MapboxPolygonOverlayRenderer` + - Description: Draws the polygon fill (with holes) into the fill source. +- `outline` + - Type: `MapboxPolylineOverlayRenderer` + - Description: Draws the outlines into the polygon outline source. diff --git a/docs/polygon/MapboxPolygonConductor.kt.md b/docs/polygon/MapboxPolygonConductor.kt.md deleted file mode 100644 index 3002a71..0000000 --- a/docs/polygon/MapboxPolygonConductor.kt.md +++ /dev/null @@ -1,221 +0,0 @@ -# MapboxPolygonConductor - -## Class: `MapboxPolygonConductor` - -### Description - -The `MapboxPolygonConductor` class is a controller responsible for managing and rendering polygon -overlays on a Mapbox map. It implements the `OverlayControllerInterface` to handle the lifecycle of -polygon features, including their filled areas and outlines. - -This conductor uses two distinct renderers: one for the polygon fills -(`MapboxPolygonOverlayRenderer`) and another for the polyline outlines -(`MapboxPolylineOverlayRenderer`). It orchestrates these renderers to add, update, clear, and find -polygons, as well as to handle user interactions like clicks. - -### Signature - -```kotlin -class MapboxPolygonConductor( - val polygonOverlay: MapboxPolygonOverlayRenderer, - val polylineOverlay: MapboxPolylineOverlayRenderer, -) : OverlayControllerInterface< - PolygonState, - PolygonEntityInterface, - PolygonEvent, - > -``` - -### Parameters - -- `polygonOverlay` - - Type: `MapboxPolygonOverlayRenderer` - - Description: The renderer responsible for drawing the filled area of the polygons. -- `polylineOverlay` - - Type: `MapboxPolylineOverlayRenderer` - - Description: The renderer responsible for drawing the outlines (strokes) of the polygons. - -## Properties - -### zIndex - -The stacking order of this overlay controller on the map. Overlays with higher z-indices are drawn -on top of those with lower indices. - -**Signature** -```kotlin -override val zIndex: Int = 2 -``` - -### clickListener - -A callback function that is invoked when any polygon managed by this conductor is clicked. This -provides a centralized way to handle click events for all polygons. - -**Signature** -```kotlin -override var clickListener: ((PolygonEvent) -> Unit)? = null -``` - -## Methods - -### add - -Adds a list of polygons to the map. This method performs a diffing operation: it removes any -previously added polygons that are not in the new `data` list and then adds or updates the polygons -specified in the list. For each polygon, it also creates a corresponding closed polyline to serve as -its outline. - -**Signature** -```kotlin -override suspend fun add(data: List) -``` - -**Parameters** - -- `data` - - Type: `List` - - Description: A list of `PolygonState` objects representing the polygons to be displayed on the - map. - -### update - -Adds or updates a single polygon on the map. If a polygon with the same ID already exists, it will -be replaced with the new state. A corresponding outline is also created or updated. - -**Signature** -```kotlin -override suspend fun update(state: PolygonState) -``` - -**Parameters** - -- `state` - - Type: `PolygonState` - - Description: The state of the single polygon to add or update. - -### dispatchClick - -Dispatches a click event. This method is typically called by the underlying map framework when a tap -is detected on a polygon. It triggers two callbacks in order: -1. The `onClick` handler defined within the specific `PolygonState` of the clicked polygon. -2. The global `clickListener` set on this `MapboxPolygonConductor` instance. - -**Signature** -```kotlin -fun dispatchClick(event: PolygonEvent) -``` - -**Parameters** - -- `event` - - Type: `PolygonEvent` - - Description: The event object containing details about the click, including the state of the - clicked polygon. - -### find - -Finds the topmost polygon entity at a given geographic coordinate. - -**Signature** -```kotlin -override fun find(position: GeoPointInterface): PolygonEntityInterface? -``` - -**Parameters** - -- `position` - - Type: `GeoPointInterface` - - Description: The geographic coordinates (latitude and longitude) to search at. - -**Returns** - -`PolygonEntityInterface?` — The found polygon entity, or `null` if no polygon exists -at the specified position. - -### clear - -Removes all polygons and their associated outlines from the map that are managed by this conductor. - -**Signature** -```kotlin -override suspend fun clear() -``` - -### onCameraChanged - -A lifecycle method called when the map's camera position changes. This implementation is currently -empty as no action is required for polygons on camera change. - -**Signature** -```kotlin -override suspend fun onCameraChanged(mapCameraPosition: MapCameraPosition) -``` - -**Parameters** - -- `mapCameraPosition` - - Type: `MapCameraPosition` - - Description: The new camera position of the map. - -### destroy - -Cleans up resources used by the controller. This implementation is empty as there are no specific -native resources to release for polygons. - -**Signature** -```kotlin -override fun destroy() -``` - -## Example - -Here is an example of how to instantiate `MapboxPolygonConductor` and use it to manage polygons on a -map. - -```kotlin -// Assume polygonOverlay and polylineOverlay are already initialized -val polygonConductor = MapboxPolygonConductor(polygonOverlay, polylineOverlay) - -// Set a global click listener for all polygons -polygonConductor.clickListener = { event -> - println("Polygon with ID ${event.state.id} was clicked!") -} - -// Define the state for two polygons -val polygon1State = PolygonState( - id = "p1", - points = listOf( - GeoPoint(40.7128, -74.0060), // New York - GeoPoint(34.0522, -118.2437), // Los Angeles - GeoPoint(29.7604, -95.3698) // Houston - ), - fillColor = Color.argb(100, 0, 0, 255), // Semi-transparent blue - strokeColor = Color.BLUE, - strokeWidth = 2f, - onClick = { event -> - println("Specific onClick for polygon ${event.state.id}") - } -) - -val polygon2State = PolygonState( - id = "p2", - points = listOf( - GeoPoint(41.8781, -87.6298), // Chicago - GeoPoint(39.9526, -75.1652), // Philadelphia - GeoPoint(33.4484, -112.0740) // Phoenix - ), - fillColor = Color.argb(100, 255, 0, 0) // Semi-transparent red -) - -// Add the polygons to the map -// This must be called from a coroutine scope -coroutineScope.launch { - polygonConductor.add(listOf(polygon1State, polygon2State)) -} - -// ... later, to remove all polygons -coroutineScope.launch { - polygonConductor.clear() -} -``` diff --git a/docs/polygon/MapboxPolygonController.kt.md b/docs/polygon/MapboxPolygonController.kt.md new file mode 100644 index 0000000..5327ef3 --- /dev/null +++ b/docs/polygon/MapboxPolygonController.kt.md @@ -0,0 +1,30 @@ +# MapboxPolygonController + +## Signature + +```kotlin +class MapboxPolygonController( + override val renderer: MapboxPolygonCompositeRenderer, +) : PolygonController(renderer.fill.polygonManager, renderer) +``` + +## Description + +The polygon controller of the Mapbox provider. It extends the core `PolygonController`, so diffing +(add / change / remove), `has`, `find`, the click cascade (`resolveTap`, `dispatchClick`), +`clear` and `destroy` are the core implementation shared with every other provider. + +The only provider-specific part is the renderer: a fill layer can only draw a 1 px outline, so the +outline is drawn as a separate polyline by [`MapboxPolygonCompositeRenderer`](./MapboxPolygonCompositeRenderer.kt.md). + +## Constructor Parameters + +- `renderer` + - Type: `MapboxPolygonCompositeRenderer` + - Description: Draws both the fill and the outline. Its `fill.polygonManager` is used as the + controller's polygon manager, so the controller and the fill renderer always see the same + entities. + +## Properties + +- `zIndex`: `2` (unchanged from the former `MapboxPolygonConductor`). diff --git a/src/main/java/com/mapconductor/mapbox/MapboxMapView.kt b/src/main/java/com/mapconductor/mapbox/MapboxMapView.kt index f2a0f8d..dd8ef22 100644 --- a/src/main/java/com/mapconductor/mapbox/MapboxMapView.kt +++ b/src/main/java/com/mapconductor/mapbox/MapboxMapView.kt @@ -46,7 +46,8 @@ import com.mapconductor.mapbox.groundimage.MapboxGroundImageOverlayRenderer import com.mapconductor.mapbox.marker.MapboxMarkerController import com.mapconductor.mapbox.marker.MarkerDragLayer import com.mapconductor.mapbox.marker.MarkerLayer -import com.mapconductor.mapbox.polygon.MapboxPolygonConductor +import com.mapconductor.mapbox.polygon.MapboxPolygonCompositeRenderer +import com.mapconductor.mapbox.polygon.MapboxPolygonController import com.mapconductor.mapbox.polygon.MapboxPolygonLayer import com.mapconductor.mapbox.polygon.MapboxPolygonOverlayRenderer import com.mapconductor.mapbox.polyline.MapboxPolylineController @@ -304,7 +305,7 @@ fun createMapboxViewController( return mapController } -internal fun getPolygonController(holder: MapboxMapViewHolder): MapboxPolygonConductor { +internal fun getPolygonController(holder: MapboxMapViewHolder): MapboxPolygonController { val polylineLayer = MapboxPolylineLayer( sourceId = "polygon-outline-source", @@ -331,12 +332,15 @@ internal fun getPolygonController(holder: MapboxMapViewHolder): MapboxPolygonCon holder = holder, ) - val conductor = - MapboxPolygonConductor( - polygonOverlay = polygonOverlayRenderer, - polylineOverlay = polylineOverlayRenderer, + val controller = + MapboxPolygonController( + renderer = + MapboxPolygonCompositeRenderer( + fill = polygonOverlayRenderer, + outline = polylineOverlayRenderer, + ), ) - return conductor + return controller } internal fun getCircleController(holder: MapboxMapViewHolder): MapboxCircleController { diff --git a/src/main/java/com/mapconductor/mapbox/MapboxMapViewController.kt b/src/main/java/com/mapconductor/mapbox/MapboxMapViewController.kt index e10bc14..e305c46 100644 --- a/src/main/java/com/mapconductor/mapbox/MapboxMapViewController.kt +++ b/src/main/java/com/mapconductor/mapbox/MapboxMapViewController.kt @@ -43,7 +43,7 @@ import com.mapconductor.mapbox.marker.MapboxMarkerController import com.mapconductor.mapbox.marker.MapboxMarkerOverlayRenderer import com.mapconductor.mapbox.marker.MarkerDragLayer import com.mapconductor.mapbox.marker.MarkerLayer -import com.mapconductor.mapbox.polygon.MapboxPolygonConductor +import com.mapconductor.mapbox.polygon.MapboxPolygonController import com.mapconductor.mapbox.polyline.MapboxPolylineController import com.mapconductor.mapbox.raster.MapboxRasterLayerController import java.util.UUID @@ -68,7 +68,7 @@ class MapboxMapViewController internal constructor( internal var projection: MapProjection, internal val markerController: MapboxMarkerController, internal val polylineController: MapboxPolylineController, - internal val polygonController: MapboxPolygonConductor, + internal val polygonController: MapboxPolygonController, internal val groundImageController: MapboxGroundImageController, internal val circleController: MapboxCircleController, internal val rasterLayerController: MapboxRasterLayerController, @@ -298,7 +298,7 @@ class MapboxMapViewController internal constructor( .hasEntity(state.id) override fun hasPolygon(state: PolygonState): Boolean = - this.polygonController.polygonOverlay.polygonManager + this.polygonController.polygonManager .hasEntity(state.id) override fun applyUISettings(settings: MapUISettings) { diff --git a/src/main/java/com/mapconductor/mapbox/MapboxMapViewControllerStyle.kt b/src/main/java/com/mapconductor/mapbox/MapboxMapViewControllerStyle.kt index 5137b98..cbbc2d0 100644 --- a/src/main/java/com/mapconductor/mapbox/MapboxMapViewControllerStyle.kt +++ b/src/main/java/com/mapconductor/mapbox/MapboxMapViewControllerStyle.kt @@ -37,8 +37,8 @@ internal fun MapboxMapViewController.attachMarkerLayers( internal fun MapboxMapViewController.attachOverlaySourcesAndLayers(style: com.mapbox.maps.Style) { // Polygon sources only (z-indexed layers added below) - style.addSource(polygonController.polylineOverlay.layer.source) - style.addSource(polygonController.polygonOverlay.layer.source) + style.addSource(polygonController.renderer.outline.layer.source) + style.addSource(polygonController.renderer.fill.layer.source) // Circle fill (fill pass) then stroke (line pass) — both below general polyline style.addSource(circleController.renderer.layer.source) @@ -61,12 +61,12 @@ internal fun MapboxMapViewController.attachOverlaySourcesAndLayers(style: com.ma } internal fun MapboxMapViewController.ensurePolygonZLayers(style: com.mapbox.maps.Style) { - val fillSourceId = polygonController.polygonOverlay.layer.sourceId - val outlineSourceId = polygonController.polylineOverlay.layer.sourceId + val fillSourceId = polygonController.renderer.fill.layer.sourceId + val outlineSourceId = polygonController.renderer.outline.layer.sourceId val anchorId = polylineController.renderer.layer.layerId val zSet = - polygonController.polygonOverlay.polygonManager + polygonController.polygonManager .allEntities() .map { it.state.zIndex } .toSet() diff --git a/src/main/java/com/mapconductor/mapbox/polygon/MapboxPolygonCompositeRenderer.kt b/src/main/java/com/mapconductor/mapbox/polygon/MapboxPolygonCompositeRenderer.kt new file mode 100644 index 0000000..6bf3565 --- /dev/null +++ b/src/main/java/com/mapconductor/mapbox/polygon/MapboxPolygonCompositeRenderer.kt @@ -0,0 +1,78 @@ +package com.mapconductor.mapbox.polygon + +import com.mapconductor.core.polygon.PolygonEntityInterface +import com.mapconductor.core.polygon.PolygonOverlayRendererInterface +import com.mapconductor.core.polygon.PolygonState +import com.mapconductor.core.polyline.PolylineEntity +import com.mapconductor.core.polyline.PolylineState +import com.mapconductor.mapbox.MapboxActualPolygon +import com.mapconductor.mapbox.polyline.MapboxPolylineOverlayRenderer + +/** + * 塗りと輪郭を 1 つのレンダラとして [com.mapconductor.core.polygon.PolygonController] に渡すための合成レンダラ。 + * + * fill レイヤーは幅 1 px の輪郭しか描けないので、`strokeWidth` を守るために輪郭は + * 別のポリライン(`outline-`)として [outline] で描く。差分計算はコアの + * PolygonController に任せ、ここは「塗りへの指示を輪郭にも写す」ことだけをする。 + * + * 塗りのエンティティはコントローラと同じ [fill] の polygonManager に載る。輪郭は + * [outline] の polylineManager に載り、コントローラからは見えない。 + */ +class MapboxPolygonCompositeRenderer( + val fill: MapboxPolygonOverlayRenderer, + val outline: MapboxPolylineOverlayRenderer, +) : PolygonOverlayRendererInterface { + override suspend fun onAdd(data: List): List { + val polygons = fill.onAdd(data) + data.forEachIndexed { index, params -> + if (polygons[index] != null) putOutline(params.state) + } + return polygons + } + + override suspend fun onChange( + data: List>, + ): List { + val polygons = fill.onChange(data) + data.forEachIndexed { index, params -> + if (polygons[index] != null) putOutline(params.current.state) + } + return polygons + } + + override suspend fun onRemove(data: List>) { + data.forEach { outline.polylineManager.removeEntity(outlineId(it.state.id)) } + // clear() は onPostProcess を呼ばないので、消した結果はここで描き直す。 + fill.onRemove(data) + outline.onPostProcess() + } + + override suspend fun onPostProcess() { + fill.onPostProcess() + outline.onPostProcess() + } + + private suspend fun putOutline(state: PolygonState) { + val outlineState = state.toOutlineState() ?: return + outline.createPolyline(outlineState)?.let { polyline -> + outline.polylineManager.registerEntity(PolylineEntity(polyline = polyline, state = outlineState)) + } + } +} + +private fun outlineId(polygonId: String): String = "outline-$polygonId" + +/** 外周だけを閉じたポリラインにする(穴の輪郭は描かない)。 */ +private fun PolygonState.toOutlineState(): PolylineState? { + if (points.isEmpty()) return null + val closedPoints = if (points.first() != points.last()) points + points.first() else points + return PolylineState( + points = closedPoints, + id = outlineId(id), + strokeColor = strokeColor, + strokeWidth = strokeWidth, + geodesic = geodesic, + zIndex = zIndex, + extra = extra, + ) +} diff --git a/src/main/java/com/mapconductor/mapbox/polygon/MapboxPolygonConductor.kt b/src/main/java/com/mapconductor/mapbox/polygon/MapboxPolygonConductor.kt deleted file mode 100644 index d797101..0000000 --- a/src/main/java/com/mapconductor/mapbox/polygon/MapboxPolygonConductor.kt +++ /dev/null @@ -1,152 +0,0 @@ -package com.mapconductor.mapbox.polygon - -import com.mapconductor.core.controller.OverlayHit -import com.mapconductor.core.controller.OverlayKind -import com.mapconductor.core.controller.SlottedOverlayController -import com.mapconductor.core.features.GeoPointInterface -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.polyline.PolylineEntity -import com.mapconductor.core.polyline.PolylineState -import com.mapconductor.mapbox.polyline.MapboxPolylineOverlayRenderer - -class MapboxPolygonConductor( - val polygonOverlay: MapboxPolygonOverlayRenderer, - val polylineOverlay: MapboxPolylineOverlayRenderer, -) : SlottedOverlayController< - PolygonState, - PolygonEntityInterface, - > { - override val zIndex: Int = 2 - - override suspend fun add(data: List) { - val nextIds = data.asSequence().map { it.id }.toSet() - val prevIds = - polygonOverlay.polygonManager - .allEntities() - .asSequence() - .map { it.state.id } - .toSet() - val removeIds = prevIds - nextIds - - removeIds.forEach { id -> - polygonOverlay.polygonManager.removeEntity(id) - polylineOverlay.polylineManager.removeEntity("outline-$id") - } - - data.forEach { polygonState -> - polygonOverlay.createPolygon(polygonState)?.let { polygon -> - val polygonEntity = - PolygonEntity( - polygon = polygon, - state = polygonState, - ) - polygonOverlay.polygonManager.registerEntity(polygonEntity) - } - - val polylineState = polygonState.toPolylineState() - polylineOverlay.createPolyline(polylineState)?.let { polyline -> - val polylineEntity = - PolylineEntity( - polyline = polyline, - state = polylineState, - ) - polylineOverlay.polylineManager.registerEntity(polylineEntity) - } - } - polygonOverlay.onPostProcess() - polylineOverlay.onPostProcess() - } - - override suspend fun update(state: PolygonState) { - polygonOverlay.createPolygon(state)?.let { polygon -> - val polygonEntity = - PolygonEntity( - polygon = polygon, - state = state, - ) - polygonOverlay.polygonManager.registerEntity(polygonEntity) - } - - val polylineState = state.toPolylineState() - polylineOverlay.createPolyline(polylineState)?.let { polyline -> - val polylineEntity = - PolylineEntity( - polyline = polyline, - state = polylineState, - ) - polylineOverlay.polylineManager.registerEntity(polylineEntity) - } - polygonOverlay.onPostProcess() - polylineOverlay.onPostProcess() - } - - fun dispatchClick(event: PolygonEvent) { - // 配送座標の wrap は PolygonEvent の生成時に一元化済み。 - event.state.onClick?.invoke(event) - clickListener?.invoke(event) - } - - var clickListener: ((PolygonEvent) -> Unit)? = null - - @Suppress("UNCHECKED_CAST") - override fun find(position: GeoPointInterface): PolygonEntityInterface? = - polygonOverlay.polygonManager.find(position) as? PolygonEntityInterface - - override suspend fun clear() { - polygonOverlay.polygonManager.clear() - polylineOverlay.polylineManager.clear() - polygonOverlay.onPostProcess() - polylineOverlay.onPostProcess() - } - - // ── Capable ファサードのスロットとクリックカスケードへの参加 ────────── - // - // このコンダクタはコアの PolygonController を継承していない(ポリゴンの輪郭を - // ポリラインとして描くために polygon と polyline の 2 レンダラを束ねている)。 - // それでも扱いは他プロバイダのポリゴンと同じでなければならないので、 - // スロットの契約はここで自前で満たす。 - // - // 実際、Step 4 でこれを実装し忘れて `hasPolygon` が常に false になり、 - // ポリゴン単体の状態更新が黙って捨てられていた。 - - override val kind: OverlayKind = OverlayKind.Polygon - - override fun has(id: String): Boolean = polygonOverlay.polygonManager.hasEntity(id) - - @Suppress("UNCHECKED_CAST") - override fun setClickListenerAny(listener: Any?) { - clickListener = listener as? ((PolygonEvent) -> Unit) - } - - override fun resolveTap(position: GeoPointInterface): OverlayHit? = - find(position)?.let { entity -> - OverlayHit(OverlayKind.Polygon, position) { - dispatchClick(PolygonEvent(entity.state, position)) - } - } - - override fun destroy() { - // No native resources to clean up for polygons - } -} - -private fun PolygonState.toPolylineState(): PolylineState { - val closedPoints = - if (points.first() != points.last()) { - points + points.first() - } else { - points - } - return PolylineState( - points = closedPoints, - id = "outline-${this.id}", - strokeColor = this.strokeColor, - strokeWidth = this.strokeWidth, - geodesic = this.geodesic, - zIndex = this.zIndex, - extra = this.extra, - ) -} diff --git a/src/main/java/com/mapconductor/mapbox/polygon/MapboxPolygonController.kt b/src/main/java/com/mapconductor/mapbox/polygon/MapboxPolygonController.kt new file mode 100644 index 0000000..e67ac2c --- /dev/null +++ b/src/main/java/com/mapconductor/mapbox/polygon/MapboxPolygonController.kt @@ -0,0 +1,17 @@ +package com.mapconductor.mapbox.polygon + +import com.mapconductor.core.polygon.PolygonController +import com.mapconductor.mapbox.MapboxActualPolygon + +/** + * Mapbox 用のポリゴンコントローラ。 + * + * 追加・更新・削除の差分計算、`has`、クリックカスケードはコアの [PolygonController] が担う。 + * 塗りと輪郭(幅のある LineLayer)の 2 系統の描画は [MapboxPolygonCompositeRenderer] が束ねる。 + */ +class MapboxPolygonController( + override val renderer: MapboxPolygonCompositeRenderer, +) : PolygonController(renderer.fill.polygonManager, renderer) { + // 旧 MapboxPolygonConductor と同じ値を保つ。 + override val zIndex: Int = 2 +} diff --git a/src/main/java/com/mapconductor/mapbox/polygon/MapboxPolygonOverlayRenderer.kt b/src/main/java/com/mapconductor/mapbox/polygon/MapboxPolygonOverlayRenderer.kt index 51a5bc8..853670c 100644 --- a/src/main/java/com/mapconductor/mapbox/polygon/MapboxPolygonOverlayRenderer.kt +++ b/src/main/java/com/mapconductor/mapbox/polygon/MapboxPolygonOverlayRenderer.kt @@ -17,7 +17,10 @@ class MapboxPolygonOverlayRenderer( override val holder: MapboxMapViewHolder, override val coroutine: CoroutineScope = CoroutineScope(Dispatchers.Main), ) : AbstractPolygonOverlayRenderer() { + // 消えた分を除いた残りで描き直す。コントローラはこの呼び出しの後で polygonManager から + // 外すので(clear() では onPostProcess も来ない)、ここで除外しておかないと消えずに残る。 override suspend fun onRemove(data: List>) { + layer.draw(getRemainingPolygonEntities(data)) } override suspend fun onPostProcess() { @@ -61,4 +64,11 @@ class MapboxPolygonOverlayRenderer( private fun getAllPolygonEntities(): List> = polygonManager.allEntities() + + private fun getRemainingPolygonEntities( + removed: List>, + ): List> { + val removedIds = removed.mapTo(HashSet()) { it.state.id } + return polygonManager.allEntities().filter { it.state.id !in removedIds } + } }