From ddc164189ebde9bad65820f7b4d7735771bbfef2 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 MapLibre polygons MapLibrePolygonConductor 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 MapLibrePolygonController, which extends core's PolygonController, and MapLibrePolygonCompositeRenderer, 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-maplibre.api | 41 ++- docs/MapLibreViewControllerImpl.kt.md | 4 +- .../MapLibrePolygonCompositeRenderer.kt.md | 37 +++ docs/polygon/MapLibrePolygonConductor.kt.md | 244 ------------------ docs/polygon/MapLibrePolygonController.kt.md | 30 +++ .../mapconductor/maplibre/MapLibreMapView.kt | 14 +- .../maplibre/MapLibreViewController.kt | 4 +- .../maplibre/MapLibreViewControllerStyle.kt | 14 +- .../MapLibrePolygonCompositeRenderer.kt | 78 ++++++ .../polygon/MapLibrePolygonConductor.kt | 153 ----------- .../polygon/MapLibrePolygonController.kt | 17 ++ .../polygon/MapLibrePolygonOverlayRenderer.kt | 16 +- 12 files changed, 213 insertions(+), 439 deletions(-) create mode 100644 docs/polygon/MapLibrePolygonCompositeRenderer.kt.md delete mode 100644 docs/polygon/MapLibrePolygonConductor.kt.md create mode 100644 docs/polygon/MapLibrePolygonController.kt.md create mode 100644 src/main/java/com/mapconductor/maplibre/polygon/MapLibrePolygonCompositeRenderer.kt delete mode 100644 src/main/java/com/mapconductor/maplibre/polygon/MapLibrePolygonConductor.kt create mode 100644 src/main/java/com/mapconductor/maplibre/polygon/MapLibrePolygonController.kt diff --git a/api/android-for-maplibre.api b/api/android-for-maplibre.api index 3db8253..c156484 100644 --- a/api/android-for-maplibre.api +++ b/api/android-for-maplibre.api @@ -83,7 +83,7 @@ public final class com.mapconductor.maplibre.MapLibreMapViewKt { public static final com.mapconductor.maplibre.MapLibreViewController createMapLibreViewController(com.mapconductor.maplibre.MapLibreMapViewHolderInterface, com.mapconductor.core.marker.MarkerTilingOptions, com.mapconductor.core.map.MutableMapServiceRegistry); public static final com.mapconductor.maplibre.marker.MapLibreMarkerController getMarkerController(com.mapconductor.maplibre.MapLibreMapViewHolderInterface, com.mapconductor.core.marker.MarkerTilingOptions); public static final com.mapconductor.maplibre.polyline.MapLibrePolylineController getPolylineController(com.mapconductor.maplibre.MapLibreMapViewHolderInterface); - public static final com.mapconductor.maplibre.polygon.MapLibrePolygonConductor getPolygonController(com.mapconductor.maplibre.MapLibreMapViewHolderInterface); + public static final com.mapconductor.maplibre.polygon.MapLibrePolygonController getPolygonController(com.mapconductor.maplibre.MapLibreMapViewHolderInterface); public static final com.mapconductor.maplibre.circle.MapLibreCircleController getCircleController(com.mapconductor.maplibre.MapLibreMapViewHolderInterface); public static final com.mapconductor.maplibre.raster.MapLibreRasterLayerController getRasterLayerController(com.mapconductor.maplibre.MapLibreMapViewHolderInterface); public static final com.mapconductor.maplibre.groundimage.MapLibreGroundImageController getGroundImageController(com.mapconductor.maplibre.MapLibreMapViewHolderInterface); @@ -109,8 +109,8 @@ public final class com.mapconductor.maplibre.MapLibrePolyUtilsKt { public final class com.mapconductor.maplibre.MapLibreTypeAliasKt { } public final class com.mapconductor.maplibre.MapLibreViewController extends com.mapconductor.core.controller.BaseMapViewController implements com.mapconductor.maplibre.MapLibreViewControllerInterface,org.maplibre.android.maps.MapLibreMap$OnMapClickListener,org.maplibre.android.maps.MapLibreMap$OnMapLongClickListener,org.maplibre.android.maps.MapLibreMap$OnMoveListener,org.maplibre.android.maps.MapLibreMap$OnCameraMoveListener,org.maplibre.android.maps.MapLibreMap$OnCameraIdleListener { - public com.mapconductor.maplibre.MapLibreViewController(com.mapconductor.maplibre.MapLibreMapViewHolderInterface, com.mapconductor.maplibre.marker.MapLibreMarkerController, com.mapconductor.maplibre.polyline.MapLibrePolylineController, com.mapconductor.maplibre.polygon.MapLibrePolygonConductor, com.mapconductor.maplibre.groundimage.MapLibreGroundImageController, com.mapconductor.maplibre.circle.MapLibreCircleController, com.mapconductor.maplibre.raster.MapLibreRasterLayerController, kotlinx.coroutines.CoroutineScope, kotlinx.coroutines.CoroutineScope); - public com.mapconductor.maplibre.MapLibreViewController(com.mapconductor.maplibre.MapLibreMapViewHolderInterface, com.mapconductor.maplibre.marker.MapLibreMarkerController, com.mapconductor.maplibre.polyline.MapLibrePolylineController, com.mapconductor.maplibre.polygon.MapLibrePolygonConductor, com.mapconductor.maplibre.groundimage.MapLibreGroundImageController, com.mapconductor.maplibre.circle.MapLibreCircleController, com.mapconductor.maplibre.raster.MapLibreRasterLayerController, kotlinx.coroutines.CoroutineScope, kotlinx.coroutines.CoroutineScope, int, kotlin.jvm.internal.DefaultConstructorMarker); + public com.mapconductor.maplibre.MapLibreViewController(com.mapconductor.maplibre.MapLibreMapViewHolderInterface, com.mapconductor.maplibre.marker.MapLibreMarkerController, com.mapconductor.maplibre.polyline.MapLibrePolylineController, com.mapconductor.maplibre.polygon.MapLibrePolygonController, com.mapconductor.maplibre.groundimage.MapLibreGroundImageController, com.mapconductor.maplibre.circle.MapLibreCircleController, com.mapconductor.maplibre.raster.MapLibreRasterLayerController, kotlinx.coroutines.CoroutineScope, kotlinx.coroutines.CoroutineScope); + public com.mapconductor.maplibre.MapLibreViewController(com.mapconductor.maplibre.MapLibreMapViewHolderInterface, com.mapconductor.maplibre.marker.MapLibreMarkerController, com.mapconductor.maplibre.polyline.MapLibrePolylineController, com.mapconductor.maplibre.polygon.MapLibrePolygonController, com.mapconductor.maplibre.groundimage.MapLibreGroundImageController, com.mapconductor.maplibre.circle.MapLibreCircleController, com.mapconductor.maplibre.raster.MapLibreRasterLayerController, kotlinx.coroutines.CoroutineScope, kotlinx.coroutines.CoroutineScope, int, kotlin.jvm.internal.DefaultConstructorMarker); public com.mapconductor.maplibre.MapLibreMapViewHolderInterface getHolder(); public kotlinx.coroutines.CoroutineScope getMainCoroutine(); public kotlinx.coroutines.CoroutineScope getDefaultCoroutine(); @@ -358,27 +358,22 @@ public class com.mapconductor.maplibre.marker.MarkerLayer { public final boolean wouldSkipDraw(java.util.List>); public final void draw(java.util.List>, org.maplibre.android.maps.Style); } -public final class com.mapconductor.maplibre.polygon.MapLibrePolygonConductor implements com.mapconductor.core.controller.SlottedOverlayController> { - public com.mapconductor.maplibre.polygon.MapLibrePolygonConductor(com.mapconductor.maplibre.polygon.MapLibrePolygonOverlayRenderer, com.mapconductor.maplibre.polyline.MapLibrePolylineOverlayRenderer); - public final com.mapconductor.maplibre.polygon.MapLibrePolygonOverlayRenderer getPolygonOverlay(); - public final com.mapconductor.maplibre.polyline.MapLibrePolylineOverlayRenderer getPolylineOverlay(); - public int getZIndex(); - 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); - public java.lang.Object update(com.mapconductor.core.polygon.PolygonState, kotlin.coroutines.Continuation); - public final void dispatchClick(com.mapconductor.core.polygon.PolygonEvent); - 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.maplibre.polygon.MapLibrePolygonCompositeRenderer implements com.mapconductor.core.polygon.PolygonOverlayRendererInterface> { + public com.mapconductor.maplibre.polygon.MapLibrePolygonCompositeRenderer(com.mapconductor.maplibre.polygon.MapLibrePolygonOverlayRenderer, com.mapconductor.maplibre.polyline.MapLibrePolylineOverlayRenderer); + public final com.mapconductor.maplibre.polygon.MapLibrePolygonOverlayRenderer getFill(); + public final com.mapconductor.maplibre.polyline.MapLibrePolylineOverlayRenderer 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.maplibre.polygon.MapLibrePolygonConductorKt { +public final class com.mapconductor.maplibre.polygon.MapLibrePolygonCompositeRendererKt { +} +public final class com.mapconductor.maplibre.polygon.MapLibrePolygonController extends com.mapconductor.core.polygon.PolygonController> { + public com.mapconductor.maplibre.polygon.MapLibrePolygonController(com.mapconductor.maplibre.polygon.MapLibrePolygonCompositeRenderer); + public com.mapconductor.maplibre.polygon.MapLibrePolygonCompositeRenderer getRenderer(); + public int getZIndex(); + public com.mapconductor.core.polygon.PolygonOverlayRendererInterface getRenderer(); } public final class com.mapconductor.maplibre.polygon.MapLibrePolygonLayer { public com.mapconductor.maplibre.polygon.MapLibrePolygonLayer(java.lang.String, java.lang.String); diff --git a/docs/MapLibreViewControllerImpl.kt.md b/docs/MapLibreViewControllerImpl.kt.md index 4b0fd76..b58f1a6 100644 --- a/docs/MapLibreViewControllerImpl.kt.md +++ b/docs/MapLibreViewControllerImpl.kt.md @@ -14,7 +14,7 @@ class MapLibreViewController( override val holder: MapLibreMapViewHolderInterface, private val markerController: MapLibreMarkerController, private val polylineController: MapLibrePolylineController, - private val polygonController: MapLibrePolygonConductor, + private val polygonController: MapLibrePolygonController, private val groundImageController: MapLibreGroundImageController, private val circleController: MapLibreCircleController, private val rasterLayerController: MapLibreRasterLayerController, @@ -39,7 +39,7 @@ Initializes a new instance of the `MapLibreViewController`. - Type: `MapLibrePolylineController` - Description: The controller responsible for managing polylines. - `polygonController` - - Type: `MapLibrePolygonConductor` + - Type: `MapLibrePolygonController` - Description: The controller responsible for managing polygons. - `groundImageController` - Type: `MapLibreGroundImageController` diff --git a/docs/polygon/MapLibrePolygonCompositeRenderer.kt.md b/docs/polygon/MapLibrePolygonCompositeRenderer.kt.md new file mode 100644 index 0000000..013734a --- /dev/null +++ b/docs/polygon/MapLibrePolygonCompositeRenderer.kt.md @@ -0,0 +1,37 @@ +# MapLibrePolygonCompositeRenderer + +## Signature + +```kotlin +class MapLibrePolygonCompositeRenderer( + val fill: MapLibrePolygonOverlayRenderer, + val outline: MapLibrePolylineOverlayRenderer, +) : PolygonOverlayRendererInterface +``` + +## Description + +Presents the fill renderer and the outline renderer to the core `PolygonController` as a single +renderer. + +A MapLibre 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: `MapLibrePolygonOverlayRenderer` + - Description: Draws the polygon fill (with holes) into the fill source. +- `outline` + - Type: `MapLibrePolylineOverlayRenderer` + - Description: Draws the outlines into the polygon outline source. diff --git a/docs/polygon/MapLibrePolygonConductor.kt.md b/docs/polygon/MapLibrePolygonConductor.kt.md deleted file mode 100644 index 45e265e..0000000 --- a/docs/polygon/MapLibrePolygonConductor.kt.md +++ /dev/null @@ -1,244 +0,0 @@ -# MapLibrePolygonConductor - -The `MapLibrePolygonConductor` class is a controller responsible for managing polygon overlays on a -MapLibre map. It implements the `OverlayControllerInterface` to handle the lifecycle of polygons, -including their creation, update, and removal. This conductor also manages the rendering of polygon -outlines by creating corresponding polylines. - -It acts as a bridge between the abstract `PolygonState` data and the concrete rendering -implementation provided by `MapLibrePolygonOverlayRenderer` and `MapLibrePolylineOverlayRenderer`. - -## Signature - -```kotlin -class MapLibrePolygonConductor( - val polygonOverlay: MapLibrePolygonOverlayRenderer, - val polylineOverlay: MapLibrePolylineOverlayRenderer, -) : OverlayControllerInterface< - PolygonState, - PolygonEntityInterface, - PolygonEvent, - > -``` - -## Constructor - -### `MapLibrePolygonConductor(...)` - -Initializes a new instance of the `MapLibrePolygonConductor`. - -#### Signature - -```kotlin -MapLibrePolygonConductor( - val polygonOverlay: MapLibrePolygonOverlayRenderer, - val polylineOverlay: MapLibrePolylineOverlayRenderer, -) -``` - -#### Parameters - -- `polygonOverlay` - - Type: `MapLibrePolygonOverlayRenderer` - - Description: The renderer responsible for drawing the filled area of the polygons. -- `polylineOverlay` - - Type: `MapLibrePolylineOverlayRenderer` - - Description: The renderer responsible for drawing the outlines (strokes) of the polygons. - -## Properties - -### `zIndex` - -Specifies the drawing order of the overlay on the map. Higher values are drawn on top of lower -values. - -#### 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 efficiently synchronizes the state of the map with -the provided list. It adds new polygons, updates existing ones, and removes any polygons that are -not in the new list. It also creates and manages the corresponding outlines for each polygon. - -#### Signature - -```kotlin -override suspend fun add(data: List) -``` - -#### Parameters - -- `data` - - Type: `List` - - Description: A list of `PolygonState` objects to display on the map. - -### `update` - -Updates a single polygon on the map based on its `PolygonState`. If a polygon with the same ID -already exists, its properties will be updated. If it does not exist, a new polygon will be created. - -#### Signature - -```kotlin -override suspend fun update(state: PolygonState) -``` - -#### Parameters - -- `state` - - Type: `PolygonState` - - Description: The state object representing the polygon to update or add. - -### `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 the `onClick` handler defined within the specific -`PolygonEvent.state` and also invokes the global `clickListener` if it has been set. - -#### Signature - -```kotlin -fun dispatchClick(event: PolygonEvent) -``` - -#### Parameters - -- `event` - - Type: `PolygonEvent` - - Description: The event object containing details about the click. - -### `find` - -Finds the topmost polygon entity at a given geographic position on the map. - -#### Signature - -```kotlin -override fun find(position: GeoPointInterface): PolygonEntityInterface? -``` - -#### Parameters - -- `position` - - Type: `GeoPointInterface` - - Description: The geographic coordinate (latitude and longitude) to search at. - -#### Returns - -- Type: `PolygonEntityInterface?` - - Description: The found polygon entity, or `null` if no polygon is found at the specified - position. - -### `clear` - -Removes all polygons and their outlines that are currently managed by this conductor from the map. - -#### Signature - -```kotlin -override suspend fun clear() -``` - -### `onCameraChanged` - -A lifecycle method called when the map's camera position changes. This implementation is currently -empty. - -#### Signature - -```kotlin -override suspend fun onCameraChanged(mapCameraPosition: MapCameraPosition) -``` - -### `destroy` - -Cleans up resources used by the conductor. This implementation is currently empty as there are no -specific native resources to release for polygons. - -#### Signature - -```kotlin -override fun destroy() -``` - -## Example - -Here's an example of how to instantiate and use `MapLibrePolygonConductor` to manage polygons on a -map. - -```kotlin -// 1. Assume polygonOverlay and polylineOverlay are already initialized -// val polygonOverlay: MapLibrePolygonOverlayRenderer = ... -// val polylineOverlay: MapLibrePolylineOverlayRenderer = ... - -// 2. Create an instance of the conductor -val polygonConductor = MapLibrePolygonConductor(polygonOverlay, polylineOverlay) - -// 3. Define the state for the polygons you want to add -val polygonState1 = PolygonState( - id = "polygon-1", - 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 = 5f, - onClick = { event -> - println("Clicked on polygon with ID: ${event.state.id}") - } -) - -val polygonState2 = PolygonState( - id = "polygon-2", - points = listOf( - GeoPoint(41.8781, -87.6298), // Chicago - GeoPoint(39.7392, -104.9903), // Denver - GeoPoint(47.6062, -122.3321) // Seattle - ), - fillColor = Color.argb(100, 255, 0, 0) // Semi-transparent red -) - -// 4. Add the polygons to the map -// This needs to be called from a coroutine scope -lifecycleScope.launch { - polygonConductor.add(listOf(polygonState1, polygonState2)) -} - -// 5. Set a global click listener for all polygons -polygonConductor.clickListener = { event -> - println("Global listener: Clicked on polygon ${event.state.id}") - // You can, for example, show an info window here -} - -// 6. Update a polygon later -lifecycleScope.launch { - val updatedPolygonState1 = polygonState1.copy( - fillColor = Color.argb(100, 0, 255, 0) // Change color to green - ) - polygonConductor.update(updatedPolygonState1) -} - -// 7. Clear all polygons from the map -lifecycleScope.launch { - polygonConductor.clear() -} -``` diff --git a/docs/polygon/MapLibrePolygonController.kt.md b/docs/polygon/MapLibrePolygonController.kt.md new file mode 100644 index 0000000..7640446 --- /dev/null +++ b/docs/polygon/MapLibrePolygonController.kt.md @@ -0,0 +1,30 @@ +# MapLibrePolygonController + +## Signature + +```kotlin +class MapLibrePolygonController( + override val renderer: MapLibrePolygonCompositeRenderer, +) : PolygonController(renderer.fill.polygonManager, renderer) +``` + +## Description + +The polygon controller of the MapLibre 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 [`MapLibrePolygonCompositeRenderer`](./MapLibrePolygonCompositeRenderer.kt.md). + +## Constructor Parameters + +- `renderer` + - Type: `MapLibrePolygonCompositeRenderer` + - 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 `MapLibrePolygonConductor`). diff --git a/src/main/java/com/mapconductor/maplibre/MapLibreMapView.kt b/src/main/java/com/mapconductor/maplibre/MapLibreMapView.kt index 89af5ea..07f88e4 100644 --- a/src/main/java/com/mapconductor/maplibre/MapLibreMapView.kt +++ b/src/main/java/com/mapconductor/maplibre/MapLibreMapView.kt @@ -38,7 +38,8 @@ import com.mapconductor.maplibre.marker.MapLibreMarkerController import com.mapconductor.maplibre.marker.MapLibreMarkerOverlayRenderer import com.mapconductor.maplibre.marker.MarkerDragLayer import com.mapconductor.maplibre.marker.MarkerLayer -import com.mapconductor.maplibre.polygon.MapLibrePolygonConductor +import com.mapconductor.maplibre.polygon.MapLibrePolygonCompositeRenderer +import com.mapconductor.maplibre.polygon.MapLibrePolygonController import com.mapconductor.maplibre.polygon.MapLibrePolygonLayer import com.mapconductor.maplibre.polygon.MapLibrePolygonOverlayRenderer import com.mapconductor.maplibre.polyline.MapLibrePolylineController @@ -314,7 +315,7 @@ fun getPolylineController(holder: MapLibreMapViewHolderInterface): MapLibrePolyl return controller } -fun getPolygonController(holder: MapLibreMapViewHolderInterface): MapLibrePolygonConductor { +fun getPolygonController(holder: MapLibreMapViewHolderInterface): MapLibrePolygonController { val polylineLayer = MapLibrePolylineLayer( sourceId = "polygon-outline-source", @@ -341,9 +342,12 @@ fun getPolygonController(holder: MapLibreMapViewHolderInterface): MapLibrePolygo holder = holder, ) - return MapLibrePolygonConductor( - polygonOverlay = polygonOverlayRenderer, - polylineOverlay = polylineOverlayRenderer, + return MapLibrePolygonController( + renderer = + MapLibrePolygonCompositeRenderer( + fill = polygonOverlayRenderer, + outline = polylineOverlayRenderer, + ), ) } diff --git a/src/main/java/com/mapconductor/maplibre/MapLibreViewController.kt b/src/main/java/com/mapconductor/maplibre/MapLibreViewController.kt index 4bd5b8f..5193651 100644 --- a/src/main/java/com/mapconductor/maplibre/MapLibreViewController.kt +++ b/src/main/java/com/mapconductor/maplibre/MapLibreViewController.kt @@ -25,7 +25,7 @@ import com.mapconductor.maplibre.marker.MapLibreMarkerController import com.mapconductor.maplibre.marker.MapLibreMarkerOverlayRenderer import com.mapconductor.maplibre.marker.MarkerDragLayer import com.mapconductor.maplibre.marker.MarkerLayer -import com.mapconductor.maplibre.polygon.MapLibrePolygonConductor +import com.mapconductor.maplibre.polygon.MapLibrePolygonController import com.mapconductor.maplibre.polyline.MapLibrePolylineController import com.mapconductor.maplibre.raster.MapLibreRasterLayerController import org.maplibre.android.geometry.LatLng @@ -46,7 +46,7 @@ class MapLibreViewController( override val holder: MapLibreMapViewHolderInterface, internal val markerController: MapLibreMarkerController, internal val polylineController: MapLibrePolylineController, - internal val polygonController: MapLibrePolygonConductor, + internal val polygonController: MapLibrePolygonController, internal val groundImageController: MapLibreGroundImageController, internal val circleController: MapLibreCircleController, internal val rasterLayerController: MapLibreRasterLayerController, diff --git a/src/main/java/com/mapconductor/maplibre/MapLibreViewControllerStyle.kt b/src/main/java/com/mapconductor/maplibre/MapLibreViewControllerStyle.kt index 4dd43d3..4c1c3e8 100644 --- a/src/main/java/com/mapconductor/maplibre/MapLibreViewControllerStyle.kt +++ b/src/main/java/com/mapconductor/maplibre/MapLibreViewControllerStyle.kt @@ -81,8 +81,8 @@ internal fun MapLibreViewController.setupStyle(style: Style) { (markerController.renderer as MapLibreMarkerOverlayRenderer).ensureDefaultIcon(style) // Polygon sources only (layers will be added per zIndex) - ensureGeoJsonSource(style, polygonController.polylineOverlay.layer.sourceId) - ensureGeoJsonSource(style, polygonController.polygonOverlay.layer.sourceId) + ensureGeoJsonSource(style, polygonController.renderer.outline.layer.sourceId) + ensureGeoJsonSource(style, polygonController.renderer.fill.layer.sourceId) // Circle cts as anchor above polygons ensureGeoJsonSource(style, circleController.renderer.layer.sourceId) @@ -159,8 +159,8 @@ internal fun MapLibreViewController.setupStyle(style: Style) { polylineController.renderer.redraw() // The polygon sources are new and empty on every style; the data has to // be written into them again, like the markers' and the polylines'. - polygonController.polygonOverlay.redraw() - polygonController.polylineOverlay.redraw() + polygonController.renderer.fill.redraw() + polygonController.renderer.outline.redraw() mainCoroutine.launch { groundImageController.reapplyStyle() rasterLayerController.reapplyStyle() @@ -172,12 +172,12 @@ internal fun MapLibreViewController.setupStyle(style: Style) { } internal fun MapLibreViewController.ensurePolygonZLayers(style: 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/maplibre/polygon/MapLibrePolygonCompositeRenderer.kt b/src/main/java/com/mapconductor/maplibre/polygon/MapLibrePolygonCompositeRenderer.kt new file mode 100644 index 0000000..f9c4f8b --- /dev/null +++ b/src/main/java/com/mapconductor/maplibre/polygon/MapLibrePolygonCompositeRenderer.kt @@ -0,0 +1,78 @@ +package com.mapconductor.maplibre.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.maplibre.MapLibreActualPolygon +import com.mapconductor.maplibre.polyline.MapLibrePolylineOverlayRenderer + +/** + * 塗りと輪郭を 1 つのレンダラとして [com.mapconductor.core.polygon.PolygonController] に渡すための合成レンダラ。 + * + * fill レイヤーは幅 1 px の輪郭しか描けないので、`strokeWidth` を守るために輪郭は + * 別のポリライン(`outline-`)として [outline] で描く。差分計算はコアの + * PolygonController に任せ、ここは「塗りへの指示を輪郭にも写す」ことだけをする。 + * + * 塗りのエンティティはコントローラと同じ [fill] の polygonManager に載る。輪郭は + * [outline] の polylineManager に載り、コントローラからは見えない。 + */ +class MapLibrePolygonCompositeRenderer( + val fill: MapLibrePolygonOverlayRenderer, + val outline: MapLibrePolylineOverlayRenderer, +) : 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/maplibre/polygon/MapLibrePolygonConductor.kt b/src/main/java/com/mapconductor/maplibre/polygon/MapLibrePolygonConductor.kt deleted file mode 100644 index f0150eb..0000000 --- a/src/main/java/com/mapconductor/maplibre/polygon/MapLibrePolygonConductor.kt +++ /dev/null @@ -1,153 +0,0 @@ -package com.mapconductor.maplibre.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.maplibre.polyline.MapLibrePolylineOverlayRenderer - -class MapLibrePolygonConductor( - val polygonOverlay: MapLibrePolygonOverlayRenderer, - val polylineOverlay: MapLibrePolylineOverlayRenderer, -) : SlottedOverlayController< - PolygonState, - PolygonEntityInterface, - > { - override val zIndex: Int = 2 - - var clickListener: ((PolygonEvent) -> Unit)? = null - - 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) - } - - @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/maplibre/polygon/MapLibrePolygonController.kt b/src/main/java/com/mapconductor/maplibre/polygon/MapLibrePolygonController.kt new file mode 100644 index 0000000..37a4424 --- /dev/null +++ b/src/main/java/com/mapconductor/maplibre/polygon/MapLibrePolygonController.kt @@ -0,0 +1,17 @@ +package com.mapconductor.maplibre.polygon + +import com.mapconductor.core.polygon.PolygonController +import com.mapconductor.maplibre.MapLibreActualPolygon + +/** + * MapLibre 用のポリゴンコントローラ。 + * + * 追加・更新・削除の差分計算、`has`、クリックカスケードはコアの [PolygonController] が担う。 + * 塗りと輪郭(幅のある LineLayer)の 2 系統の描画は [MapLibrePolygonCompositeRenderer] が束ねる。 + */ +class MapLibrePolygonController( + override val renderer: MapLibrePolygonCompositeRenderer, +) : PolygonController(renderer.fill.polygonManager, renderer) { + // 旧 MapLibrePolygonConductor と同じ値を保つ。 + override val zIndex: Int = 2 +} diff --git a/src/main/java/com/mapconductor/maplibre/polygon/MapLibrePolygonOverlayRenderer.kt b/src/main/java/com/mapconductor/maplibre/polygon/MapLibrePolygonOverlayRenderer.kt index 4c495d2..321f6a9 100644 --- a/src/main/java/com/mapconductor/maplibre/polygon/MapLibrePolygonOverlayRenderer.kt +++ b/src/main/java/com/mapconductor/maplibre/polygon/MapLibrePolygonOverlayRenderer.kt @@ -23,10 +23,13 @@ class MapLibrePolygonOverlayRenderer( override val holder: MapLibreMapViewHolderInterface, override val coroutine: CoroutineScope = CoroutineScope(Dispatchers.Main), ) : AbstractPolygonOverlayRenderer() { + // 消えた分を除いた残りで描き直す。コントローラはこの呼び出しの後で polygonManager から + // 外すので(clear() では onPostProcess も来ない)、ここで除外しておかないと消えずに残る。 override suspend fun onRemove(data: List>) { - coroutine.launch { - holder.map.style?.let { - layer.draw(getAllPolygonEntities(), it) + val polygons = getRemainingPolygonEntities(data) + holder.map.style?.let { + coroutine.launch { + layer.draw(polygons, it) } } } @@ -87,4 +90,11 @@ class MapLibrePolygonOverlayRenderer( 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 } + } }