diff --git a/.claude/settings.json b/.claude/settings.json new file mode 100644 index 0000000..671a084 --- /dev/null +++ b/.claude/settings.json @@ -0,0 +1,6 @@ +{ + "attribution": { + "commit": "", + "pr": "" + } +} diff --git a/.github/workflows/build.yml b/.github/workflows/build.yml index 79f872f..f0d3bb0 100644 --- a/.github/workflows/build.yml +++ b/.github/workflows/build.yml @@ -38,8 +38,29 @@ jobs: cache: 'gradle' - name: Build core and compose locally + env: + HEAD_REF: ${{ github.head_ref }} run: | - git clone --depth 1 https://github.com/MapConductor/android-sdk-core.git _core + # コア側に PR と同名のブランチがあるなら、そちらを組む。 + # + # ここを default ブランチ固定にしていると、**コアの未マージな変更に追随する + # PR は永久に赤いまま**になる。実際 CameraBearing をコアへ足したとき、 + # プロバイダ側の PR が 10 本まとめて Unresolved reference で落ちた。 + # コアが先にマージされるまで待つ、という運用は取り込み順の事故を呼ぶので、 + # 同名ブランチがあれば拾う形にしてある(無ければ従来どおり default)。 + clone_matching() { + repo="$1"; dir="$2" + url="https://github.com/MapConductor/$repo.git" + if [ -n "$HEAD_REF" ] && git ls-remote --exit-code --heads "$url" "$HEAD_REF" >/dev/null 2>&1; then + echo "$repo: matching branch $HEAD_REF" + git clone --depth 1 --branch "$HEAD_REF" "$url" "$dir" + else + echo "$repo: default branch" + git clone --depth 1 "$url" "$dir" + fi + } + + clone_matching android-sdk-core _core cd _core ./gradlew --no-daemon publishToMavenLocal cd .. @@ -53,7 +74,7 @@ jobs: } EOF - git clone --depth 1 https://github.com/MapConductor/android-sdk-compose.git _compose + clone_matching android-sdk-compose _compose cd _compose ./gradlew --no-daemon publishToMavenLocal cd .. diff --git a/api/android-for-mapbox.api b/api/android-for-mapbox.api index 1ab909d..0e55423 100644 --- a/api/android-for-mapbox.api +++ b/api/android-for-mapbox.api @@ -20,22 +20,29 @@ public interface com.mapconductor.mapbox.IMapboxPaddingsInterface extends com.ma public abstract com.mapbox.maps.EdgeInsets toEdgeInsects(); } public final class com.mapconductor.mapbox.MapCameraPositionKt { + public static final double MAPBOX_VERTICAL_FOV; + public static final com.mapbox.maps.EdgeInsets toMapboxEdgeInsets(com.mapconductor.core.map.MapPaddingsInterface, float); + public static final com.mapconductor.core.map.MapPaddings toMapPaddings(com.mapbox.maps.EdgeInsets, float); public static final com.mapconductor.core.map.MapCameraPosition toMapCameraPosition(com.mapbox.maps.CameraChanged); public static final com.mapbox.maps.CameraOptions toCameraOptions(com.mapconductor.core.map.MapCameraPosition); + public static final com.mapbox.maps.CameraOptions toCameraOptions(com.mapconductor.core.map.MapCameraPosition, java.lang.Float); public static final com.mapbox.maps.CameraState toCameraState(com.mapconductor.core.map.MapCameraPosition); public static final com.mapconductor.core.map.MapCameraPosition from(com.mapconductor.core.map.MapCameraPosition$Companion, com.mapconductor.core.map.MapCameraPositionInterface); public static final com.mapconductor.core.map.MapCameraPosition toMapCameraPosition(com.mapbox.maps.CameraOptions); public static final com.mapconductor.core.map.MapCameraPosition toMapCameraPosition(com.mapbox.maps.CameraState); - public static final com.mapconductor.core.map.MapCameraPosition toMapCameraPosition(com.mapbox.maps.CameraState, java.lang.Double); + public static final com.mapconductor.core.map.MapCameraPosition toMapCameraPosition(com.mapbox.maps.CameraState, java.lang.Double, java.lang.Float); } public final class com.mapconductor.mapbox.MapboxCameraStateSnapshot { - public com.mapconductor.mapbox.MapboxCameraStateSnapshot(com.mapbox.maps.CameraState, java.lang.Double); + public com.mapconductor.mapbox.MapboxCameraStateSnapshot(com.mapbox.maps.CameraState, java.lang.Double, java.lang.Float); + public com.mapconductor.mapbox.MapboxCameraStateSnapshot(com.mapbox.maps.CameraState, java.lang.Double, java.lang.Float, int, kotlin.jvm.internal.DefaultConstructorMarker); public final com.mapbox.maps.CameraState getCameraState(); public final java.lang.Double getLogicalTiltHint(); + public final java.lang.Float getDensity(); public final com.mapconductor.core.map.MapCameraPosition toMapCameraPosition(); public final com.mapbox.maps.CameraState component1(); public final java.lang.Double component2(); - public final com.mapconductor.mapbox.MapboxCameraStateSnapshot copy(com.mapbox.maps.CameraState, java.lang.Double); + public final java.lang.Float component3(); + public final com.mapconductor.mapbox.MapboxCameraStateSnapshot copy(com.mapbox.maps.CameraState, java.lang.Double, java.lang.Float); public java.lang.String toString(); public int hashCode(); public boolean equals(java.lang.Object); @@ -75,6 +82,11 @@ public final class com.mapconductor.mapbox.MapboxMapDesign$NavigationDay extends public final class com.mapconductor.mapbox.MapboxMapDesign$NavigationNight extends com.mapconductor.mapbox.MapboxMapDesign { public static final com.mapconductor.mapbox.MapboxMapDesign$NavigationNight INSTANCE; } +public final class com.mapconductor.mapbox.MapboxMapDesign$None extends com.mapconductor.mapbox.MapboxMapDesign { + public static final com.mapconductor.mapbox.MapboxMapDesign$None INSTANCE; + public java.lang.String getValue(); + public java.lang.Object getValue(); +} public final class com.mapconductor.mapbox.MapboxMapDesign$Outdoors extends com.mapconductor.mapbox.MapboxMapDesign { public static final com.mapconductor.mapbox.MapboxMapDesign$Outdoors INSTANCE; } @@ -93,12 +105,17 @@ public final class com.mapconductor.mapbox.MapboxMapDesign$StandardSatellite ext public final class com.mapconductor.mapbox.MapboxMapDesign$Streets extends com.mapconductor.mapbox.MapboxMapDesign { public static final com.mapconductor.mapbox.MapboxMapDesign$Streets INSTANCE; } +public final class com.mapconductor.mapbox.MapboxMapDesign$StyleUri extends com.mapconductor.mapbox.MapboxMapDesign { + public final java.lang.String getUri(); + public java.lang.String getValue(); + public java.lang.Object getValue(); +} 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(); @@ -141,6 +158,9 @@ public final class com.mapconductor.mapbox.MapboxMapViewController extends com.m public final class com.mapconductor.mapbox.MapboxMapViewControllerCameraKt { public static final com.mapconductor.core.map.MapCameraPosition getMapCameraPosition(com.mapconductor.mapbox.MapboxMapViewController); public static final com.mapconductor.core.map.MapCameraPosition readLogicalCameraPosition(com.mapconductor.mapbox.MapboxMapViewController); + public static final double viewportHeightDp(com.mapconductor.mapbox.MapboxMapViewController); + public static final com.mapconductor.core.map.MapCameraPosition toNativeCamera(com.mapconductor.mapbox.MapboxMapViewController, com.mapconductor.core.map.MapCameraPosition); + public static final float density(com.mapconductor.mapbox.MapboxMapViewController); public static final java.lang.Object getMapCameraPositionAsync(com.mapconductor.mapbox.MapboxMapViewController, kotlin.coroutines.Continuation); public static final void handleMoveCamera(com.mapconductor.mapbox.MapboxMapViewController, com.mapconductor.core.map.MapCameraPosition); public static final void handleAnimateCamera(com.mapconductor.mapbox.MapboxMapViewController, com.mapconductor.core.map.MapCameraPosition, long); @@ -174,14 +194,17 @@ public final class com.mapconductor.mapbox.MapboxMapViewHolder implements com.ma public void onLowMemory(); public void onStart(); public void onStop(); + public com.mapconductor.core.projection.ProjectedPoint toProjectedPoint(com.mapconductor.core.features.GeoPointInterface); + public com.mapconductor.core.features.GeoPoint fromProjectedPoint(com.mapconductor.core.projection.ProjectedPoint); + public com.mapconductor.core.projection.MapCoordinateSystem getCoordinateSystem(); public void onResume(); public java.lang.Object getMapView(); public java.lang.Object getMap(); } 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, kotlin.jvm.functions.Function3, androidx.compose.runtime.Composer, int, int, int); + 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); @@ -201,6 +224,18 @@ public final class com.mapconductor.mapbox.MapboxMapViewSaver extends com.mapcon public final class com.mapconductor.mapbox.MapboxMapViewScope extends com.mapconductor.compose.MapViewScope { public com.mapconductor.mapbox.MapboxMapViewScope(); } +public final class com.mapconductor.mapbox.MapboxOverlayStacking { + public static final com.mapconductor.mapbox.MapboxOverlayStacking INSTANCE; + 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 getCIRCLE_TILE(); + public final com.mapconductor.core.controller.OverlayStacking$Key getPOLYLINE_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); +} public final class com.mapconductor.mapbox.MapboxPaddings extends com.mapconductor.core.map.MapPaddings implements com.mapconductor.mapbox.IMapboxPaddingsInterface { public static final com.mapconductor.mapbox.MapboxPaddings$Companion Companion; public com.mapconductor.mapbox.MapboxPaddings(double, double, double, double); @@ -359,6 +394,7 @@ public final class com.mapconductor.mapbox.marker.MapboxMarkerOverlayRenderer ex public void setMarkerPosition(com.mapconductor.core.marker.MarkerEntityInterface, com.mapconductor.core.features.GeoPoint); public java.lang.Object onAdd(java.util.List, kotlin.coroutines.Continuation>); public java.lang.Object onRemove(java.util.List>, kotlin.coroutines.Continuation); + protected java.lang.Object applyDrawOrder(java.util.List>, kotlin.coroutines.Continuation); public java.lang.Object onPostProcess(kotlin.coroutines.Continuation); public java.lang.Object onChange(java.util.List>, kotlin.coroutines.Continuation>); public void onDragSelectionChanged(com.mapconductor.core.marker.MarkerEntityInterface, com.mapconductor.core.marker.MarkerEntityInterface); @@ -390,27 +426,23 @@ 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 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.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 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.MapboxPolygonConductorKt { + 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); @@ -477,7 +509,6 @@ public final class com.mapconductor.mapbox.polyline.MapboxPolylineOverlayRendere public final class com.mapconductor.mapbox.raster.MapboxRasterLayerController extends com.mapconductor.core.raster.RasterLayerController { public com.mapconductor.mapbox.raster.MapboxRasterLayerController(com.mapconductor.core.raster.RasterLayerManagerInterface, com.mapconductor.mapbox.raster.MapboxRasterLayerOverlayRenderer); public com.mapconductor.mapbox.raster.MapboxRasterLayerController(com.mapconductor.core.raster.RasterLayerManagerInterface, com.mapconductor.mapbox.raster.MapboxRasterLayerOverlayRenderer, int, kotlin.jvm.internal.DefaultConstructorMarker); - public final java.lang.Object reapplyStyle(kotlin.coroutines.Continuation); } public final class com.mapconductor.mapbox.raster.MapboxRasterLayerHandle { public com.mapconductor.mapbox.raster.MapboxRasterLayerHandle(java.lang.String, java.lang.String); diff --git a/build.gradle.kts b/build.gradle.kts index 7b581a1..8071159 100644 --- a/build.gradle.kts +++ b/build.gradle.kts @@ -93,6 +93,12 @@ dependencies { testImplementation(libs.junit) androidTestImplementation(libs.androidx.junit) androidTestImplementation(libs.androidx.espresso.core) + // A real MapView loading real (local) styles: what a design change does to + // the overlays only shows on a map that actually swaps its style. + androidTestImplementation(libs.androidx.runner) + androidTestImplementation(platform(libs.androidx.compose.bom)) + androidTestImplementation(libs.androidx.ui.test.junit4) + debugImplementation(libs.androidx.ui.test.manifest) } // Set project version for NMCP plugin 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/gradle.properties b/gradle.properties index 26d9949..8b1a332 100644 --- a/gradle.properties +++ b/gradle.properties @@ -3,10 +3,10 @@ android.useAndroidX=true kotlin.code.style=official android.nonTransitiveRClass=true -compileSdk=35 +compileSdk=37 minSdk=26 javaVersion=17 jvmTarget=17 -libraryVersion=1.3.1 +libraryVersion=1.4.0 android.generateSyncIssueWhenLibraryConstraintsAreEnabled=false diff --git a/gradle/libs.versions.toml b/gradle/libs.versions.toml index a9d0e75..65aa5f4 100644 --- a/gradle/libs.versions.toml +++ b/gradle/libs.versions.toml @@ -1,17 +1,21 @@ [versions] -composeLibs = "1.7.8" -agp = "9.2.1" -kotlin = "2.0.21" - -composeBom = "2025.02.00" -lifecycleRuntimeKtx = "2.8.7" -mapboxAndroid = "11.24.3" -activityCompose = "1.9.3" +composeLibs = "1.12.1" +# material3 は compose-ui とは別のバージョン系列。composeLibs(ui 系)を流用すると +# 存在しない material3:1.7.8 を要求してクラスパスごと解決に失敗する。 +material3 = "1.4.0" +agp = "9.4.1" +kotlin = "2.4.20" + +composeBom = "2026.09.00" +lifecycleRuntimeKtx = "2.11.0" +mapboxAndroid = "11.33.0" +activityCompose = "1.13.0" ktLint = "14.2.0" junit = "4.13.2" junitVersion = "1.3.0" espressoCore = "3.7.0" +runner = "1.7.0" [libraries] # Jetpack Compose (with BOM) @@ -29,13 +33,19 @@ mapbox-android = { module = "com.mapbox.maps:android-ndk27", version.ref = "mapb # Sample app androidx-activity-compose = { group = "androidx.activity", name = "activity-compose", version.ref = "activityCompose" } -androidx-material3 = { group = "androidx.compose.material3", name = "material3", version.ref = "composeLibs" } +androidx-material3 = { group = "androidx.compose.material3", name = "material3", version.ref = "material3" } +# sample-app が Icons.AutoMirrored を使う。material3 は material-icons を推移的に持ってこないので +# 明示的に足す。版は compose BOM 任せ(集約ビルドの catalog と同じ形)。 +androidx-material-icons-core = { group = "androidx.compose.material", name = "material-icons-core" } androidx-ui-tooling = { group = "androidx.compose.ui", name = "ui-tooling", version.ref = "composeLibs" } # Test junit = { group = "junit", name = "junit", version.ref = "junit" } androidx-junit = { group = "androidx.test.ext", name = "junit", version.ref = "junitVersion" } androidx-espresso-core = { group = "androidx.test.espresso", name = "espresso-core", version.ref = "espressoCore" } +androidx-runner = { module = "androidx.test:runner", version.ref = "runner" } +androidx-ui-test-junit4 = { group = "androidx.compose.ui", name = "ui-test-junit4", version.ref = "composeLibs" } +androidx-ui-test-manifest = { group = "androidx.compose.ui", name = "ui-test-manifest", version.ref = "composeLibs" } [plugins] android-application = { id = "com.android.application", version.ref = "agp" } diff --git a/sample-app/build.gradle.kts b/sample-app/build.gradle.kts index 8f2c78e..23993c2 100644 --- a/sample-app/build.gradle.kts +++ b/sample-app/build.gradle.kts @@ -55,6 +55,7 @@ dependencies { implementation(libs.androidx.ui) implementation(libs.androidx.ui.tooling.preview) implementation(libs.androidx.material3) + implementation(libs.androidx.material.icons.core) debugImplementation(platform(libs.androidx.compose.bom)) debugImplementation(libs.androidx.ui.tooling) } diff --git a/src/androidTest/java/com/mapconductor/mapbox/ExampleInstrumentedTest.kt b/src/androidTest/java/com/mapconductor/mapbox/ExampleInstrumentedTest.kt index dc43fd6..f6941f2 100644 --- a/src/androidTest/java/com/mapconductor/mapbox/ExampleInstrumentedTest.kt +++ b/src/androidTest/java/com/mapconductor/mapbox/ExampleInstrumentedTest.kt @@ -17,6 +17,6 @@ class ExampleInstrumentedTest { fun useAppContext() { // Context of the app under test. val appContext = InstrumentationRegistry.getInstrumentation().targetContext - assertEquals("com.mapconductor.googlemaps", appContext.packageName) + assertEquals("com.mapconductor.mapbox.test", appContext.packageName) } } diff --git a/src/androidTest/java/com/mapconductor/mapbox/MapboxGroundImageStyleChangeTest.kt b/src/androidTest/java/com/mapconductor/mapbox/MapboxGroundImageStyleChangeTest.kt new file mode 100644 index 0000000..3ad3192 --- /dev/null +++ b/src/androidTest/java/com/mapconductor/mapbox/MapboxGroundImageStyleChangeTest.kt @@ -0,0 +1,142 @@ +package com.mapconductor.mapbox + +import android.graphics.Bitmap +import android.graphics.Color +import android.graphics.drawable.BitmapDrawable +import androidx.compose.foundation.layout.fillMaxSize +import androidx.compose.runtime.getValue +import androidx.compose.runtime.mutableStateOf +import androidx.compose.runtime.setValue +import androidx.compose.ui.Modifier +import androidx.compose.ui.test.junit4.createComposeRule +import androidx.test.ext.junit.runners.AndroidJUnit4 +import androidx.test.platform.app.InstrumentationRegistry +import com.mapbox.common.MapboxOptions +import com.mapbox.maps.Style +import com.mapconductor.compose.groundimage.GroundImage +import com.mapconductor.core.features.GeoPoint +import com.mapconductor.core.features.GeoRectBounds +import com.mapconductor.core.groundimage.GroundImageState +import com.mapconductor.core.map.AttributionRule +import org.junit.Rule +import org.junit.Test +import org.junit.runner.RunWith + +/** + * A ground image survives the map design changing under it. + * + * Replacing the style throws away every source and layer added at runtime, so + * the SDK has to put the image back into the new style. iOS kept the handle it + * had made for the old style and skipped an unchanged image (ios-for-maplibre + * #12); this checks the Android path — `setMapDesignType` → `loadStyle` → + * `subscribeStyleLoaded` — on a real MapView loading two local style documents. + * + * The styles have no sources, so nothing is fetched and no real access token + * is needed; a placeholder satisfies the SDK's check. + */ +@RunWith(AndroidJUnit4::class) +class MapboxGroundImageStyleChangeTest { + @get:Rule + val compose = createComposeRule() + + private class LocalStyle(override val id: String) : MapboxDesignType { + override val attributionRules: List = emptyList() + + override fun getValue(): String = + """{"version": 8, "name": "$id", "sources": {}, + "layers": [{"id": "background-$id", "type": "background"}]}""" + } + + private val first = LocalStyle("first") + private val second = LocalStyle("second") + + private fun groundImage(): GroundImageState { + val bitmap = Bitmap.createBitmap(4, 4, Bitmap.Config.ARGB_8888).apply { eraseColor(Color.RED) } + val resources = InstrumentationRegistry.getInstrumentation().targetContext.resources + return GroundImageState( + bounds = GeoRectBounds(GeoPoint(35.6, 139.6), GeoPoint(35.8, 139.9)), + image = BitmapDrawable(resources, bitmap), + id = "tokyo", + ) + } + + /** The loaded style, when it is the one the given design describes. */ + private fun styleOf(state: MapboxViewState, design: LocalStyle): Style? { + var style: Style? = null + InstrumentationRegistry.getInstrumentation().runOnMainSync { + style = state.getMapViewHolder()?.map?.style + ?.takeIf { it.isStyleLoaded() && it.styleLayerExists("background-${design.id}") } + } + return style + } + + private fun hasGroundImage(style: Style?): Boolean { + var found = false + InstrumentationRegistry.getInstrumentation().runOnMainSync { + found = style != null && + style.styleSourceExists("mc-gimg-src-tokyo") && + style.styleLayerExists("mc-gimg-lyr-tokyo") + } + return found + } + + private var shown by mutableStateOf(true) + + private fun show(state: MapboxViewState, image: GroundImageState) { + compose.setContent { + MapboxMapView( + state = state, + modifier = Modifier.fillMaxSize(), + sdkInitialize = { + MapboxOptions.accessToken = "pk.placeholder-for-local-styles" + true + }, + ) { + if (shown) GroundImage(image) + } + } + } + + @Test + fun anUnchangedGroundImageIsInTheNewStyle() { + val state = MapboxViewState(mapDesignType = first, id = "unchanged") + show(state, groundImage()) + compose.waitUntil(TIMEOUT) { hasGroundImage(styleOf(state, first)) } + + compose.runOnUiThread { state.mapDesignType = second } + compose.waitUntil(TIMEOUT) { styleOf(state, second) != null } + compose.waitUntil(TIMEOUT) { hasGroundImage(styleOf(state, second)) } + } + + @Test + fun theSameDesignSetAgainKeepsTheImage() { + val state = MapboxViewState(mapDesignType = first, id = "same") + show(state, groundImage()) + compose.waitUntil(TIMEOUT) { hasGroundImage(styleOf(state, first)) } + + compose.runOnUiThread { state.mapDesignType = first } + Thread.sleep(SETTLE_MS) + compose.waitUntil(TIMEOUT) { hasGroundImage(styleOf(state, first)) } + } + + @Test + fun aGroundImageRemovedAfterTheChangeLeavesTheNewStyle() { + val state = MapboxViewState(mapDesignType = first, id = "removed") + show(state, groundImage()) + compose.waitUntil(TIMEOUT) { hasGroundImage(styleOf(state, first)) } + + compose.runOnUiThread { state.mapDesignType = second } + compose.waitUntil(TIMEOUT) { hasGroundImage(styleOf(state, second)) } + + compose.runOnUiThread { shown = false } + compose.waitUntil(TIMEOUT) { + val style = styleOf(state, second) + style != null && !hasGroundImage(style) + } + } + + private companion object { + const val TIMEOUT = 20_000L + const val SETTLE_MS = 1_500L + } +} diff --git a/src/androidTest/java/com/mapconductor/mapbox/MapboxMapViewSaverTest.kt b/src/androidTest/java/com/mapconductor/mapbox/MapboxMapViewSaverTest.kt new file mode 100644 index 0000000..471cb44 --- /dev/null +++ b/src/androidTest/java/com/mapconductor/mapbox/MapboxMapViewSaverTest.kt @@ -0,0 +1,113 @@ +package com.mapconductor.mapbox + +import androidx.compose.runtime.saveable.Saver +import androidx.compose.runtime.saveable.SaverScope +import androidx.test.ext.junit.runners.AndroidJUnit4 +import com.mapconductor.core.features.GeoPoint +import com.mapconductor.core.map.AttributionRule +import com.mapconductor.core.map.MapCameraPosition +import com.mapconductor.core.map.MapDesignTypeInterface +import com.mapconductor.core.map.MapUISettings +import org.junit.Assert.assertEquals +import org.junit.Assert.assertSame +import org.junit.Assert.assertTrue +import org.junit.Test +import org.junit.runner.RunWith +import android.os.Bundle +import android.os.Parcel + +/** + * 回転などの構成変更で state が [MapboxMapViewSaver] を通って作り直されても、デザイン(id・ + * attributionRules ほか)と uiSettings・カメラが落ちないこと。Bundle は実機の Parcel を一度通す。 + */ +@RunWith(AndroidJUnit4::class) +class MapboxMapViewSaverTest { + private val rules = + listOf(AttributionRule("© test"), AttributionRule("© ranged", minZoom = 3, maxZoom = 15)) + + private val camera = + MapCameraPosition( + position = GeoPoint(21.38, -157.93), + zoom = 12.0, + bearing = 20.0, + tilt = 10.0, + ) + + @Test + fun keepsCustomDesignWithAttributionRules() { + val design = MapboxMapDesign.Custom("my-account/my-style", rules) + val state = MapboxViewState(mapDesignType = design, id = "mapbox-1", cameraPosition = camera) + state.uiSettings = MapUISettings(rotateGesture = false, tiltGesture = false) + + val restored = roundTrip(MapboxMapViewSaver().createSaver(), state) + + val actual = restored.mapDesignType + assertTrue(actual is MapboxMapDesign.Custom) + assertEquals("my-account/my-style", actual.id) + assertEquals(rules, actual.attributionRules) + assertCommon(restored, "mapbox-1") + } + + @Test + fun keepsStyleUriAsStyleUri() { + // 以前は id だけを保存して create(id) で戻しており、URI が mapbox://styles/mapbox/ の + // Custom になっていた。 + val design = MapboxMapDesign.StyleUri("https://example.com/style.json", rules) + val restored = roundTrip(MapboxMapViewSaver().createSaver(), MapboxViewState(design, "mapbox-2")) + + val actual = restored.mapDesignType + assertTrue(actual is MapboxMapDesign.StyleUri) + assertEquals("https://example.com/style.json", actual.getValue()) + assertEquals(rules, actual.attributionRules) + } + + @Test + fun keepsPresetDesignAsTheSameObject() { + val restored = roundTrip(MapboxMapViewSaver().createSaver(), MapboxViewState(MapboxMapDesign.Dark, "mapbox-3")) + assertSame(MapboxMapDesign.Dark, restored.mapDesignType) + } + + @Test + fun appImplementationComesBackWithItsStyle() { + val design = + object : MapDesignTypeInterface { + override val id: String = "app-design" + override val attributionRules: List = rules + + override fun getValue(): String = "${MapboxMapDesign.MAPBOX_URL}/streets-v12" + } + val restored = roundTrip(MapboxMapViewSaver().createSaver(), MapboxViewState(design, "mapbox-4")) + + assertEquals(design.getValue(), restored.mapDesignType.getValue()) + assertEquals(rules, restored.mapDesignType.attributionRules) + } + + private fun assertCommon( + restored: com.mapconductor.core.map.MapViewStateInterface<*>, + id: String, + ) { + assertEquals(id, restored.id) + assertEquals(false, restored.uiSettings.rotateGesture) + assertEquals(false, restored.uiSettings.tiltGesture) + assertEquals(true, restored.uiSettings.scrollGesture) + assertEquals(12.0, restored.cameraPosition.zoom, 1e-9) + assertEquals(20.0, restored.cameraPosition.bearing, 1e-9) + assertEquals(21.38, restored.cameraPosition.position.latitude, 1e-9) + } + + private fun roundTrip( + saver: Saver, + value: T, + ): T { + val saved = with(saver) { SaverScope { true }.save(value) }!! + // 一度 Parcel に書いて読み戻す(プロセスが落ちたあとの復元と同じ経路)。 + val parcel = Parcel.obtain() + try { + parcel.writeBundle(saved) + parcel.setDataPosition(0) + return saver.restore(parcel.readBundle(javaClass.classLoader)!!)!! + } finally { + parcel.recycle() + } + } +} diff --git a/src/main/java/com/mapconductor/mapbox/MapCameraPosition.kt b/src/main/java/com/mapconductor/mapbox/MapCameraPosition.kt index 629e4a9..58cefb5 100644 --- a/src/main/java/com/mapconductor/mapbox/MapCameraPosition.kt +++ b/src/main/java/com/mapconductor/mapbox/MapCameraPosition.kt @@ -6,8 +6,10 @@ import com.mapbox.maps.CameraState import com.mapbox.maps.EdgeInsets import com.mapconductor.core.features.GeoPoint import com.mapconductor.core.features.GeoPointInterface +import com.mapconductor.core.map.CameraBearing import com.mapconductor.core.map.MapCameraPosition import com.mapconductor.core.map.MapCameraPositionInterface +import com.mapconductor.core.map.MapPaddings import com.mapconductor.core.map.MapPaddingsInterface import com.mapconductor.core.map.VisibleRegion import com.mapconductor.core.spherical.Spherical @@ -18,21 +20,42 @@ import kotlin.math.tan private val converter = ZoomAltitudeConverter() +/** + * Mapbox のカメラの縦方向の視野角(度)。Mapbox Maps の既定値で、地図の大きさに + * よらず一定(焦点距離 = 地図の高さ × 1.5)。TB520FU で 36.9° を実測。 + */ +internal const val MAPBOX_VERTICAL_FOV = 36.87 + +/** dp のパディング → Mapbox の px のパディング。 */ +internal fun MapPaddingsInterface.toMapboxEdgeInsets(density: Float): EdgeInsets = + EdgeInsets(top * density, left * density, bottom * density, right * density) + +/** Mapbox の px のパディング → dp のパディング。 */ +internal fun EdgeInsets.toMapPaddings(density: Float): MapPaddings = + MapPaddings(top = top / density, left = left / density, bottom = bottom / density, right = right / density) + fun CameraChanged.toMapCameraPosition() = cameraState .toMapCameraPosition() .copy(paddings = cameraState.padding.toPaddings()) -fun MapCameraPosition.toCameraOptions(): CameraOptions { +fun MapCameraPosition.toCameraOptions(): CameraOptions = toCameraOptions(density = null) + +/** + * [density] があれば [MapCameraPosition.paddings] を Mapbox のパディングとして渡す。 + * Mapbox はパディングを除いた領域の中央にカメラの中心を置き、tilt の遠近もエンジンが解く。 + * パディングは次に変えるまで残るので、null(未指定)のときは渡さない。 + */ +internal fun MapCameraPosition.toCameraOptions(density: Float?): CameraOptions { + val padding = if (density != null) paddings?.toMapboxEdgeInsets(density) else null if (this.tilt >= 0) { return CameraOptions .Builder() .center(GeoPoint.from(position).toPoint()) .zoom(ZoomAltitudeConverter.googleZoomToMapboxZoom(zoom)) .pitch(tilt) - .bearing(bearing) - // TODO: - // .padding(paddings?.toEdgeInsects()) + .bearing(CameraBearing.toNativeHeading(bearing)) + .padding(padding) .build() } else { // tilt < 0: Mapbox cannot represent an upward pitch directly. @@ -43,7 +66,7 @@ fun MapCameraPosition.toCameraOptions(): CameraOptions { val mapboxZoomForAltitude = ZoomAltitudeConverter.googleZoomToMapboxZoom(zoom) val altitude = converter.zoomLevelToAltitude(mapboxZoomForAltitude, position.latitude, 0.0) val distanceForward = altitude * tan(tiltAbsRad) - val target = Spherical.computeOffset(position, distanceForward, bearing) + val target = Spherical.computeOffset(position, distanceForward, CameraBearing.toNativeHeading(bearing)) val mapboxZoom = converter.altitudeToZoomLevel(altitude / cos(tiltAbsRad), target.latitude, 0.0) return CameraOptions @@ -51,9 +74,8 @@ fun MapCameraPosition.toCameraOptions(): CameraOptions { .center(target.toPoint()) .zoom(mapboxZoom) .pitch(tiltAbsDeg) - .bearing(bearing) - // TODO: - // .padding(paddings?.toEdgeInsects()) + .bearing(CameraBearing.toNativeHeading(bearing)) + .padding(padding) .build() } } @@ -63,7 +85,7 @@ fun MapCameraPosition.toCameraState(): CameraState = GeoPoint.from(position).toPoint(), EdgeInsets(0.0, 0.0, 0.0, 0.0), ZoomAltitudeConverter.googleZoomToMapboxZoom(zoom), - bearing, + CameraBearing.toNativeHeading(bearing), tilt, ) @@ -78,6 +100,7 @@ fun MapCameraPosition.Companion.from(cameraPosition: MapCameraPositionInterface) tilt = cameraPosition.tilt, paddings = MapboxPaddings.from(cameraPosition.paddings), visibleRegion = cameraPosition.visibleRegion, + fov = cameraPosition.fov, ) } @@ -100,20 +123,24 @@ fun CameraState.toMapCameraPosition() = logicalTiltHint = null, ) -internal fun CameraState.toMapCameraPosition(logicalTiltHint: Double?) = - toMapCameraPosition( - center = center.toGeoPoint(), - mapboxZoom = zoom, - bearing = bearing, - pitch = pitch, - logicalTiltHint = logicalTiltHint, - ) +internal fun CameraState.toMapCameraPosition( + logicalTiltHint: Double?, + density: Float? = null, +) = toMapCameraPosition( + center = center.toGeoPoint(), + mapboxZoom = zoom, + bearing = bearing, + pitch = pitch, + paddings = density?.let { padding.toMapPaddings(it) }, + logicalTiltHint = logicalTiltHint, +) internal data class MapboxCameraStateSnapshot( val cameraState: CameraState, val logicalTiltHint: Double?, + val density: Float? = null, ) { - fun toMapCameraPosition(): MapCameraPosition = cameraState.toMapCameraPosition(logicalTiltHint) + fun toMapCameraPosition(): MapCameraPosition = cameraState.toMapCameraPosition(logicalTiltHint, density) } private fun toMapCameraPosition( @@ -130,10 +157,11 @@ private fun toMapCameraPosition( return MapCameraPosition( position = center, zoom = ZoomAltitudeConverter.mapboxZoomToGoogleZoom(mapboxZoom), - bearing = bearing, + bearing = CameraBearing.bearingFromNativeHeading(bearing), tilt = pitch, paddings = paddings, visibleRegion = visibleRegion, + fov = MAPBOX_VERTICAL_FOV, ) } @@ -148,9 +176,10 @@ private fun toMapCameraPosition( return MapCameraPosition( position = originalCenter, zoom = ZoomAltitudeConverter.mapboxZoomToGoogleZoom(originalMapboxZoom), - bearing = bearing, + bearing = CameraBearing.bearingFromNativeHeading(bearing), tilt = -pitchAbsDeg, paddings = paddings, visibleRegion = visibleRegion, + fov = MAPBOX_VERTICAL_FOV, ) } diff --git a/src/main/java/com/mapconductor/mapbox/MapboxMapDesign.kt b/src/main/java/com/mapconductor/mapbox/MapboxMapDesign.kt index a79ad2c..7d4e69d 100644 --- a/src/main/java/com/mapconductor/mapbox/MapboxMapDesign.kt +++ b/src/main/java/com/mapconductor/mapbox/MapboxMapDesign.kt @@ -2,6 +2,7 @@ package com.mapconductor.mapbox import com.mapbox.maps.Style import com.mapconductor.core.map.AttributionRule +import com.mapconductor.core.map.BlankMapStyle import com.mapconductor.core.map.MapDesignTypeInterface import com.mapconductor.mapbox.MapboxMapDesign.Companion.MAPBOX_URL @@ -11,6 +12,16 @@ sealed class MapboxMapDesign( override val id: String, override val attributionRules: List = emptyList(), ) : MapboxDesignType { + /** + * ベースマップ無し。背景色だけのスタイルで、タイルは何も取得しない。 + * + * `getValue()` は URI ではなくスタイル JSON そのもの。Mapbox の `loadStyle` と + * `MapInitOptions.styleUri` はどちらも URI か JSON の文字列を受ける。 + */ + object None : MapboxMapDesign("none") { + override fun getValue(): String = BlankMapStyle.JSON + } + object Standard : MapboxMapDesign("standard") object StandardSatellite : MapboxMapDesign("standard-satellite") @@ -36,6 +47,20 @@ sealed class MapboxMapDesign( attributionRules: List = emptyList(), ) : MapboxMapDesign(layerId, attributionRules) + /** + * A style at an arbitrary URI -- `http(s)://`, `asset://`, `file://` -- + * rather than one of Mapbox's own. [Custom] cannot say this: its value is + * always prefixed with [MAPBOX_URL]. The id is the URI itself, so the + * design a loaded style reports back as (see [toMapDesignType]) compares + * equal to the one that was set. + */ + class StyleUri( + val uri: String, + attributionRules: List = emptyList(), + ) : MapboxMapDesign(uri, attributionRules) { + override fun getValue(): String = uri + } + override fun getValue(): String = "${MAPBOX_URL}/${this.id}" companion object { @@ -43,6 +68,7 @@ sealed class MapboxMapDesign( fun create(layerId: String): MapboxMapDesign = when (layerId) { + None.id -> None Standard.id -> Standard StandardSatellite.id -> StandardSatellite Streets.id -> Streets @@ -71,7 +97,11 @@ fun Style.toMapDesignType(): MapboxDesignType = "$MAPBOX_URL/${MapboxMapDesign.NavigationDay.id}" -> MapboxMapDesign.NavigationDay "$MAPBOX_URL/${MapboxMapDesign.NavigationNight.id}" -> MapboxMapDesign.NavigationNight else -> - MapboxMapDesign.Custom( - layerId = this.styleURI.replaceFirst("${MAPBOX_URL}/", ""), - ) + if (this.styleURI.startsWith("$MAPBOX_URL/")) { + MapboxMapDesign.Custom( + layerId = this.styleURI.replaceFirst("${MAPBOX_URL}/", ""), + ) + } else { + MapboxMapDesign.StyleUri(this.styleURI) + } } diff --git a/src/main/java/com/mapconductor/mapbox/MapboxMapView.kt b/src/main/java/com/mapconductor/mapbox/MapboxMapView.kt index 935c2e8..0f60a3b 100644 --- a/src/main/java/com/mapconductor/mapbox/MapboxMapView.kt +++ b/src/main/java/com/mapconductor/mapbox/MapboxMapView.kt @@ -1,388 +1,449 @@ -package com.mapconductor.mapbox - -import androidx.compose.runtime.Composable -import androidx.compose.runtime.DisposableEffect -import androidx.compose.runtime.SideEffect -import androidx.compose.runtime.mutableStateOf -import androidx.compose.runtime.remember -import androidx.compose.ui.Modifier -import androidx.compose.ui.node.Ref -import androidx.compose.ui.platform.LocalContext -import androidx.lifecycle.DefaultLifecycleObserver -import androidx.lifecycle.LifecycleOwner -import androidx.lifecycle.compose.LocalLifecycleOwner -import com.mapbox.maps.MapInitOptions -import com.mapbox.maps.MapView -import com.mapconductor.compose.map.MapViewBase -import com.mapconductor.core.OnCameraMoveHandler -import com.mapconductor.core.OnMapEventHandler -import com.mapconductor.core.OnMapLoadedHandler -import com.mapconductor.core.circle.CircleManager -import com.mapconductor.core.map.CameraRestriction -import com.mapconductor.core.map.MapCameraPositionInterface -import com.mapconductor.core.map.MapProjection -import com.mapconductor.core.map.MutableMapServiceRegistry -import com.mapconductor.core.marker.MarkerEventControllerInterface -import com.mapconductor.core.marker.MarkerManager -import com.mapconductor.core.marker.MarkerOverlayRendererInterface -import com.mapconductor.core.marker.MarkerRenderingStrategyInterface -import com.mapconductor.core.marker.MarkerRenderingSupport -import com.mapconductor.core.marker.MarkerRenderingSupportKey -import com.mapconductor.core.marker.MarkerTilingOptions -import com.mapconductor.core.marker.StrategyMarkerController -import com.mapconductor.core.polygon.PolygonManager -import com.mapconductor.core.polyline.PolylineManager -import com.mapconductor.mapbox.circle.MapboxCircleController -import com.mapconductor.mapbox.circle.MapboxCircleLayer -import com.mapconductor.mapbox.circle.MapboxCircleOverlayRenderer -import com.mapconductor.mapbox.groundimage.MapboxGroundImageController -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.MapboxPolygonLayer -import com.mapconductor.mapbox.polygon.MapboxPolygonOverlayRenderer -import com.mapconductor.mapbox.polyline.MapboxPolylineController -import com.mapconductor.mapbox.polyline.MapboxPolylineLayer -import com.mapconductor.mapbox.polyline.MapboxPolylineOverlayRenderer -import com.mapconductor.mapbox.raster.MapboxRasterLayerController -import com.mapconductor.mapbox.raster.MapboxRasterLayerOverlayRenderer -import android.annotation.SuppressLint -import android.app.Activity -import android.content.Context -import android.content.ContextWrapper -import android.view.ViewGroup - -@SuppressLint("Lifecycle") -@Composable -fun MapboxMapView( - state: MapboxViewState, - modifier: Modifier = Modifier, - markerTiling: MarkerTilingOptions? = null, - cameraRestriction: CameraRestriction? = null, - sdkInitialize: (suspend (Context) -> Boolean)? = null, - onMapLoaded: OnMapLoadedHandler? = null, - onMapClick: OnMapEventHandler? = null, - onMapLongClick: OnMapEventHandler? = null, - onCameraMoveStart: OnCameraMoveHandler? = null, - onCameraMove: OnCameraMoveHandler? = null, - onCameraMoveEnd: OnCameraMoveHandler? = null, - projection: MapProjection = MapProjection.Mercator, - content: (@Composable MapboxMapViewScope.() -> Unit)? = null, -) { - val holderRef = remember { Ref() } - val context = LocalContext.current - val controllerRef = remember { Ref() } - val scope = remember { MapboxMapViewScope() } - val registry = remember { scope.buildRegistry() } - val lifecycle = LocalLifecycleOwner.current.lifecycle - val cameraState = remember { mutableStateOf(state.cameraPosition) } - - SideEffect { - controllerRef.value?.setProjection(projection) - } - - MapViewBase( - state = state, - cameraState = cameraState, - modifier = modifier, - viewProvider = { - val cameraOptions = - state.cameraPosition.toCameraOptions() - - val styleUri = state.mapDesignType.getValue() - - val mapOptions = - MapInitOptions( - context = context, - textureView = true, - styleUri = styleUri, - cameraOptions = cameraOptions, - ) - - MapView(context, mapOptions).also { - it.onStart() - } - }, - holderProvider = { mapView -> MapboxMapViewHolder(mapView, mapView.mapboxMap) }, - controllerProvider = { holder -> - // **組み立ては `createMapboxViewController` に一本化してある。** - // React Native のような非 Compose ホストも同じ関数を通るので、 - // ここでコントローラを直接組み直さないこと(片方だけ配線が増えて食い違う)。 - createMapboxViewController( - holder = holder, - markerTiling = markerTiling ?: MarkerTilingOptions.Default, - projection = projection, - serviceRegistry = state.serviceRegistry, - ).also { mapController -> - mapController.setCameraMoveStartListener { - cameraState.value = it - state.updateCameraPosition(it) - onCameraMoveStart?.invoke(it) - } - mapController.setCameraMoveListener { - cameraState.value = it - state.updateCameraPosition(it) - onCameraMove?.invoke(it) - } - mapController.setCameraMoveEndListener { - cameraState.value = it - state.updateCameraPosition(it) - onCameraMoveEnd?.invoke(it) - } - mapController.setMapClickListener(onMapClick) - mapController.setMapLongClickListener(onMapLongClick) - mapController.setMapDesignTypeChangeListener(state::onMapDesignTypeChange) - cameraRestriction?.let { mapController.setCameraRestriction(it) } - state.setController(mapController) - - holderRef.value = holder - controllerRef.value = mapController - - // Post an initial camera update once the MapView is laid out and style is ready - holder.mapView.post { mapController.sendInitialCameraUpdate() } - } - }, - scope = scope, - registry = registry, - sdkInitialize = { - if (sdkInitialize != null) { - sdkInitialize(context) - } else { - MapboxInitSDK(context) - true - } - }, - onMapLoaded = onMapLoaded, - // Pass content if it needs to be rendered within the overlay providers in MapViewBase, - // or handle it here if it's specific to MapboxMapView structure before calling MapViewBase. - // For now, assuming content relates to overlay definitions. - content = content, // This might need adjustment based on how overlays are handled - customDisposableEffect = { _, holderRef -> - - // HERE specific DisposableEffect logic - DisposableEffect(lifecycle) { - val observer = - object : DefaultLifecycleObserver { - override fun onResume(owner: LifecycleOwner) { - holderRef.value?.mapView?.onResume() - } - - override fun onPause(owner: LifecycleOwner) { - // Do not call here to keep the MapView instance - // holderRef.value?.mapView?.onPause() - } - - @SuppressLint("Lifecycle") - override fun onDestroy(owner: LifecycleOwner) { - val currentHolder = holderRef.value - if (currentHolder != null) { - val activity = context.findActivity() - if (activity?.isChangingConfigurations == true) { - (currentHolder.mapView.parent as? ViewGroup)?.removeView(currentHolder.mapView) - } else { - // Ensure these calls are safe if mapView might be null or already destroyed - currentHolder.mapView.onDestroy() - } - } - } - } - lifecycle.addObserver(observer) - onDispose { - lifecycle.removeObserver(observer) - } - } - }, - ) -} - -/** - * 命令的なコントローラ一式を組み立てる。Compose の [MapboxMapView] と、 - * **Compose を通さないホスト(React Native / Cordova)の両方**から使う。 - * - * ここに置くことで、UI 統合ごとにプロバイダ固有のレイヤ設定を書き写さずに済む。 - * android-for-maplibre の `createMapLibreViewController()` と同じ役割・同じ形。 - * - * @param serviceRegistry 登録先のサービスレジストリ。Compose からは `state.serviceRegistry` を渡す - * (react-sdk / ios-sdk と同じく持ち主は state)。React Native のような非 Compose ホストは - * state を持たないので、自前のレジストリを渡す。 - * **null にすると `MarkerRenderingSupportKey` が登録されず、クラスタリングが - * エラーも出さずに何も描かなくなる。** - */ -fun createMapboxViewController( - holder: MapboxMapViewHolder, - markerTiling: MarkerTilingOptions = MarkerTilingOptions.Default, - projection: MapProjection = MapProjection.Mercator, - serviceRegistry: MutableMapServiceRegistry? = null, -): MapboxMapViewController { - val mapController = - MapboxMapViewController( - holder = holder, - projection = projection, - markerController = getMarkerController(holder = holder, markerTiling = markerTiling), - polylineController = getPolylineController(holder), - polygonController = getPolygonController(holder), - groundImageController = getGroundImageController(holder), - circleController = getCircleController(holder), - rasterLayerController = getRasterLayerController(holder), - ) - - serviceRegistry?.put( - MarkerRenderingSupportKey, - object : MarkerRenderingSupport { - override fun createMarkerRenderer( - strategy: MarkerRenderingStrategyInterface, - ): MarkerOverlayRendererInterface = mapController.createMarkerRenderer(strategy) - - override fun createMarkerEventController( - controller: StrategyMarkerController, - renderer: MarkerOverlayRendererInterface, - ): MarkerEventControllerInterface = - mapController.createMarkerEventController(controller = controller, renderer = renderer) - - override fun registerMarkerEventController( - controller: MarkerEventControllerInterface, - ) { - mapController.registerMarkerEventController(controller) - } - - override fun onMarkerRenderingReady() { - mapController.sendInitialCameraUpdate() - } - }, - ) - - return mapController -} - -internal fun getPolygonController(holder: MapboxMapViewHolder): MapboxPolygonConductor { - val polylineLayer = - MapboxPolylineLayer( - sourceId = "polygon-outline-source", - layerId = "polygon-outline-layer", - ) - val polylineManager = PolylineManager() - val polylineOverlayRenderer = - MapboxPolylineOverlayRenderer( - layer = polylineLayer, - polylineManager = polylineManager, - holder = holder, - ) - - val polygonManager = PolygonManager() - val polygonLayer = - MapboxPolygonLayer( - sourceId = "polygon-fill-source", - layerId = "polygon-fill-layer", - ) - val polygonOverlayRenderer = - MapboxPolygonOverlayRenderer( - layer = polygonLayer, - polygonManager = polygonManager, - holder = holder, - ) - - val conductor = - MapboxPolygonConductor( - polygonOverlay = polygonOverlayRenderer, - polylineOverlay = polylineOverlayRenderer, - ) - return conductor -} - -internal fun getCircleController(holder: MapboxMapViewHolder): MapboxCircleController { - val circleLayer = - MapboxCircleLayer( - sourceId = "circle-source", - layerId = "circle-layer", - ) - val circleManager = CircleManager() - - val renderer = - MapboxCircleOverlayRenderer( - layer = circleLayer, - circleManager = circleManager, - holder = holder, - ) - - val controller = - MapboxCircleController( - renderer = renderer, - ) - return controller -} - -internal fun getPolylineController(holder: MapboxMapViewHolder): MapboxPolylineController { - val polylineLayer = - MapboxPolylineLayer( - sourceId = "polyline-source", - layerId = "polyline-layer", - ) - val polylineManager = PolylineManager() - - val renderer = - MapboxPolylineOverlayRenderer( - layer = polylineLayer, - polylineManager = polylineManager, - holder = holder, - ) - - val controller = - MapboxPolylineController( - renderer = renderer, - ) - return controller -} - -internal fun getMarkerController( - holder: MapboxMapViewHolder, - markerTiling: MarkerTilingOptions, -): MapboxMarkerController { - val manager = - MarkerManager.defaultManager( - minMarkerCount = markerTiling.minMarkerCount, - ) - val markerLayer = - MarkerLayer( - sourceId = "markers-source", - layerId = "markers-layer", - ) - val dragLayer = - MarkerDragLayer( - sourceId = "marker-drag-source", - layerId = "marker-drag-layer", - ) - return MapboxMarkerController.create( - holder = holder, - markerManager = manager, - markerLayer = markerLayer, - dragLayer = dragLayer, - markerTiling = markerTiling, - ) -} - -internal fun getRasterLayerController(holder: MapboxMapViewHolder): MapboxRasterLayerController { - val renderer = - MapboxRasterLayerOverlayRenderer( - holder = holder, - ) - return MapboxRasterLayerController( - renderer = renderer, - ) -} - -internal fun getGroundImageController(holder: MapboxMapViewHolder): MapboxGroundImageController { - val renderer = - MapboxGroundImageOverlayRenderer( - holder = holder, - ) - return MapboxGroundImageController(renderer = renderer) -} - -internal fun Context.findActivity(): Activity? = - when (this) { - is Activity -> this - is ContextWrapper -> baseContext.findActivity() - else -> null - } +package com.mapconductor.mapbox + +import androidx.compose.runtime.Composable +import androidx.compose.runtime.DisposableEffect +import androidx.compose.runtime.SideEffect +import androidx.compose.runtime.mutableStateOf +import androidx.compose.runtime.remember +import androidx.compose.ui.Modifier +import androidx.compose.ui.node.Ref +import androidx.compose.ui.platform.LocalContext +import androidx.lifecycle.DefaultLifecycleObserver +import androidx.lifecycle.LifecycleOwner +import androidx.lifecycle.compose.LocalLifecycleOwner +import androidx.lifecycle.findViewTreeLifecycleOwner +import androidx.lifecycle.setViewTreeLifecycleOwner +import com.mapbox.maps.MapInitOptions +import com.mapbox.maps.MapView +import com.mapconductor.compose.map.MapViewBase +import com.mapconductor.compose.map.MapViewLifecycleOwner +import com.mapconductor.core.OnCameraMoveHandler +import com.mapconductor.core.OnMapEventHandler +import com.mapconductor.core.OnMapLoadedHandler +import com.mapconductor.core.circle.CircleManager +import com.mapconductor.core.map.CameraRestriction +import com.mapconductor.core.map.MapCameraPositionInterface +import com.mapconductor.core.map.MapPaddings +import com.mapconductor.core.map.MapProjection +import com.mapconductor.core.map.MapViewStyle +import com.mapconductor.core.map.MutableMapServiceRegistry +import com.mapconductor.core.map.StyleMutationStore +import com.mapconductor.core.map.VectorStyleAsDesign +import com.mapconductor.core.map.VectorStyleMutationSupportKey +import com.mapconductor.core.map.VectorStyleSupportKey +import com.mapconductor.core.map.applyStyleMutations +import com.mapconductor.core.marker.MarkerEventControllerInterface +import com.mapconductor.core.marker.MarkerManager +import com.mapconductor.core.marker.MarkerOverlayRendererInterface +import com.mapconductor.core.marker.MarkerRenderingStrategyInterface +import com.mapconductor.core.marker.MarkerRenderingSupport +import com.mapconductor.core.marker.MarkerRenderingSupportKey +import com.mapconductor.core.marker.MarkerTilingOptions +import com.mapconductor.core.marker.StrategyMarkerController +import com.mapconductor.core.polygon.PolygonManager +import com.mapconductor.core.polyline.PolylineManager +import com.mapconductor.mapbox.circle.MapboxCircleController +import com.mapconductor.mapbox.circle.MapboxCircleLayer +import com.mapconductor.mapbox.circle.MapboxCircleOverlayRenderer +import com.mapconductor.mapbox.groundimage.MapboxGroundImageController +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.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 +import com.mapconductor.mapbox.polyline.MapboxPolylineLayer +import com.mapconductor.mapbox.polyline.MapboxPolylineOverlayRenderer +import com.mapconductor.mapbox.raster.MapboxRasterLayerController +import com.mapconductor.mapbox.raster.MapboxRasterLayerOverlayRenderer +import android.annotation.SuppressLint +import android.app.Activity +import android.content.Context +import android.content.ContextWrapper + +@SuppressLint("Lifecycle") +@Composable +fun MapboxMapView( + state: MapboxViewState, + modifier: Modifier = Modifier, + markerTiling: MarkerTilingOptions? = null, + cameraRestriction: CameraRestriction? = null, + sdkInitialize: (suspend (Context) -> Boolean)? = null, + onMapLoaded: OnMapLoadedHandler? = null, + onMapClick: OnMapEventHandler? = null, + onMapLongClick: OnMapEventHandler? = null, + onCameraMoveStart: OnCameraMoveHandler? = null, + onCameraMove: OnCameraMoveHandler? = null, + onCameraMoveEnd: OnCameraMoveHandler? = null, + projection: MapProjection = MapProjection.Mercator, + /** + * How the map looks, when the app states it rather than naming a design. + * + * A vector style *is* the basemap. `com.mapconductor:vectorstyle` builds + * one; what happens underneath depends on this backend and the app does + * not have to know. + */ + style: MapViewStyle? = null, + onStyleDiagnostics: ((List) -> Unit)? = null, + content: (@Composable MapboxMapViewScope.() -> Unit)? = null, +) { + val holderRef = remember { Ref() } + val context = LocalContext.current + val controllerRef = remember { Ref() } + val scope = remember { MapboxMapViewScope() } + val registry = remember { scope.buildRegistry() } + val lifecycle = LocalLifecycleOwner.current.lifecycle + val cameraState = remember { mutableStateOf(state.cameraPosition) } + + SideEffect { + controllerRef.value?.setProjection(projection) + } + + MapViewBase( + state = state, + cameraState = cameraState, + modifier = modifier, + viewProvider = { + val cameraOptions = + // パディングはネイティブに渡さない。接続時の moveCamera が中心のずらしで適用する。 + state.cameraPosition.copy(paddings = MapPaddings.Zeros).toCameraOptions(context.resources.displayMetrics.density) + + val styleUri = state.mapDesignType.getValue() + + val mapOptions = + MapInitOptions( + context = context, + textureView = true, + styleUri = styleUri, + cameraOptions = cameraOptions, + ) + + MapView(context, mapOptions).also { + // Mapbox's lifecycle plugin follows the view tree's lifecycle and + // only lets go of the MapView (component callbacks, observers) + // when that lifecycle is destroyed. With the Activity's, every + // MapView the user switched away from would stay until the + // Activity goes, so it gets one of its own; see onDestroyMapView. + it.setViewTreeLifecycleOwner(MapViewLifecycleOwner(lifecycle)) + it.onStart() + } + }, + holderProvider = { mapView -> MapboxMapViewHolder(mapView, mapView.mapboxMap) }, + controllerProvider = { holder -> + // **組み立ては `createMapboxViewController` に一本化してある。** + // React Native のような非 Compose ホストも同じ関数を通るので、 + // ここでコントローラを直接組み直さないこと(片方だけ配線が増えて食い違う)。 + createMapboxViewController( + holder = holder, + markerTiling = markerTiling ?: MarkerTilingOptions.Default, + projection = projection, + serviceRegistry = state.serviceRegistry, + ).also { mapController -> + mapController.setCameraMoveStartListener { + cameraState.value = it + state.updateCameraPosition(it) + onCameraMoveStart?.invoke(it) + } + mapController.setCameraMoveListener { + cameraState.value = it + state.updateCameraPosition(it) + onCameraMove?.invoke(it) + } + mapController.setCameraMoveEndListener { + cameraState.value = it + state.updateCameraPosition(it) + onCameraMoveEnd?.invoke(it) + } + mapController.setMapClickListener(onMapClick) + mapController.setMapLongClickListener(onMapLongClick) + mapController.setMapDesignTypeChangeListener(state::onMapDesignTypeChange) + cameraRestriction?.let { mapController.setCameraRestriction(it) } + state.setController(mapController) + // Mapbox draws vector styles natively: a layer with a style to + // show hands it over instead of rasterising it. On the state, + // not in `createMapboxViewController` -- it acts on the + // state's design, which non-Compose hosts do not have. + state.serviceRegistry.put( + VectorStyleSupportKey, + VectorStyleAsDesign(state) { url, rules -> MapboxMapDesign.StyleUri(url, rules) }, + ) + // Mapbox can also be told to change a loaded style in place, + // so adjusting one need not hand it a new document -- which + // would drop its tiles and rebuild every overlay the app + // added. `VectorStyle` looks this up and takes the cheap road + // when it is here. + // + // The style is read through a lambda rather than captured: + // every design change replaces it, and a target holding the + // old one would patch a style the map has already dropped. + val mutations = + StyleMutationStore { list -> + applyStyleMutations( + MapboxStyleMutationTarget( + style = { holder.map.style }, + ready = { mapController.isShowingRequestedStyle() }, + ), + list, + ) + } + state.serviceRegistry.put(VectorStyleMutationSupportKey, mutations) + // Whatever was applied is lost the moment the map loads a + // style again; this is where it goes back on. + mapController.styleLoadedListener = { mutations.onStyleLoaded() } + + holderRef.value = holder + controllerRef.value = mapController + + // Post an initial camera update once the MapView is laid out and style is ready + holder.mapView.post { mapController.sendInitialCameraUpdate() } + } + }, + scope = scope, + registry = registry, + sdkInitialize = { + if (sdkInitialize != null) { + sdkInitialize(context) + } else { + MapboxInitSDK(context) + true + } + }, + onMapLoaded = onMapLoaded, + // Pass content if it needs to be rendered within the overlay providers in MapViewBase, + // or handle it here if it's specific to MapboxMapView structure before calling MapViewBase. + // For now, assuming content relates to overlay definitions. + style = style, + onStyleDiagnostics = onStyleDiagnostics, + content = content, // This might need adjustment based on how overlays are handled + customDisposableEffect = { _, holderRef -> + + // HERE specific DisposableEffect logic + DisposableEffect(lifecycle) { + val observer = + object : DefaultLifecycleObserver { + override fun onResume(owner: LifecycleOwner) { + holderRef.value?.mapView?.onResume() + } + + override fun onPause(owner: LifecycleOwner) { + // Do not call here to keep the MapView instance + // holderRef.value?.mapView?.onPause() + } + } + lifecycle.addObserver(observer) + onDispose { + lifecycle.removeObserver(observer) + } + } + }, + // Ending the MapView's own lifecycle has the lifecycle plugin stop and + // destroy it and unregister what it registered. A MapView left alive + // keeps its renderer and, through its listeners, the controller with + // every overlay. + onDestroyMapView = { mapView -> + val owner = mapView.findViewTreeLifecycleOwner() as? MapViewLifecycleOwner + if (owner != null) { + owner.destroy() + } else { + mapView.onStop() + mapView.onDestroy() + } + }, + ) +} + +/** + * 命令的なコントローラ一式を組み立てる。Compose の [MapboxMapView] と、 + * **Compose を通さないホスト(React Native / Cordova)の両方**から使う。 + * + * ここに置くことで、UI 統合ごとにプロバイダ固有のレイヤ設定を書き写さずに済む。 + * android-for-maplibre の `createMapLibreViewController()` と同じ役割・同じ形。 + * + * @param serviceRegistry 登録先のサービスレジストリ。Compose からは `state.serviceRegistry` を渡す + * (react-sdk / ios-sdk と同じく持ち主は state)。React Native のような非 Compose ホストは + * state を持たないので、自前のレジストリを渡す。 + * **null にすると `MarkerRenderingSupportKey` が登録されず、クラスタリングが + * エラーも出さずに何も描かなくなる。** + */ +fun createMapboxViewController( + holder: MapboxMapViewHolder, + markerTiling: MarkerTilingOptions = MarkerTilingOptions.Default, + projection: MapProjection = MapProjection.Mercator, + serviceRegistry: MutableMapServiceRegistry? = null, +): MapboxMapViewController { + val mapController = + MapboxMapViewController( + holder = holder, + projection = projection, + markerController = getMarkerController(holder = holder, markerTiling = markerTiling), + polylineController = getPolylineController(holder), + polygonController = getPolygonController(holder), + groundImageController = getGroundImageController(holder), + circleController = getCircleController(holder), + rasterLayerController = getRasterLayerController(holder), + ) + + serviceRegistry?.put( + MarkerRenderingSupportKey, + object : MarkerRenderingSupport { + override fun createMarkerRenderer( + strategy: MarkerRenderingStrategyInterface, + ): MarkerOverlayRendererInterface = mapController.createMarkerRenderer(strategy) + + override fun createMarkerEventController( + controller: StrategyMarkerController, + renderer: MarkerOverlayRendererInterface, + ): MarkerEventControllerInterface = + mapController.createMarkerEventController(controller = controller, renderer = renderer) + + override fun registerMarkerEventController( + controller: MarkerEventControllerInterface, + ) { + mapController.registerMarkerEventController(controller) + } + + override fun onMarkerRenderingReady() { + mapController.sendInitialCameraUpdate() + } + }, + ) + + return mapController +} + +internal fun getPolygonController(holder: MapboxMapViewHolder): MapboxPolygonController { + val polylineLayer = + MapboxPolylineLayer( + sourceId = "polygon-outline-source", + layerId = "polygon-outline-layer", + ) + val polylineManager = PolylineManager() + val polylineOverlayRenderer = + MapboxPolylineOverlayRenderer( + layer = polylineLayer, + polylineManager = polylineManager, + holder = holder, + ) + + val polygonManager = PolygonManager() + val polygonLayer = + MapboxPolygonLayer( + sourceId = "polygon-fill-source", + layerId = "polygon-fill-layer", + ) + val polygonOverlayRenderer = + MapboxPolygonOverlayRenderer( + layer = polygonLayer, + polygonManager = polygonManager, + holder = holder, + ) + + val controller = + MapboxPolygonController( + renderer = + MapboxPolygonCompositeRenderer( + fill = polygonOverlayRenderer, + outline = polylineOverlayRenderer, + ), + ) + return controller +} + +internal fun getCircleController(holder: MapboxMapViewHolder): MapboxCircleController { + val circleLayer = + MapboxCircleLayer( + sourceId = "circle-source", + layerId = "circle-layer", + ) + val circleManager = CircleManager() + + val renderer = + MapboxCircleOverlayRenderer( + layer = circleLayer, + circleManager = circleManager, + holder = holder, + ) + + val controller = + MapboxCircleController( + renderer = renderer, + ) + return controller +} + +internal fun getPolylineController(holder: MapboxMapViewHolder): MapboxPolylineController { + val polylineLayer = + MapboxPolylineLayer( + sourceId = "polyline-source", + layerId = "polyline-layer", + ) + val polylineManager = PolylineManager() + + val renderer = + MapboxPolylineOverlayRenderer( + layer = polylineLayer, + polylineManager = polylineManager, + holder = holder, + ) + + val controller = + MapboxPolylineController( + renderer = renderer, + ) + return controller +} + +internal fun getMarkerController( + holder: MapboxMapViewHolder, + markerTiling: MarkerTilingOptions, +): MapboxMarkerController { + val manager = + MarkerManager.defaultManager( + minMarkerCount = markerTiling.minMarkerCount, + ) + val markerLayer = + MarkerLayer( + sourceId = "markers-source", + layerId = "markers-layer", + ) + val dragLayer = + MarkerDragLayer( + sourceId = "marker-drag-source", + layerId = "marker-drag-layer", + ) + return MapboxMarkerController.create( + holder = holder, + markerManager = manager, + markerLayer = markerLayer, + dragLayer = dragLayer, + markerTiling = markerTiling, + ) +} + +internal fun getRasterLayerController(holder: MapboxMapViewHolder): MapboxRasterLayerController { + val renderer = + MapboxRasterLayerOverlayRenderer( + holder = holder, + ) + return MapboxRasterLayerController( + renderer = renderer, + ) +} + +internal fun getGroundImageController(holder: MapboxMapViewHolder): MapboxGroundImageController { + val renderer = + MapboxGroundImageOverlayRenderer( + holder = holder, + ) + return MapboxGroundImageController(renderer = renderer) +} + +internal fun Context.findActivity(): Activity? = + when (this) { + is Activity -> this + is ContextWrapper -> baseContext.findActivity() + else -> null + } diff --git a/src/main/java/com/mapconductor/mapbox/MapboxMapViewController.kt b/src/main/java/com/mapconductor/mapbox/MapboxMapViewController.kt index 3542951..2b044f4 100644 --- a/src/main/java/com/mapconductor/mapbox/MapboxMapViewController.kt +++ b/src/main/java/com/mapconductor/mapbox/MapboxMapViewController.kt @@ -1,421 +1,510 @@ -package com.mapconductor.mapbox - -import MapboxMapViewControllerInterface -import com.mapbox.android.gestures.MoveGestureDetector -import com.mapbox.geojson.Point -import com.mapbox.maps.extension.style.layers.properties.generated.ProjectionName -import com.mapbox.maps.extension.style.projection.generated.Projection -import com.mapbox.maps.extension.style.projection.generated.setProjection -import com.mapbox.maps.plugin.gestures.OnMapClickListener -import com.mapbox.maps.plugin.gestures.OnMapLongClickListener -import com.mapbox.maps.plugin.gestures.OnMoveListener -import com.mapbox.maps.plugin.gestures.addOnMapClickListener -import com.mapbox.maps.plugin.gestures.addOnMapLongClickListener -import com.mapbox.maps.plugin.gestures.addOnMoveListener -import com.mapbox.maps.plugin.gestures.gestures -import com.mapbox.maps.plugin.gestures.removeOnMapClickListener -import com.mapbox.maps.plugin.gestures.removeOnMapLongClickListener -import com.mapbox.maps.plugin.gestures.removeOnMoveListener -import com.mapconductor.core.circle.OnCircleEventHandler -import com.mapconductor.core.controller.BaseMapViewController -import com.mapconductor.core.features.GeoPointInterface -import com.mapconductor.core.features.GeoRectBounds -import com.mapconductor.core.groundimage.OnGroundImageEventHandler -import com.mapconductor.core.map.CameraRestriction -import com.mapconductor.core.map.MapCameraPosition -import com.mapconductor.core.map.MapProjection -import com.mapconductor.core.map.MapUISettings -import com.mapconductor.core.marker.DefaultMarkerEventController -import com.mapconductor.core.marker.MarkerAnimationOverlayHost -import com.mapconductor.core.marker.MarkerEventControllerInterface -import com.mapconductor.core.marker.MarkerOverlayRendererInterface -import com.mapconductor.core.marker.MarkerRenderingStrategyInterface -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.PolygonState -import com.mapconductor.core.polyline.OnPolylineEventHandler -import com.mapconductor.core.polyline.PolylineState -import com.mapconductor.mapbox.circle.MapboxCircleController -import com.mapconductor.mapbox.groundimage.MapboxGroundImageController -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.polyline.MapboxPolylineController -import com.mapconductor.mapbox.raster.MapboxRasterLayerController -import java.util.UUID -import java.util.concurrent.atomic.AtomicInteger -import kotlinx.coroutines.CoroutineScope -import kotlinx.coroutines.Dispatchers -import kotlinx.coroutines.launch - -typealias MapboxMapDesignTypeChangeHandler = (MapboxDesignType) -> Unit - -/** - * Mapbox のマップコントローラ。 - * - * **型は公開、構築は非公開。** `createMapboxViewController()` の戻り型として、また - * React Native のような非 Compose ホストが `BaseMapViewController` として受けるために - * 型は見える必要がある(LongdoMapViewController / MapTilerMapViewController も公開)。 - * 一方コンストラクタ引数は internal なコントローラ群なので、`internal constructor` に - * して**組み立てはファクトリ 1 か所に閉じる**。2 経路で組むと配線が片方だけ増える。 - */ -class MapboxMapViewController internal constructor( - override val holder: MapboxMapViewHolder, - internal var projection: MapProjection, - internal val markerController: MapboxMarkerController, - internal val polylineController: MapboxPolylineController, - internal val polygonController: MapboxPolygonConductor, - internal val groundImageController: MapboxGroundImageController, - internal val circleController: MapboxCircleController, - internal val rasterLayerController: MapboxRasterLayerController, - override val mainCoroutine: CoroutineScope = CoroutineScope(Dispatchers.Main), - override val defaultCoroutine: CoroutineScope = CoroutineScope(Dispatchers.Default), -) : BaseMapViewController(), - MapboxMapViewControllerInterface, - OnMapClickListener, - OnMapLongClickListener, - OnMoveListener { - // Track created z-indexed polygon layers to manage add/remove without enumerating style layers - internal val polygonZLayers: MutableSet = mutableSetOf() - internal val markerEventControllers = mutableListOf>() - internal var activeDragController: DefaultMarkerEventController? = null - - internal val cameraUpdateToken = AtomicInteger(0) - internal var lastLogicalCameraPosition: MapCameraPosition? = null - - internal var markerClickListener: OnMarkerEventHandler? = null - internal var markerDragStartListener: OnMarkerEventHandler? = null - internal var markerDragListener: OnMarkerEventHandler? = null - internal var markerDragEndListener: OnMarkerEventHandler? = null - internal var markerAnimateStartListener: OnMarkerEventHandler? = null - internal var markerAnimateEndListener: OnMarkerEventHandler? = null - internal var involvedMapInitializedCallback: Boolean = false - - init { - setupListeners() - registerOverlayController(markerController) - registerOverlayController(polylineController) - registerOverlayController(polygonController) - registerOverlayController(groundImageController) - registerOverlayController(circleController) - registerOverlayController(rasterLayerController) - registerMarkerEventController(DefaultMarkerEventController(markerController)) - - markerController.setRasterLayerCallback { state -> - if (state != null) { - rasterLayerController.upsert(state) - } else { - val markerTileLayers = - rasterLayerController.rasterLayerManager - .allEntities() - .filter { it.state.id.startsWith("marker-tile-") } - markerTileLayers.forEach { entity -> rasterLayerController.removeById(entity.state.id) } - } - } - } - - override fun moveCamera(position: MapCameraPosition) = handleMoveCamera(position) - - override fun animateCamera( - position: MapCameraPosition, - duration: Long, - ) = handleAnimateCamera(position, duration) - - override fun setCameraRestriction(restriction: CameraRestriction?) = handleCameraRestriction(restriction) - - override fun fitBounds( - bounds: GeoRectBounds, - padding: Int, - ) = handleFitBounds(bounds, padding) - - override fun onMapLongClick(point: Point): Boolean = handleMapLongClick(point) - - override fun onMapClick(point: Point): Boolean = handleMapClick(point) - - override fun onMove(detector: MoveGestureDetector): Boolean = handleMove(detector) - - override fun onMoveBegin(detector: MoveGestureDetector) = handleMoveBegin(detector) - - override fun onMoveEnd(detector: MoveGestureDetector) = handleMoveEnd(detector) - - /** - * マーカーのヒットテスト。クリックカスケードの先頭。 - * - * Mapbox は地図クリックの座標からそのまま引けるので、コアの - * [dispatchGeoMarkerClick] に委ねる(`clickable = false` の透過もそちら)。 - */ - override fun dispatchMarkerTap(position: GeoPointInterface): Boolean = - markerEventControllers.dispatchGeoMarkerClick(position) - - // 拡張ファイル(Style / Camera / Gestures)からは基底クラスの protected へ - // 触れないため、ここで internal の入口を用意しておく。 - - internal fun emitMapInitialized() { - mapInitializedCallback?.invoke() - } - - internal fun emitCameraMoveStart(position: MapCameraPosition) { - cameraMoveStartCallback?.invoke(position) - } - - internal fun emitCameraMove(position: MapCameraPosition) { - cameraMoveCallback?.invoke(position) - } - - internal fun emitCameraMoveEnd(position: MapCameraPosition) { - cameraMoveEndCallback?.invoke(position) - } - - internal suspend fun emitCameraPosition(position: MapCameraPosition) { - notifyMapCameraPosition(position) - } - - fun setupListeners() { - holder.map.subscribeCameraChanged { - val token = cameraUpdateToken.incrementAndGet() - defaultCoroutine.launch { - if (token != cameraUpdateToken.get()) return@launch - // Calculate camera position on background thread - val mapCameraPosition = getMapCameraPositionAsync() ?: return@launch - notifyMapCameraPosition(mapCameraPosition) - cameraMoveCallback?.invoke(mapCameraPosition) - } - } - holder.map.subscribeStyleLoaded { - holder.map.style?.let { style -> - applyProjection() - // When style reloads, our runtime sources/layers/images are dropped. - // Reattach overlays and ensure marker images exist, then redraw. - attachOverlaySourcesAndLayers(style) - markerEventControllers.forEach { controller -> - val renderer = controller.renderer as MapboxMarkerOverlayRenderer - renderer.ensureStyleImages(style) - renderer.redraw() - } - mainCoroutine.launch { - groundImageController.reapplyStyle() - rasterLayerController.reapplyStyle() - } - - // After style is ready, trigger an initial camera update - sendInitialCameraUpdate() - - style.toMapDesignType().let { mapDesign -> - this@MapboxMapViewController.mapDesignType = mapDesign - mapDesignTypeChangeListener?.invoke(mapDesign) - } - } - } - - holder.map.subscribeStyleImageMissing { evt -> - val missingId = evt.imageId - markerEventControllers.forEach { controller -> - (controller.renderer as MapboxMarkerOverlayRenderer).onStyleImageMissing(missingId) - } - } - holder.map.subscribeMapIdle { - mainCoroutine.launch { - getMapCameraPosition()?.let { mapCameraPosition -> - defaultCoroutine.launch { - notifyMapCameraPosition(mapCameraPosition) - cameraMoveEndCallback?.invoke(mapCameraPosition) - } - } - } - } - - holder.map.removeOnMapClickListener(this) - holder.map.addOnMapClickListener(this) - - holder.map.removeOnMapLongClickListener(this) - holder.map.addOnMapLongClickListener(this) - - holder.map.removeOnMoveListener(this) - holder.map.addOnMoveListener(this) - } - - fun setProjection(value: MapProjection) { - if (projection == value) return - projection = value - mainCoroutine.launch { - applyProjection() - } - } - - private fun applyProjection() { - if (holder.map.style == null) return - val name = - when (projection) { - MapProjection.Mercator -> ProjectionName.MERCATOR - MapProjection.Globe -> ProjectionName.GLOBE - } - holder.map.setProjection(Projection(name)) - } - - override suspend fun clearOverlays() { - markerController.clear() - polylineController.clear() - polygonController.clear() - groundImageController.clear() - rasterLayerController.clear() - } - - override fun setMarkerAnimationOverlayHost(host: MarkerAnimationOverlayHost?) { - (markerController.renderer as MapboxMarkerOverlayRenderer).animationOverlayHost = host - } - - override suspend fun compositionPolygons(data: List) { - polygonController.add(data) - holder.map.getStyle { ensurePolygonZLayers(it) } - } - - override suspend fun updatePolygon(state: PolygonState) { - polygonController.update(state) - holder.map.getStyle { ensurePolygonZLayers(it) } - } - - @Deprecated("Use CircleState.onClick instead.") - override fun setOnCircleClickListener(listener: OnCircleEventHandler?) { - this.circleController.clickListener = listener - } - - @Deprecated("Use GroundImageState.onClick instead.") - override fun setOnGroundImageClickListener(listener: OnGroundImageEventHandler?) { - this.groundImageController.clickListener = listener - } - - override fun hasPolyline(state: PolylineState): Boolean = - this.polylineController.polylineManager - .hasEntity(state.id) - - override fun hasPolygon(state: PolygonState): Boolean = - this.polygonController.polygonOverlay.polygonManager - .hasEntity(state.id) - - override fun applyUISettings(settings: MapUISettings) { - holder.mapView.gestures.apply { - scrollEnabled = settings.scrollGesture - pinchToZoomEnabled = settings.zoomGesture - doubleTapToZoomInEnabled = settings.zoomGesture - doubleTouchToZoomOutEnabled = settings.zoomGesture - quickZoomEnabled = settings.zoomGesture - rotateEnabled = settings.rotateGesture - pitchEnabled = settings.tiltGesture - } - } - - @Deprecated("Use MarkerState.onDragStart instead.") - override fun setOnMarkerDragStart(listener: OnMarkerEventHandler?) { - markerDragStartListener = listener - markerEventControllers.forEach { it.setDragStartListener(listener) } - } - - @Deprecated("Use MarkerState.onDrag instead.") - override fun setOnMarkerDrag(listener: OnMarkerEventHandler?) { - markerDragListener = listener - markerEventControllers.forEach { it.setDragListener(listener) } - } - - @Deprecated("Use MarkerState.onDragEnd instead.") - override fun setOnMarkerDragEnd(listener: OnMarkerEventHandler?) { - markerDragEndListener = listener - markerEventControllers.forEach { it.setDragEndListener(listener) } - } - - @Deprecated("Use MarkerState.onAnimateStart instead.") - override fun setOnMarkerAnimateStart(listener: OnMarkerEventHandler?) { - markerAnimateStartListener = listener - markerEventControllers.forEach { it.setAnimateStartListener(listener) } - } - - @Deprecated("Use MarkerState.onAnimateEnd instead.") - override fun setOnMarkerAnimateEnd(listener: OnMarkerEventHandler?) { - markerAnimateEndListener = listener - markerEventControllers.forEach { it.setAnimateEndListener(listener) } - } - - @Deprecated("Use MarkerState.onClick instead.") - override fun setOnMarkerClickListener(listener: OnMarkerEventHandler?) { - markerClickListener = listener - markerEventControllers.forEach { it.setClickListener(listener) } - } - - @Deprecated("Use PolylineState.onClick instead.") - override fun setOnPolylineClickListener(listener: OnPolylineEventHandler?) { - polylineController.clickListener = listener - } - - @Deprecated("Use PolygonState.onClick instead.") - override fun setOnPolygonClickListener(listener: OnPolygonEventHandler?) { - polygonController.clickListener = listener - } - - internal var mapDesignType: MapboxDesignType = MapboxMapDesign.Standard - - internal var mapDesignTypeChangeListener: MapboxMapDesignTypeChangeHandler? = null - - override fun setMapDesignType(value: MapboxDesignType) { - mainCoroutine.launch { - holder.mapView.mapboxMap.loadStyle(value.getValue()) - } - } - - override fun setMapDesignTypeChangeListener(listener: MapboxMapDesignTypeChangeHandler) { - mapDesignTypeChangeListener = listener - } - - internal fun registerMarkerEventController(controller: DefaultMarkerEventController) { - if (markerEventControllers.contains(controller)) return - markerEventControllers.add(controller) - controller.setClickListener(markerClickListener) - controller.setDragStartListener(markerDragStartListener) - controller.setDragListener(markerDragListener) - controller.setDragEndListener(markerDragEndListener) - controller.setAnimateStartListener(markerAnimateStartListener) - controller.setAnimateEndListener(markerAnimateEndListener) - - holder.map.style?.let { style -> - val renderer = controller.renderer as MapboxMarkerOverlayRenderer - attachMarkerLayers(style, renderer) - renderer.ensureStyleImages(style) - renderer.redraw() - } - } - - fun createMarkerRenderer( - strategy: MarkerRenderingStrategyInterface, - ): MarkerOverlayRendererInterface { - val groupId = UUID.randomUUID().toString() - val markerLayer = - MarkerLayer( - sourceId = "markers-source-$groupId", - layerId = "markers-layer-$groupId", - ) - val dragLayer = - MarkerDragLayer( - sourceId = "marker-drag-source-$groupId", - layerId = "marker-drag-layer-$groupId", - ) - return MapboxMarkerOverlayRenderer( - holder = holder, - markerManager = strategy.markerManager, - markerLayer = markerLayer, - dragLayer = dragLayer, - ) - } - - fun createMarkerEventController( - controller: StrategyMarkerController, - renderer: MarkerOverlayRendererInterface, - ): MarkerEventControllerInterface = DefaultMarkerEventController(controller) - - fun registerMarkerEventController(controller: MarkerEventControllerInterface) { - @Suppress("UNCHECKED_CAST") - val typed = controller as? DefaultMarkerEventController ?: return - registerMarkerEventController(typed) - } -} +package com.mapconductor.mapbox + +import com.mapconductor.core.circle.CIRCLE_TILE_LAYER_ID +import com.mapconductor.core.circle.CircleTiler +import com.mapconductor.core.circle.circleTileRasterLayerState +import com.mapconductor.core.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 +import com.mapbox.maps.extension.style.layers.properties.generated.ProjectionName +import com.mapbox.maps.extension.style.projection.generated.Projection +import com.mapbox.maps.extension.style.projection.generated.setProjection +import com.mapbox.maps.plugin.gestures.OnMapClickListener +import com.mapbox.maps.plugin.gestures.OnMapLongClickListener +import com.mapbox.maps.plugin.gestures.OnMoveListener +import com.mapbox.maps.plugin.gestures.addOnMapClickListener +import com.mapbox.maps.plugin.gestures.addOnMapLongClickListener +import com.mapbox.maps.plugin.gestures.addOnMoveListener +import com.mapbox.maps.plugin.gestures.gestures +import com.mapbox.maps.plugin.gestures.removeOnMapClickListener +import com.mapbox.maps.plugin.gestures.removeOnMapLongClickListener +import com.mapbox.maps.plugin.gestures.removeOnMoveListener +import com.mapconductor.core.circle.OnCircleEventHandler +import com.mapconductor.core.controller.BaseMapViewController +import com.mapconductor.core.features.GeoPointInterface +import com.mapconductor.core.features.GeoRectBounds +import com.mapconductor.core.groundimage.OnGroundImageEventHandler +import com.mapconductor.core.map.CameraRestriction +import com.mapconductor.core.map.MapCameraPosition +import com.mapconductor.core.map.MapPaddingsInterface +import com.mapconductor.core.map.MapPaddings +import com.mapconductor.core.map.MapProjection +import com.mapconductor.core.map.MapUISettings +import com.mapconductor.core.marker.DefaultMarkerEventController +import com.mapconductor.core.marker.MarkerAnimationOverlayHost +import com.mapconductor.core.marker.MarkerEventControllerInterface +import com.mapconductor.core.marker.MarkerOverlayRendererInterface +import com.mapconductor.core.marker.MarkerRenderingStrategyInterface +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.PolygonState +import com.mapconductor.core.polyline.OnPolylineEventHandler +import com.mapconductor.core.polyline.POLYLINE_TILE_LAYER_ID +import com.mapconductor.core.polyline.PolylineState +import com.mapconductor.core.polyline.PolylineTiler +import com.mapconductor.core.polyline.polylineTileRasterLayerState +import com.mapconductor.mapbox.circle.MapboxCircleController +import com.mapconductor.mapbox.groundimage.MapboxGroundImageController +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.MapboxPolygonController +import com.mapconductor.mapbox.polyline.MapboxPolylineController +import com.mapconductor.mapbox.raster.MapboxRasterLayerController +import java.util.UUID +import java.util.concurrent.atomic.AtomicInteger +import kotlinx.coroutines.CoroutineScope +import kotlinx.coroutines.Dispatchers +import kotlinx.coroutines.launch + +typealias MapboxMapDesignTypeChangeHandler = (MapboxDesignType) -> Unit + +/** + * Mapbox のマップコントローラ。 + * + * **型は公開、構築は非公開。** `createMapboxViewController()` の戻り型として、また + * React Native のような非 Compose ホストが `BaseMapViewController` として受けるために + * 型は見える必要がある(LongdoMapViewController / MapTilerMapViewController も公開)。 + * 一方コンストラクタ引数は internal なコントローラ群なので、`internal constructor` に + * して**組み立てはファクトリ 1 か所に閉じる**。2 経路で組むと配線が片方だけ増える。 + */ +class MapboxMapViewController internal constructor( + override val holder: MapboxMapViewHolder, + internal var projection: MapProjection, + internal val markerController: MapboxMarkerController, + internal val polylineController: MapboxPolylineController, + internal val polygonController: MapboxPolygonController, + internal val groundImageController: MapboxGroundImageController, + internal val circleController: MapboxCircleController, + internal val rasterLayerController: MapboxRasterLayerController, + override val mainCoroutine: CoroutineScope = CoroutineScope(Dispatchers.Main), + override val defaultCoroutine: CoroutineScope = CoroutineScope(Dispatchers.Default), +) : BaseMapViewController(), + MapboxMapViewControllerInterface, + OnMapClickListener, + OnMapLongClickListener, + OnMoveListener { + // Track created z-indexed polygon layers to manage add/remove without enumerating style layers + internal val polygonZLayers: MutableSet = mutableSetOf() + internal val markerEventControllers = mutableListOf>() + internal var activeDragController: DefaultMarkerEventController? = null + + internal val cameraUpdateToken = AtomicInteger(0) + internal var lastLogicalCameraPosition: MapCameraPosition? = null + + /** アプリが指定したパディング(dp)。ネイティブには渡さず、中心のずらしで再現する。 */ + internal var cameraPaddings: MapPaddingsInterface = MapPaddings.Zeros + + internal var markerClickListener: OnMarkerEventHandler? = null + internal var markerDragStartListener: OnMarkerEventHandler? = null + internal var markerDragListener: OnMarkerEventHandler? = null + internal var markerDragEndListener: OnMarkerEventHandler? = null + internal var markerAnimateStartListener: OnMarkerEventHandler? = null + internal var markerAnimateEndListener: OnMarkerEventHandler? = null + internal var involvedMapInitializedCallback: Boolean = false + + init { + setupListeners() + registerOverlayController(markerController) + registerOverlayController(polylineController) + registerOverlayController(polygonController) + registerOverlayController(groundImageController) + registerOverlayController(circleController) + registerOverlayController(rasterLayerController) + registerMarkerEventController(DefaultMarkerEventController(markerController)) + + markerController.setRasterLayerCallback { state -> + if (state != null) { + rasterLayerController.upsert(state) + } else { + val markerTileLayers = + rasterLayerController.rasterLayerManager + .allEntities() + .filter { it.state.id.startsWith("marker-tile-") } + 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) + } + } + } + + // Many circles too: the same raster layer path, in the circle band. + circleController.tiler = + CircleTiler().apply { + onTemplate = { template, tileSize -> + if (template != null) { + rasterLayerController.upsert(circleTileRasterLayerState(template, tileSize)) + } else { + rasterLayerController.removeById(CIRCLE_TILE_LAYER_ID) + } + } + } + + // Many polylines too: the same raster layer path, in the polyline band. + polylineController.tiler = + PolylineTiler().apply { + onTemplate = { template, tileSize -> + if (template != null) { + rasterLayerController.upsert(polylineTileRasterLayerState(template, tileSize)) + } else { + rasterLayerController.removeById(POLYLINE_TILE_LAYER_ID) + } + } + } + } + + override fun moveCamera(position: MapCameraPosition) = handleMoveCamera(position) + + override fun animateCamera( + position: MapCameraPosition, + duration: Long, + ) = handleAnimateCamera(position, duration) + + override fun setCameraRestriction(restriction: CameraRestriction?) = handleCameraRestriction(restriction) + + override fun fitBounds( + bounds: GeoRectBounds, + padding: Int, + ) = handleFitBounds(bounds, padding) + + override fun onMapLongClick(point: Point): Boolean = handleMapLongClick(point) + + override fun onMapClick(point: Point): Boolean = handleMapClick(point) + + override fun onMove(detector: MoveGestureDetector): Boolean = handleMove(detector) + + override fun onMoveBegin(detector: MoveGestureDetector) = handleMoveBegin(detector) + + override fun onMoveEnd(detector: MoveGestureDetector) = handleMoveEnd(detector) + + /** + * マーカーのヒットテスト。クリックカスケードの先頭。 + * + * Mapbox は地図クリックの座標からそのまま引けるので、コアの + * [dispatchGeoMarkerClick] に委ねる(`clickable = false` の透過もそちら)。 + */ + override fun dispatchMarkerTap(position: GeoPointInterface): Boolean = + markerEventControllers.dispatchGeoMarkerClick(position) + + // 拡張ファイル(Style / Camera / Gestures)からは基底クラスの protected へ + // 触れないため、ここで internal の入口を用意しておく。 + + internal fun emitMapInitialized() { + mapInitializedCallback?.invoke() + } + + internal fun emitCameraMoveStart(position: MapCameraPosition) { + cameraMoveStartCallback?.invoke(position) + } + + internal fun emitCameraMove(position: MapCameraPosition) { + cameraMoveCallback?.invoke(position) + } + + internal fun emitCameraMoveEnd(position: MapCameraPosition) { + cameraMoveEndCallback?.invoke(position) + } + + internal suspend fun emitCameraPosition(position: MapCameraPosition) { + notifyMapCameraPosition(position) + } + + fun setupListeners() { + holder.map.subscribeCameraChanged { + val token = cameraUpdateToken.incrementAndGet() + defaultCoroutine.launch { + if (token != cameraUpdateToken.get()) return@launch + // Calculate camera position on background thread + val mapCameraPosition = getMapCameraPositionAsync() ?: return@launch + notifyMapCameraPosition(mapCameraPosition) + cameraMoveCallback?.invoke(mapCameraPosition) + } + } + holder.map.subscribeStyleLoaded { + holder.map.style?.let { style -> + applyProjection() + // When style reloads, our runtime sources/layers/images are dropped. + // Reattach overlays and ensure marker images exist, then redraw. + attachOverlaySourcesAndLayers(style) + markerEventControllers.forEach { controller -> + val renderer = controller.renderer as MapboxMarkerOverlayRenderer + renderer.ensureStyleImages(style) + renderer.redraw() + } + mainCoroutine.launch { + groundImageController.reapplyStyle() + rasterLayerController.reapplyStyle() + } + + // After style is ready, trigger an initial camera update + sendInitialCameraUpdate() + + style.toMapDesignType().let { mapDesign -> + this@MapboxMapViewController.mapDesignType = mapDesign + mapDesignTypeChangeListener?.invoke(mapDesign) + } + + // Last, once the overlays are back on: a vector style + // adjustment lost everything it set with the old style, and + // this is the moment the new one is known to be complete. + styleLoadedListener?.invoke() + } + } + + holder.map.subscribeStyleImageMissing { evt -> + val missingId = evt.imageId + markerEventControllers.forEach { controller -> + (controller.renderer as MapboxMarkerOverlayRenderer).onStyleImageMissing(missingId) + } + } + holder.map.subscribeMapIdle { + mainCoroutine.launch { + getMapCameraPosition()?.let { mapCameraPosition -> + defaultCoroutine.launch { + notifyMapCameraPosition(mapCameraPosition) + cameraMoveEndCallback?.invoke(mapCameraPosition) + } + } + } + } + + holder.map.removeOnMapClickListener(this) + holder.map.addOnMapClickListener(this) + + holder.map.removeOnMapLongClickListener(this) + holder.map.addOnMapLongClickListener(this) + + holder.map.removeOnMoveListener(this) + holder.map.addOnMoveListener(this) + } + + fun setProjection(value: MapProjection) { + if (projection == value) return + projection = value + mainCoroutine.launch { + applyProjection() + } + } + + private fun applyProjection() { + if (holder.map.style == null) return + val name = + when (projection) { + MapProjection.Mercator -> ProjectionName.MERCATOR + MapProjection.Globe -> ProjectionName.GLOBE + } + holder.map.setProjection(Projection(name)) + } + + override suspend fun clearOverlays() { + markerController.clear() + polylineController.clear() + polygonController.clear() + groundImageController.clear() + circleController.clear() + rasterLayerController.clear() + // 拡張モジュール(クラスタリングなど)が登録した Strategy のマーカーも消す。 + clearStrategyMarkerControllers() + } + + override fun setMarkerAnimationOverlayHost(host: MarkerAnimationOverlayHost?) { + (markerController.renderer as MapboxMarkerOverlayRenderer).animationOverlayHost = host + } + + override suspend fun compositionPolygons(data: List) { + polygonController.add(data) + holder.map.getStyle { ensurePolygonZLayers(it) } + } + + override suspend fun updatePolygon(state: PolygonState) { + polygonController.update(state) + holder.map.getStyle { ensurePolygonZLayers(it) } + } + + @Deprecated("Use CircleState.onClick instead.") + override fun setOnCircleClickListener(listener: OnCircleEventHandler?) { + this.circleController.clickListener = listener + } + + @Deprecated("Use GroundImageState.onClick instead.") + override fun setOnGroundImageClickListener(listener: OnGroundImageEventHandler?) { + this.groundImageController.clickListener = listener + } + + override fun hasPolyline(state: PolylineState): Boolean = + this.polylineController.polylineManager + .hasEntity(state.id) + + override fun hasPolygon(state: PolygonState): Boolean = + this.polygonController.polygonManager + .hasEntity(state.id) + + override fun applyUISettings(settings: MapUISettings) { + holder.mapView.gestures.apply { + scrollEnabled = settings.scrollGesture + pinchToZoomEnabled = settings.zoomGesture + doubleTapToZoomInEnabled = settings.zoomGesture + doubleTouchToZoomOutEnabled = settings.zoomGesture + quickZoomEnabled = settings.zoomGesture + rotateEnabled = settings.rotateGesture + pitchEnabled = settings.tiltGesture + } + } + + @Deprecated("Use MarkerState.onDragStart instead.") + override fun setOnMarkerDragStart(listener: OnMarkerEventHandler?) { + markerDragStartListener = listener + markerEventControllers.forEach { it.setDragStartListener(listener) } + } + + @Deprecated("Use MarkerState.onDrag instead.") + override fun setOnMarkerDrag(listener: OnMarkerEventHandler?) { + markerDragListener = listener + markerEventControllers.forEach { it.setDragListener(listener) } + } + + @Deprecated("Use MarkerState.onDragEnd instead.") + override fun setOnMarkerDragEnd(listener: OnMarkerEventHandler?) { + markerDragEndListener = listener + markerEventControllers.forEach { it.setDragEndListener(listener) } + } + + @Deprecated("Use MarkerState.onAnimateStart instead.") + override fun setOnMarkerAnimateStart(listener: OnMarkerEventHandler?) { + markerAnimateStartListener = listener + markerEventControllers.forEach { it.setAnimateStartListener(listener) } + } + + @Deprecated("Use MarkerState.onAnimateEnd instead.") + override fun setOnMarkerAnimateEnd(listener: OnMarkerEventHandler?) { + markerAnimateEndListener = listener + markerEventControllers.forEach { it.setAnimateEndListener(listener) } + } + + @Deprecated("Use MarkerState.onClick instead.") + override fun setOnMarkerClickListener(listener: OnMarkerEventHandler?) { + markerClickListener = listener + markerEventControllers.forEach { it.setClickListener(listener) } + } + + @Deprecated("Use PolylineState.onClick instead.") + override fun setOnPolylineClickListener(listener: OnPolylineEventHandler?) { + polylineController.clickListener = listener + } + + @Deprecated("Use PolygonState.onClick instead.") + override fun setOnPolygonClickListener(listener: OnPolygonEventHandler?) { + polygonController.clickListener = listener + } + + internal var mapDesignType: MapboxDesignType = MapboxMapDesign.Standard + + internal var mapDesignTypeChangeListener: MapboxMapDesignTypeChangeHandler? = null + + /** + * Called once the map has a style up and the overlays are back on it. + * + * How a vector style adjustment survives a design change. Kept in step + * with `MapLibreViewController.styleLoadedListener`. + */ + internal var styleLoadedListener: (() -> Unit)? = null + + /** The document the map was last asked for, and the one it reported loading. */ + private var requestedStyle: String? = null + private var loadedStyleValue: String? = null + + /** + * Whether the style on screen is the document the map was last asked + * for. + * + * What [MapboxStyleMutationTarget] answers `isReady` with, and not the + * same question as "is a style loaded". A vector style hands the map a + * new document and then applies the deltas compiled for it; in between, + * the map still has the document being replaced, whose layers have + * other names, so every delta came back as a property this map "cannot + * set by name". Mirrors `MapLibreViewController.isShowingRequestedStyle`. + */ + internal fun isShowingRequestedStyle(): Boolean = + holder.map.style != null && loadedStyleValue == requestedStyle + + override fun setMapDesignType(value: MapboxDesignType) { + val document = value.getValue() + requestedStyle = document + mainCoroutine.launch { + holder.mapView.mapboxMap.loadStyle(document) { + loadedStyleValue = document + } + } + } + + override fun setMapDesignTypeChangeListener(listener: MapboxMapDesignTypeChangeHandler) { + mapDesignTypeChangeListener = listener + } + + internal fun registerMarkerEventController(controller: DefaultMarkerEventController) { + if (markerEventControllers.contains(controller)) return + markerEventControllers.add(controller) + controller.setClickListener(markerClickListener) + controller.setDragStartListener(markerDragStartListener) + controller.setDragListener(markerDragListener) + controller.setDragEndListener(markerDragEndListener) + controller.setAnimateStartListener(markerAnimateStartListener) + controller.setAnimateEndListener(markerAnimateEndListener) + + holder.map.style?.let { style -> + val renderer = controller.renderer as MapboxMarkerOverlayRenderer + attachMarkerLayers(style, renderer) + renderer.ensureStyleImages(style) + renderer.redraw() + } + } + + fun createMarkerRenderer( + strategy: MarkerRenderingStrategyInterface, + ): MarkerOverlayRendererInterface { + val groupId = UUID.randomUUID().toString() + val markerLayer = + MarkerLayer( + sourceId = "markers-source-$groupId", + layerId = "markers-layer-$groupId", + ) + val dragLayer = + MarkerDragLayer( + sourceId = "marker-drag-source-$groupId", + layerId = "marker-drag-layer-$groupId", + ) + return MapboxMarkerOverlayRenderer( + holder = holder, + markerManager = strategy.markerManager, + markerLayer = markerLayer, + dragLayer = dragLayer, + ) + } + + fun createMarkerEventController( + controller: StrategyMarkerController, + renderer: MarkerOverlayRendererInterface, + ): MarkerEventControllerInterface = DefaultMarkerEventController(controller) + + fun registerMarkerEventController(controller: MarkerEventControllerInterface) { + @Suppress("UNCHECKED_CAST") + val typed = controller as? DefaultMarkerEventController ?: return + registerMarkerEventController(typed) + } +} diff --git a/src/main/java/com/mapconductor/mapbox/MapboxMapViewControllerCamera.kt b/src/main/java/com/mapconductor/mapbox/MapboxMapViewControllerCamera.kt index fd3d37f..e574209 100644 --- a/src/main/java/com/mapconductor/mapbox/MapboxMapViewControllerCamera.kt +++ b/src/main/java/com/mapconductor/mapbox/MapboxMapViewControllerCamera.kt @@ -3,9 +3,12 @@ package com.mapconductor.mapbox import com.mapbox.maps.CameraBoundsOptions import com.mapbox.maps.plugin.animation.MapAnimationOptions import com.mapbox.maps.plugin.animation.flyTo +import android.view.View import com.mapconductor.core.features.GeoRectBounds +import com.mapconductor.core.map.CameraPaddingEmulation import com.mapconductor.core.map.CameraRestriction import com.mapconductor.core.map.MapCameraPosition +import com.mapconductor.core.map.MapPaddings import com.mapconductor.core.map.buildVisibleRegion import com.mapconductor.mapbox.zoom.ZoomAltitudeConverter import android.animation.Animator @@ -28,10 +31,65 @@ internal fun MapboxMapViewController.getMapCameraPosition(): MapCameraPosition? } internal fun MapboxMapViewController.readLogicalCameraPosition(): MapCameraPosition = - MapboxCameraStateSnapshot( - cameraState = holder.map.cameraState, - logicalTiltHint = lastLogicalCameraPosition?.tilt, - ).toMapCameraPosition() + CameraPaddingEmulation.fromNativeCamera( + MapboxCameraStateSnapshot( + cameraState = holder.map.cameraState, + logicalTiltHint = lastLogicalCameraPosition?.tilt, + density = density(), + ).toMapCameraPosition(), + cameraPaddings, + MAPBOX_VERTICAL_FOV, + viewportHeightDp(), + ) + +/** 地図の高さ(dp)。パディングのエミュレーションで遠近を解くのに使う。 */ +internal fun MapboxMapViewController.viewportHeightDp(): Double = holder.mapView.height / density().toDouble() + +/** + * パディングを中心のずらしに置き換えたカメラ。Mapbox のパディングは常に 0 にする。 + * + * ネイティブのパディングは視錐台を上へ広げる。tilt があると上のパディングが + * 大きいほど画面の上端が地平線の先まで届き、そこまでタイルを読みに行って固まる。 + * 中心をずらせば、パディング無しと同じ視錐台のまま target をパディング中央に置ける。 + * paddings が null なら今のパディングを使い続ける。 + */ +internal fun MapboxMapViewController.toNativeCamera(position: MapCameraPosition): MapCameraPosition { + position.paddings?.let { cameraPaddings = it } + return CameraPaddingEmulation + .toNativeCamera(position.copy(paddings = cameraPaddings), MAPBOX_VERTICAL_FOV, viewportHeightDp()) + .copy(paddings = MapPaddings.Zeros) +} + +/** 地図がまだ大きさを持たない(レイアウト前)ときは、パディングを解けないので最初のレイアウトまで待つ。 */ +private fun MapboxMapViewController.needsLayoutFor(position: MapCameraPosition): Boolean { + val paddings = position.paddings ?: cameraPaddings + val padded = paddings.top != 0.0 || paddings.left != 0.0 || paddings.bottom != 0.0 || paddings.right != 0.0 + return padded && holder.mapView.height <= 0 +} + +private fun MapboxMapViewController.afterFirstLayout(action: () -> Unit) { + holder.mapView.addOnLayoutChangeListener( + object : View.OnLayoutChangeListener { + override fun onLayoutChange( + v: View, + left: Int, + top: Int, + right: Int, + bottom: Int, + oldLeft: Int, + oldTop: Int, + oldRight: Int, + oldBottom: Int, + ) { + if (bottom - top <= 0) return + v.removeOnLayoutChangeListener(this) + action() + } + }, + ) +} + +internal fun MapboxMapViewController.density(): Float = holder.mapView.resources.displayMetrics.density /** * Async version of getMapCameraPosition that can be called from background threads. @@ -43,10 +101,13 @@ internal suspend fun MapboxMapViewController.getMapCameraPositionAsync(): MapCam } internal fun MapboxMapViewController.handleMoveCamera(position: MapCameraPosition) { - val cameraOptions = position.toCameraOptions() lastLogicalCameraPosition = position mainCoroutine.launch { - holder.map.setCamera(cameraOptions) + if (needsLayoutFor(position)) { + afterFirstLayout { handleMoveCamera(position) } + return@launch + } + holder.map.setCamera(toNativeCamera(position).toCameraOptions(density())) } } @@ -54,7 +115,11 @@ internal fun MapboxMapViewController.handleAnimateCamera( position: MapCameraPosition, duration: Long, ) { - val targetCamera = position.toCameraOptions() + if (needsLayoutFor(position)) { + afterFirstLayout { handleAnimateCamera(position, duration) } + return + } + val targetCamera = toNativeCamera(position).toCameraOptions(density()) lastLogicalCameraPosition = position val animationOptions = diff --git a/src/main/java/com/mapconductor/mapbox/MapboxMapViewControllerStyle.kt b/src/main/java/com/mapconductor/mapbox/MapboxMapViewControllerStyle.kt index 5137b98..3f08602 100644 --- a/src/main/java/com/mapconductor/mapbox/MapboxMapViewControllerStyle.kt +++ b/src/main/java/com/mapconductor/mapbox/MapboxMapViewControllerStyle.kt @@ -1,9 +1,7 @@ package com.mapconductor.mapbox -import com.mapbox.maps.extension.style.layers.addLayer -import com.mapbox.maps.extension.style.layers.addLayerAbove -import com.mapbox.maps.extension.style.layers.addLayerBelow import com.mapbox.maps.extension.style.sources.addSource +import com.mapconductor.core.controller.OverlayKind import com.mapconductor.mapbox.marker.MapboxMarkerOverlayRenderer /** @@ -21,35 +19,37 @@ internal fun MapboxMapViewController.attachMarkerLayers( style.addSource(renderer.markerLayer.source) } catch (_: Exception) { } - try { - style.addLayer(renderer.markerLayer.layer) - } catch (_: Exception) { - } + MapboxOverlayStacking.add(holder.map, style, renderer.markerLayer.layer, MapboxOverlayStacking.key(OverlayKind.Marker)) try { style.addSource(renderer.dragLayer.source) } catch (_: Exception) { } - try { - style.addLayer(renderer.dragLayer.layer) - } catch (_: Exception) { - } + MapboxOverlayStacking.add(holder.map, style, renderer.dragLayer.layer, MapboxOverlayStacking.MARKER_DRAG) } 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) - style.addLayer(circleController.renderer.layer.fillLayer) - style.addLayer(circleController.renderer.layer.strokeLayer) + // 層の位置は MapboxOverlayStacking が帯の順で決める(追加の順に依らない)。 + // 線は塗りの直後(同じ鍵なら後から入れたものが上)。 + val circleKey = MapboxOverlayStacking.key(OverlayKind.Circle) + MapboxOverlayStacking.add(holder.map, style, circleController.renderer.layer.fillLayer, circleKey) + MapboxOverlayStacking.add(holder.map, style, circleController.renderer.layer.strokeLayer, circleKey) // Polyline (general) style.addSource(polylineController.renderer.layer.source) - style.addLayer(polylineController.renderer.layer.layer) + MapboxOverlayStacking.add( + holder.map, + style, + polylineController.renderer.layer.layer, + MapboxOverlayStacking.key(OverlayKind.Polyline), + ) - // Add z-indexed polygon layers below general polylines + // Polygon z-layers (polygon band: above ground images, below circles) ensurePolygonZLayers(style) // Marker + drag layers @@ -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() @@ -76,14 +76,8 @@ internal fun MapboxMapViewController.ensurePolygonZLayers(style: com.mapbox.maps toRemove.forEach { z -> val fillId = "polygon-fill-layer-$z" val outlineId = "polygon-outline-layer-$z" - try { - style.removeStyleLayer(outlineId) - } catch (_: Exception) { - } - try { - style.removeStyleLayer(fillId) - } catch (_: Exception) { - } + MapboxOverlayStacking.remove(holder.map, style, outlineId) + MapboxOverlayStacking.remove(holder.map, style, fillId) } val zList = zSet.toList().sorted() @@ -108,11 +102,7 @@ internal fun MapboxMapViewController.ensurePolygonZLayers(style: com.mapbox.maps .get("fillColor"), ) } - try { - style.addLayerBelow(layer, anchorId) - } catch (_: Exception) { - style.addLayer(layer) - } + MapboxOverlayStacking.add(holder.map, style, layer, MapboxOverlayStacking.key(OverlayKind.Polygon, z)) } // Outline layer above its fill @@ -138,11 +128,8 @@ internal fun MapboxMapViewController.ensurePolygonZLayers(style: com.mapbox.maps .get("strokeWidth"), ) } - try { - style.addLayerAbove(layer, fillId) - } catch (_: Exception) { - style.addLayer(layer) - } + // 塗りの直後(同じ鍵なら後から入れたものが上)。 + MapboxOverlayStacking.add(holder.map, style, layer, MapboxOverlayStacking.key(OverlayKind.Polygon, z)) } } // Update tracked set diff --git a/src/main/java/com/mapconductor/mapbox/MapboxOverlayStacking.kt b/src/main/java/com/mapconductor/mapbox/MapboxOverlayStacking.kt new file mode 100644 index 0000000..06c1281 --- /dev/null +++ b/src/main/java/com/mapconductor/mapbox/MapboxOverlayStacking.kt @@ -0,0 +1,96 @@ +package com.mapconductor.mapbox + +import android.util.Log +import com.mapbox.maps.MapboxMap +import com.mapbox.maps.Style +import com.mapbox.maps.extension.style.layers.Layer +import com.mapbox.maps.extension.style.layers.addLayer +import com.mapbox.maps.extension.style.layers.addLayerBelow +import com.mapconductor.core.controller.OverlayKind +import com.mapconductor.core.controller.OverlayLayerStack +import com.mapconductor.core.controller.OverlayStacking +import java.util.WeakHashMap + +/** + * オーバーレイの層をスタイルのどこへ入れるか([OverlayStacking] の帯の順)。 + * + * 下から: ラスター < GroundImage < ポリゴン < サークル < ポリライン < マーカー + * (マーカーのタイルはマーカーの帯の一番下、ドラッグ中の層は一番上。 + * ポリゴンのタイルはポリゴンの帯の一番下)。 + * + * 位置は毎回スタイルの今の並びから決めるので、追加した順・送り直し・ + * スタイルの読み直し(デザイン変更)に左右されない。 + * + * ラスターだけはベースマップのラベル(SDK の層でない最初の symbol 層)の下に入れる。 + * それ以外の帯はラベルの上に乗る(従来どおり)。Standard スタイルのように + * ベースマップが import で、その層が `styleLayers` に出てこない場合は、 + * ラスターも含めてベースマップ全体の上に乗る。 + */ +internal object MapboxOverlayStacking { + private val stacks = WeakHashMap() + + fun stackOf(map: MapboxMap): OverlayLayerStack = synchronized(stacks) { stacks.getOrPut(map) { OverlayLayerStack() } } + + fun key( + kind: OverlayKind, + zIndex: Int = 0, + ) = OverlayStacking.Key(kind, zIndex) + + /** マーカーのタイル(ラスター): マーカーの帯の一番下。 */ + val MARKER_TILE = key(OverlayKind.Marker, Int.MIN_VALUE) + + /** ポリゴンのタイル(ラスター): ポリゴンの帯の一番下。 */ + val POLYGON_TILE = key(OverlayKind.Polygon, Int.MIN_VALUE) + + /** 円のタイル(ラスター): サークルの帯の一番下(ポリゴンの上、ネイティブの円とポリラインのタイルの下)。 */ + val CIRCLE_TILE = key(OverlayKind.Circle, Int.MIN_VALUE) + + /** ポリラインのタイル(ラスター): ポリラインの帯の一番下(サークルの上、ネイティブのポリラインの下)。 */ + val POLYLINE_TILE = key(OverlayKind.Polyline, Int.MIN_VALUE) + + /** ドラッグ中のマーカーの層: マーカーの帯の一番上。 */ + val MARKER_DRAG = key(OverlayKind.Marker, Int.MAX_VALUE) + + /** [layer] を [key] の位置へ足す。同じ id の層が既にあれば何もしない。 */ + fun add( + map: MapboxMap, + style: Style, + layer: Layer, + key: OverlayStacking.Key, + ) { + val stack = stackOf(map) + stack.put(layer.layerId, key) + if (style.styleLayerExists(layer.layerId)) return + val layers = style.styleLayers + val ids = layers.map { it.id } + var before = stack.beforeId(ids, key) + if (key.kind == OverlayKind.RasterLayer) { + val label = layers.firstOrNull { it.type == "symbol" && stack.keyOf(it.id) == null }?.id + if (label != null && (before == null || ids.indexOf(label) < ids.indexOf(before))) { + before = label + } + } + try { + if (before != null) style.addLayerBelow(layer, before) else style.addLayer(layer) + } catch (e: Exception) { + if (style.styleLayerExists(layer.layerId)) return + try { + style.addLayer(layer) + } catch (e2: Exception) { + Log.w("Mapbox", "Failed to add layer: ${layer.layerId} (${e2.message})") + } + } + } + + fun remove( + map: MapboxMap, + style: Style?, + layerId: String, + ) { + stackOf(map).remove(layerId) + try { + style?.removeStyleLayer(layerId) + } catch (_: Exception) { + } + } +} diff --git a/src/main/java/com/mapconductor/mapbox/MapboxStyleMutationTarget.kt b/src/main/java/com/mapconductor/mapbox/MapboxStyleMutationTarget.kt new file mode 100644 index 0000000..43cbe09 --- /dev/null +++ b/src/main/java/com/mapconductor/mapbox/MapboxStyleMutationTarget.kt @@ -0,0 +1,142 @@ +package com.mapconductor.mapbox + +import com.mapbox.bindgen.Expected +import com.mapbox.bindgen.None +import com.mapbox.bindgen.Value +import com.mapbox.maps.LayerPosition +import com.mapbox.maps.Style +import com.mapconductor.core.InternalMapConductorApi +import com.mapconductor.core.map.StyleMutationTarget + +/** + * Changes a loaded Mapbox style in place. + * + * The seven calls [StyleMutationTarget] asks for, against Mapbox's style + * API. Everything above them — what a mutation means, which can be skipped, + * keeping one bad property from costing the rest — is + * `applyStyleMutations` in core, shared with every other provider. + * + * ## Why this is shorter than the MapLibre one + * + * Mapbox v11 keeps a generic door open: `setStyleLayerProperty` takes the + * spec's own property name and a `Value`, and `Value.fromJson` builds one + * straight from text. Paint keys, layout keys, `filter`, `minzoom` and + * `maxzoom` all go through that one call, and a whole layer document goes + * through `addStyleLayer`. There is no typed-constructor detour and no + * expression builder, so the compiler's JSON reaches the renderer unread. + * + * MapLibre on Android has no such door, which is why + * `MapLibreStyleMutationTarget` is three times the length for the same seven + * calls. The adapters differ; the decisions above them do not. + */ +@InternalMapConductorApi +internal class MapboxStyleMutationTarget( + private val style: () -> Style?, + /** + * Whether the style on screen is the document these deltas are for. + * + * Not `style() != null`: a vector style hands the map a new document + * and applies the deltas compiled for it straight after, and in between + * the map still has the one being replaced. See + * `MapboxMapViewController.isShowingRequestedStyle`. + */ + private val ready: () -> Boolean, +) : StyleMutationTarget { + override val isReady: Boolean get() = ready() + + override fun hasLayer(layerId: String): Boolean = style()?.styleLayerExists(layerId) == true + + override fun setPaint( + layerId: String, + key: String, + valueJson: String, + ) = setProperty(layerId, key, valueJson) + + // Mapbox does not split paint from layout on this call: both are the + // spec's property name on the layer. + override fun setLayout( + layerId: String, + key: String, + valueJson: String, + ) = setProperty(layerId, key, valueJson) + + override fun setFilter( + layerId: String, + filterJson: String, + ) = setProperty(layerId, "filter", filterJson) + + override fun setZoomRange( + layerId: String, + minZoom: Float, + maxZoom: Float, + ) { + val target = style() ?: return + // Both ends, because the caller always sends both -- a mutation that + // moves one has to restate the other to be undoable. + orThrow("minzoom", target.setStyleLayerProperty(layerId, "minzoom", Value(minZoom.toDouble()))) + orThrow("maxzoom", target.setStyleLayerProperty(layerId, "maxzoom", Value(maxZoom.toDouble()))) + } + + override fun addLayer( + layerId: String, + layerJson: String, + beforeId: String?, + ) { + val target = style() ?: return + // Already there: the style was reloaded and the whole set is going + // back on. + if (target.styleLayerExists(layerId)) return + val layer = valueOf(layerJson) + // `beforeId` is the layer to go *under*, matching the style + // document's own order -- earlier in the list is drawn first. + val position = + beforeId + ?.takeIf { target.styleLayerExists(it) } + ?.let { LayerPosition(null, it, null) } + orThrow("addLayer", target.addStyleLayer(layer, position)) + } + + override fun removeLayer(layerId: String) { + val target = style() ?: return + // Mapbox calls removing an absent layer an error; the contract says + // it is not one. + if (!target.styleLayerExists(layerId)) return + orThrow("removeLayer", target.removeStyleLayer(layerId)) + } + + private fun setProperty( + layerId: String, + key: String, + valueJson: String, + ) { + val target = style() ?: return + orThrow(key, target.setStyleLayerProperty(layerId, key, valueOf(valueJson))) + } + + /** + * A JSON text as the `Value` Mapbox wants. + * + * Literals, expression arrays and whole layer objects all arrive the same + * way, so there is nothing here to decide — unlike MapLibre, which needs + * the value's shape chosen for it. + */ + private fun valueOf(json: String): Value { + val parsed = Value.fromJson(json) + if (parsed.isError) throw IllegalArgumentException("not a style value: ${parsed.error}") + return parsed.value ?: Value.nullValue() + } + + /** + * Mapbox answers with an error rather than throwing, and a refused + * property that went unnoticed would leave the map looking unlike what + * the rules asked for with nothing said. Throwing puts it back on the + * path core already has for this: the mutation comes back unapplied and + * the caller reports it. + */ + private fun orThrow( + what: String, + result: Expected, + ) { + if (result.isError) throw UnsupportedOperationException("$what: ${result.error}") + } +} diff --git a/src/main/java/com/mapconductor/mapbox/MapboxViewStateImpl.kt b/src/main/java/com/mapconductor/mapbox/MapboxViewStateImpl.kt index b2ab6c7..57fa0f0 100644 --- a/src/main/java/com/mapconductor/mapbox/MapboxViewStateImpl.kt +++ b/src/main/java/com/mapconductor/mapbox/MapboxViewStateImpl.kt @@ -9,6 +9,7 @@ import com.mapconductor.compose.map.BaseMapViewSaver import com.mapconductor.core.map.MapCameraPosition import com.mapconductor.core.map.MapCameraPositionInterface import com.mapconductor.core.map.MapViewState +import com.mapconductor.core.map.MapViewStateBundle import com.mapconductor.core.map.MapViewStateInterface import com.mapconductor.mapbox.MapboxMapDesign.Standard import java.util.UUID @@ -22,7 +23,10 @@ class MapboxViewState( cameraPosition: MapCameraPosition = MapCameraPosition.Default, ) : MapViewState(cameraPosition), MapboxViewStateInterface { - private var controller: MapboxMapViewControllerInterface? = null + // Read through the base so that letting go of the controller there (MapViewBase + // does it when the map is torn down) leaves nothing behind here. + private val controller: MapboxMapViewControllerInterface? + get() = attachedMapController as? MapboxMapViewControllerInterface private var _mapDesignType: MapboxDesignType = mapDesignType @@ -34,11 +38,13 @@ class MapboxViewState( get() = _mapDesignType internal fun setController(controller: MapboxMapViewControllerInterface) { - this.controller = controller attachController(controller) } internal fun onMapDesignTypeChange(value: MapboxDesignType) { + // 地図から読み戻した値はスタイル URI から作ったデザインで、attributionRules を持たない。 + // 同じスタイルなら、アプリが渡した(あるいは復元した)デザインを残す。 + if (value.getValue() == _mapDesignType.getValue()) return _mapDesignType = value } @@ -55,7 +61,20 @@ class MapboxMapViewSaver : BaseMapViewSaver() { state: MapboxViewState, bundle: Bundle, ) { - bundle.putString("id", state.mapDesignType.id) + val design = state.mapDesignType + bundle.putString("id", design.id) + // id だけでは Custom / StyleUri / 定義済みの区別がつかず、StyleUri(URI そのもの)が + // mapbox://styles/mapbox/ の Custom として戻ってしまう。 + val kind = + when (design) { + is MapboxMapDesign.StyleUri -> KIND_STYLE_URI + is MapboxMapDesign.Custom -> KIND_CUSTOM + is MapboxMapDesign -> if (design === MapboxMapDesign.create(design.id)) KIND_PRESET else KIND_VALUE + else -> KIND_VALUE + } + bundle.putString(KEY_KIND, kind) + bundle.putString(KEY_VALUE, design.getValue()) + MapViewStateBundle.putAttributionRules(bundle, design.attributionRules) } override fun createState( @@ -65,16 +84,42 @@ class MapboxMapViewSaver : BaseMapViewSaver() { ): MapboxViewState = MapboxViewState( id = stateId, - mapDesignType = - MapboxMapDesign.create( - layerId = mapDesignBundle?.getString("id") ?: Standard.id, - ), + mapDesignType = restoreDesign(mapDesignBundle), cameraPosition = cameraPosition, ) + private fun restoreDesign(bundle: Bundle?): MapboxMapDesign { + val id = bundle?.getString("id") ?: return Standard + val rules = MapViewStateBundle.getAttributionRules(bundle) + return when (bundle.getString(KEY_KIND)) { + KIND_STYLE_URI -> MapboxMapDesign.StyleUri(uri = id, attributionRules = rules) + KIND_CUSTOM -> MapboxMapDesign.Custom(layerId = id, attributionRules = rules) + KIND_VALUE -> { + // アプリ独自の実装。地図に渡していた値(スタイル URI)から作り直す。 + val value = bundle.getString(KEY_VALUE) ?: id + val prefix = "${MapboxMapDesign.MAPBOX_URL}/" + if (value.startsWith(prefix)) { + MapboxMapDesign.Custom(layerId = value.removePrefix(prefix), attributionRules = rules) + } else { + MapboxMapDesign.StyleUri(uri = value, attributionRules = rules) + } + } + // KIND_PRESET と、kind を持たない以前の形式 + else -> MapboxMapDesign.create(layerId = id) + } + } + override fun getStateId(state: MapboxViewState): String = state.id } +// 公開 API の門番(javap)にコンパニオンの定数が載らないよう、ファイル private にする。 +private const val KEY_KIND = "kind" +private const val KEY_VALUE = "value" +private const val KIND_PRESET = "preset" +private const val KIND_CUSTOM = "custom" +private const val KIND_STYLE_URI = "styleUri" +private const val KIND_VALUE = "value" + @Composable fun rememberMapboxMapViewState( mapDesign: MapboxDesignType = Standard, diff --git a/src/main/java/com/mapconductor/mapbox/circle/MapboxCircleOverlayRenderer.kt b/src/main/java/com/mapconductor/mapbox/circle/MapboxCircleOverlayRenderer.kt index f0a7e86..2149b04 100644 --- a/src/main/java/com/mapconductor/mapbox/circle/MapboxCircleOverlayRenderer.kt +++ b/src/main/java/com/mapconductor/mapbox/circle/MapboxCircleOverlayRenderer.kt @@ -65,7 +65,7 @@ class MapboxCircleOverlayRenderer( } override suspend fun onPostProcess() { - val circles = circleManager.allEntities() + val circles = circleManager.nativeEntities().filter { (it.circle as MapboxActualCircle?) != null } coroutine.launch { layer.draw(circles) } diff --git a/src/main/java/com/mapconductor/mapbox/groundimage/MapboxGroundImageOverlayRenderer.kt b/src/main/java/com/mapconductor/mapbox/groundimage/MapboxGroundImageOverlayRenderer.kt index 34b8679..2a5c186 100644 --- a/src/main/java/com/mapconductor/mapbox/groundimage/MapboxGroundImageOverlayRenderer.kt +++ b/src/main/java/com/mapconductor/mapbox/groundimage/MapboxGroundImageOverlayRenderer.kt @@ -3,13 +3,12 @@ package com.mapconductor.mapbox.groundimage import com.mapbox.bindgen.DataRef import com.mapbox.maps.Image import com.mapbox.maps.Style -import com.mapbox.maps.extension.style.layers.addLayer -import com.mapbox.maps.extension.style.layers.addLayerBelow import com.mapbox.maps.extension.style.layers.generated.rasterLayer import com.mapbox.maps.extension.style.layers.properties.generated.Visibility import com.mapbox.maps.extension.style.sources.addSource import com.mapbox.maps.extension.style.sources.generated.imageSource import com.mapbox.maps.extension.style.utils.TypeUtils +import com.mapconductor.core.controller.OverlayKind import com.mapconductor.core.features.GeoRectBounds import com.mapconductor.core.groundimage.AbstractGroundImageOverlayRenderer import com.mapconductor.core.groundimage.GroundImageEntityInterface @@ -17,6 +16,7 @@ import com.mapconductor.core.groundimage.GroundImageFingerPrint import com.mapconductor.core.groundimage.GroundImageState import com.mapconductor.mapbox.MapboxActualGroundImage import com.mapconductor.mapbox.MapboxMapViewHolder +import com.mapconductor.mapbox.MapboxOverlayStacking import java.nio.ByteBuffer import android.annotation.SuppressLint import android.graphics.Bitmap @@ -134,25 +134,15 @@ class MapboxGroundImageOverlayRenderer( Log.w("Mapbox", "Failed to add ground image source: ${e.message}") } - try { - style.addLayerBelow(layer, BELOW_LAYER_ID) - } catch (_: Exception) { - try { - style.addLayer(layer) - } catch (e: Exception) { - Log.w("Mapbox", "Failed to add ground image layer: ${e.message}") - } - } + // GroundImage の帯(ラスターの上、ポリゴンの下)。送り直しても同じ位置。 + MapboxOverlayStacking.add(holder.map, style, layer, MapboxOverlayStacking.key(OverlayKind.GroundImage)) } private fun removeSourceAndLayerIfExists( style: Style, handle: MapboxGroundImageHandle, ) { - try { - style.removeStyleLayer(handle.layerId) - } catch (_: Exception) { - } + MapboxOverlayStacking.remove(holder.map, style, handle.layerId) try { style.removeStyleSource(handle.sourceId) } catch (_: Exception) { @@ -224,7 +214,6 @@ class MapboxGroundImageOverlayRenderer( opacity = opacity, ) - companion object { - private const val BELOW_LAYER_ID = "polyline-layer" - } + // 公開 API(Companion)の互換のために残している。 + companion object } diff --git a/src/main/java/com/mapconductor/mapbox/marker/MapboxMarkerController.kt b/src/main/java/com/mapconductor/mapbox/marker/MapboxMarkerController.kt index f1b766d..ee04202 100644 --- a/src/main/java/com/mapconductor/mapbox/marker/MapboxMarkerController.kt +++ b/src/main/java/com/mapconductor/mapbox/marker/MapboxMarkerController.kt @@ -1,343 +1,381 @@ -package com.mapconductor.mapbox.marker - -import com.mapconductor.core.controller.OnCameraChangeReceiverInterface -import com.mapconductor.core.map.MapCameraPosition -import com.mapconductor.core.marker.AbstractMarkerController -import com.mapconductor.core.marker.BitmapIcon -import com.mapconductor.core.marker.DefaultMarkerIcon -import com.mapconductor.core.marker.MarkerEntity -import com.mapconductor.core.marker.MarkerEntityInterface -import com.mapconductor.core.marker.MarkerIngestionEngine -import com.mapconductor.core.marker.MarkerManager -import com.mapconductor.core.marker.MarkerOverlayRendererInterface -import com.mapconductor.core.marker.MarkerState -import com.mapconductor.core.marker.MarkerTileRasterLayerCallback -import com.mapconductor.core.marker.MarkerTileRenderer -import com.mapconductor.core.marker.MarkerTilingOptions -import com.mapconductor.core.marker.MarkerViewportSwitch -import com.mapconductor.core.raster.RasterLayerSource -import com.mapconductor.core.raster.RasterLayerState -import com.mapconductor.core.raster.TileScheme -import com.mapconductor.core.tileserver.TileServerRegistry -import com.mapconductor.mapbox.MapboxActualMarker -import com.mapconductor.mapbox.MapboxMapViewHolder -import com.mapconductor.mapbox.toMapCameraPosition -import java.util.UUID -import kotlinx.coroutines.launch -import kotlinx.coroutines.sync.withPermit - -class MapboxMarkerController private constructor( - renderer: MapboxMarkerOverlayRenderer, - markerManager: MarkerManager, - private val markerTiling: MarkerTilingOptions, -) : AbstractMarkerController( - markerManager = markerManager, - renderer = renderer, - ), - OnCameraChangeReceiverInterface { - private val defaultMarkerIcon: BitmapIcon = DefaultMarkerIcon().toBitmapIcon() - private val tiledMarkerIds = LinkedHashSet() - private lateinit var lastCameraPosition: MapCameraPosition - - // Tile rendering via RasterLayer - private val tileServer = TileServerRegistry.get() - private var markerTileRenderer: MarkerTileRenderer? = null - private var markerTileGroupId: String? = null - private var markerTileRasterLayerState: RasterLayerState? = null - private var rasterLayerCallback: MarkerTileRasterLayerCallback? = null - private var cacheVersion: Int = 0 - - /** - * ビューポート内が少ないときだけタイルをやめてネイティブマーカーで描く切り替え器。 - * - * レンダラ/マネージャ/semaphore を共有するので、コントローラと同じ排他の下で動く。 - */ - private val viewportSwitch = - MarkerViewportSwitch( - markerManager = markerManager, - renderer = renderer, - defaultMarkerIcon = defaultMarkerIcon, - semaphore = semaphore, - policy = markerTiling.viewport, - setTileLayerVisible = ::setTileLayerVisible, - invalidateTiles = ::updateRasterLayerSource, - ) - - fun setRasterLayerCallback(callback: MarkerTileRasterLayerCallback?) { - rasterLayerCallback = callback - } - - override suspend fun add(data: List) { - // ingest はタイル担当 entity を marker = null で登録し直すので、先に昇格を戻す。 - // semaphore は再入不可なので withPermit の外で呼ぶこと。 - viewportSwitch.retract() - semaphore.withPermit { - val tilingEnabled = - markerTiling.enabled && data.size >= markerManager.minMarkerCount - val result = - MarkerIngestionEngine.ingest( - data = data, - markerManager = markerManager, - renderer = renderer, - defaultMarkerIcon = defaultMarkerIcon, - tilingEnabled = tilingEnabled, - tiledMarkerIds = tiledMarkerIds, - shouldTile = { state -> !state.draggable && state.getAnimation() == null }, - ) - - if (result.tiledDataChanged) { - syncTiledOverlay() - } else if (result.hasTiledMarkers) { - if (markerTileRenderer == null || markerTileRasterLayerState == null) { - syncTiledOverlay() - } - } else { - removeTileOverlay() - } - } - viewportSwitch.requestReapply() - } - - override suspend fun clear() { - viewportSwitch.destroy() - semaphore.withPermit { - val entities = markerManager.allEntities() - val toRemove = entities.filter { it.marker != null } - if (toRemove.isNotEmpty()) { - renderer.onRemove(toRemove) - } - markerManager.clear() - tiledMarkerIds.clear() - removeTileOverlay() - } - viewportSwitch.requestReapply() - } - - override suspend fun update(state: MarkerState) { - if (!markerManager.hasEntity(state.id)) return - - // 昇格中の 1 件なら先に取り下げる(下で marker = null 登録があるため)。 - // 昇格していないマーカー(ドラッグ中など)では何もしないので、ドラッグは素通りする。 - if (viewportSwitch.isPromoted(state.id)) viewportSwitch.release(state.id) - - val prevEntity = markerManager.getEntity(state.id) ?: return - val currentFinger = state.fingerPrint() - val prevFinger = prevEntity.fingerPrint - if (currentFinger == prevFinger) return - - semaphore.withPermit { - val tilingEnabled = - markerTiling.enabled && markerManager.allEntities().size >= markerManager.minMarkerCount - val wantsTiled = tilingEnabled && !state.draggable && state.getAnimation() == null - val wasTiled = tiledMarkerIds.contains(state.id) - val markerIcon = state.icon?.toBitmapIcon() ?: defaultMarkerIcon - - if (wantsTiled) { - if (!wasTiled) { - prevEntity.marker?.let { renderer.onRemove(listOf(prevEntity)) } - tiledMarkerIds.add(state.id) - } - markerManager.updateEntity( - MarkerEntity( - marker = null, - state = state, - visible = prevEntity.visible, - isRendered = true, - // tiling を立てないと MarkerTileRenderer の絞り込みから漏れ、 - // タイル昇格したのにタイルへ描かれないマーカーになる。 - tiling = true, - ), - ) - renderer.onPostProcess() - syncTiledOverlay() - return@withPermit - } - - if (wasTiled) { - tiledMarkerIds.remove(state.id) - } - - val params = - object : MarkerOverlayRendererInterface.ChangeParamsInterface { - override val current: MarkerEntityInterface = - MarkerEntity( - marker = prevEntity.marker, - state = state, - visible = prevEntity.visible, - isRendered = true, - ) - override val bitmapIcon: BitmapIcon = markerIcon - override val prev: MarkerEntityInterface = prevEntity - } - val markers = renderer.onChange(listOf(params)) - markers.firstOrNull()?.let { actual -> - markerManager.updateEntity( - MarkerEntity( - marker = actual, - state = state, - visible = prevEntity.visible, - isRendered = true, - ), - ) - if (prevFinger.animation != currentFinger.animation) { - state.getAnimation()?.let { renderer.onAnimate(markerManager.getEntity(state.id)!!) } - } - } - renderer.onPostProcess() - - if (tiledMarkerIds.isNotEmpty()) { - syncTiledOverlay() - } else { - removeTileOverlay() - } - } - } - - override suspend fun onCameraChanged(mapCameraPosition: MapCameraPosition) { - // 判定と昇格は debounce したうえで切り替え器の中で走る(パン中は動かない)。 - viewportSwitch.onCameraChanged(mapCameraPosition) - lastCameraPosition = mapCameraPosition - } - - /** - * マーカータイルのラスターレイヤの表示だけを切り替える。 - * - * source(URL)には触らない。触るとタイルを取り直すことになり、切り替えのたびに - * タイルキャッシュを捨てるのと同じになる。 - */ - private suspend fun setTileLayerVisible(visible: Boolean) { - val current = markerTileRasterLayerState ?: return - if (current.visible == visible) return - val newState = current.copy(visible = visible) - markerTileRasterLayerState = newState - rasterLayerCallback?.onRasterLayerUpdate(newState) - } - - override fun destroy() { - viewportSwitch.destroy() - // Clean up tile server registration - // Unregister this map's route only. Never stop the server here: it is - // a process-wide singleton shared by all map controllers and overlay - // modules; stopping it breaks tile loading for every other live map. - markerTileGroupId?.let { groupId -> - tileServer.unregister(groupId) - } - markerTileGroupId = null - markerTileRenderer = null - - (renderer as MapboxMarkerOverlayRenderer).coroutine.launch { - rasterLayerCallback?.onRasterLayerUpdate(null) - } - markerTileRasterLayerState = null - super.destroy() - } - - private suspend fun updateRasterLayerSource() { - val groupId = markerTileGroupId ?: return - val tileRenderer = markerTileRenderer ?: return - val oldState = markerTileRasterLayerState ?: return - cacheVersion = (cacheVersion + 1) and 0x7fffffff - tileRenderer.invalidate() - - val newState = - oldState.copy( - source = - RasterLayerSource.UrlTemplate( - template = "${tileServer.urlTemplate(groupId, tileRenderer.tileSize)}?v=$cacheVersion", - tileSize = tileRenderer.tileSize, - maxZoom = 22, - scheme = TileScheme.XYZ, - ), - id = oldState.id, - ) - markerTileRasterLayerState = newState - rasterLayerCallback?.onRasterLayerUpdate(newState) - } - - private suspend fun syncTiledOverlay() { - if (tiledMarkerIds.isEmpty()) { - removeTileOverlay() - return - } - if (!markerTiling.enabled) { - removeTileOverlay() - tiledMarkerIds.clear() - return - } - - getOrCreateTileRenderer() - updateRasterLayerSource() - } - - private fun getOrCreateTileRenderer(): MarkerTileRenderer { - markerTileRenderer?.let { return it } - - val groupId = UUID.randomUUID().toString() - markerTileGroupId = groupId - - val tileRenderer = - MarkerTileRenderer( - markerManager = markerManager, - tileSize = 256, - cacheSizeBytes = markerTiling.cacheSize, - debugTileOverlay = markerTiling.debugTileOverlay, - iconScaleCallback = markerTiling.iconScaleCallback, - ) - markerTileRenderer = tileRenderer - - tileServer.register(groupId, tileRenderer) - - markerTileRasterLayerState = - RasterLayerState( - id = "marker-tile-$groupId", - source = - RasterLayerSource.UrlTemplate( - template = tileServer.urlTemplate(groupId, tileRenderer.tileSize), - tileSize = tileRenderer.tileSize, - maxZoom = 22, - scheme = TileScheme.XYZ, - ), - opacity = 1.0f, - visible = true, - ) - - return tileRenderer - } - - private suspend fun removeTileOverlay() { - markerTileGroupId?.let { groupId -> - tileServer.unregister(groupId) - } - markerTileGroupId = null - markerTileRenderer = null - - rasterLayerCallback?.onRasterLayerUpdate(null) - markerTileRasterLayerState = null - } - - companion object { - fun create( - holder: MapboxMapViewHolder, - markerManager: MarkerManager, - markerLayer: MarkerLayer, - dragLayer: MarkerDragLayer, - markerTiling: MarkerTilingOptions = MarkerTilingOptions.Default, - ): MapboxMarkerController { - val renderer = - MapboxMarkerOverlayRenderer( - holder = holder, - markerManager = markerManager, - markerLayer = markerLayer, - dragLayer = dragLayer, - ) - val controller = - MapboxMarkerController( - renderer = renderer, - markerManager = markerManager, - markerTiling = markerTiling, - ) - controller.lastCameraPosition = holder.map.cameraState.toMapCameraPosition() - return controller - } - } -} +package com.mapconductor.mapbox.marker + +import com.mapconductor.core.controller.OnCameraChangeReceiverInterface +import com.mapconductor.core.map.MapCameraPosition +import com.mapconductor.core.marker.AbstractMarkerController +import com.mapconductor.core.marker.BitmapIcon +import com.mapconductor.core.marker.DefaultMarkerIcon +import com.mapconductor.core.marker.MarkerEntity +import com.mapconductor.core.marker.MarkerEntityInterface +import com.mapconductor.core.marker.MarkerIngestionEngine +import com.mapconductor.core.marker.MarkerManager +import com.mapconductor.core.marker.MarkerOverlayRendererInterface +import com.mapconductor.core.marker.MarkerState +import com.mapconductor.core.marker.MarkerTileRasterLayerCallback +import com.mapconductor.core.marker.MarkerTileRenderer +import com.mapconductor.core.marker.MarkerTilingOptions +import com.mapconductor.core.marker.MarkerViewportSwitch +import com.mapconductor.core.raster.RasterLayerSource +import com.mapconductor.core.raster.RasterLayerState +import com.mapconductor.core.raster.TileScheme +import com.mapconductor.core.tileserver.TileServerRegistry +import com.mapconductor.mapbox.MapboxActualMarker +import com.mapconductor.mapbox.MapboxMapViewHolder +import com.mapconductor.mapbox.toMapCameraPosition +import java.util.UUID +import kotlin.time.Duration.Companion.milliseconds +import kotlinx.coroutines.Job +import kotlinx.coroutines.delay +import kotlinx.coroutines.launch +import kotlinx.coroutines.sync.withPermit + +class MapboxMarkerController private constructor( + renderer: MapboxMarkerOverlayRenderer, + markerManager: MarkerManager, + private val markerTiling: MarkerTilingOptions, +) : AbstractMarkerController( + markerManager = markerManager, + renderer = renderer, + ), + OnCameraChangeReceiverInterface { + private val defaultMarkerIcon: BitmapIcon = DefaultMarkerIcon().toBitmapIcon() + private val tiledMarkerIds = LinkedHashSet() + private lateinit var lastCameraPosition: MapCameraPosition + + // Tile rendering via RasterLayer + private val tileServer = TileServerRegistry.get() + private var markerTileRenderer: MarkerTileRenderer? = null + private var markerTileGroupId: String? = null + private var markerTileRasterLayerState: RasterLayerState? = null + private var rasterLayerCallback: MarkerTileRasterLayerCallback? = null + private var cacheVersion: Int = 0 + + /** + * ビューポート内が少ないときだけタイルをやめてネイティブマーカーで描く切り替え器。 + * + * レンダラ/マネージャ/semaphore を共有するので、コントローラと同じ排他の下で動く。 + */ + private val viewportSwitch = + MarkerViewportSwitch( + markerManager = markerManager, + renderer = renderer, + defaultMarkerIcon = defaultMarkerIcon, + semaphore = semaphore, + policy = markerTiling.viewport, + setTileLayerVisible = ::setTileLayerVisible, + invalidateTiles = ::updateRasterLayerSource, + ) + + fun setRasterLayerCallback(callback: MarkerTileRasterLayerCallback?) { + rasterLayerCallback = callback + } + + override suspend fun add(data: List) { + // ingest はタイル担当 entity を marker = null で登録し直すので、先に昇格を戻す。 + // semaphore は再入不可なので withPermit の外で呼ぶこと。 + viewportSwitch.retract() + semaphore.withPermit { + val tilingEnabled = + markerTiling.enabled && data.size >= markerManager.minMarkerCount + val result = + MarkerIngestionEngine.ingest( + data = data, + markerManager = markerManager, + renderer = renderer, + defaultMarkerIcon = defaultMarkerIcon, + tilingEnabled = tilingEnabled, + tiledMarkerIds = tiledMarkerIds, + shouldTile = { state -> !state.draggable && state.getAnimation() == null }, + ) + + if (result.tiledDataChanged) { + syncTiledOverlay() + } else if (result.hasTiledMarkers) { + if (markerTileRenderer == null || markerTileRasterLayerState == null) { + syncTiledOverlay() + } + } else { + removeTileOverlay() + } + } + viewportSwitch.requestReapply() + } + + override suspend fun clear() { + viewportSwitch.destroy() + semaphore.withPermit { + val entities = markerManager.allEntities() + val toRemove = entities.filter { it.marker != null } + if (toRemove.isNotEmpty()) { + renderer.onRemove(toRemove) + } + markerManager.clear() + tiledMarkerIds.clear() + removeTileOverlay() + // onRemove はアイコンの参照数を減らすだけで、地図のソースは onPostProcess が + // manager の中身から描き直すまで変わらない。空にした後で呼ばないと、 + // clearOverlays() の後もマーカーが地図に残る。 + renderer.onPostProcess() + } + viewportSwitch.requestReapply() + } + + override suspend fun update(state: MarkerState) { + if (!markerManager.hasEntity(state.id)) return + + // 昇格中の 1 件なら先に取り下げる(下で marker = null 登録があるため)。 + // 昇格していないマーカー(ドラッグ中など)では何もしないので、ドラッグは素通りする。 + if (viewportSwitch.isPromoted(state.id)) viewportSwitch.release(state.id) + + val prevEntity = markerManager.getEntity(state.id) ?: return + val currentFinger = state.fingerPrint() + val prevFinger = prevEntity.fingerPrint + if (currentFinger == prevFinger) return + + semaphore.withPermit { + // 件数だけが要る。allEntities() は全件の写しを作るので、ラベルの付け替えのように + // 数十本が続けて変わると、そのたびに 14 万件を写すことになる。 + val tilingEnabled = + markerTiling.enabled && markerManager.getMemoryStats().entityCount >= markerManager.minMarkerCount + val wantsTiled = tilingEnabled && !state.draggable && state.getAnimation() == null + val wasTiled = tiledMarkerIds.contains(state.id) + + if (wantsTiled) { + if (!wasTiled) { + prevEntity.marker?.let { renderer.onRemove(listOf(prevEntity)) } + tiledMarkerIds.add(state.id) + } + markerManager.updateEntity( + MarkerEntity( + marker = null, + state = state, + visible = prevEntity.visible, + isRendered = true, + // tiling を立てないと MarkerTileRenderer の絞り込みから漏れ、 + // タイル昇格したのにタイルへ描かれないマーカーになる。 + tiling = true, + ), + ) + // タイル担当のままなら、ネイティブのマーカーには何も起きていない。onPostProcess は + // 全件を写してネイティブ分を描き直すので(14 万件で 1 回 50ms)、ネイティブから + // タイルへ移ったときだけ呼ぶ。 + if (!wasTiled) renderer.onPostProcess() + scheduleTileRefresh() + return@withPermit + } + + if (wasTiled) { + tiledMarkerIds.remove(state.id) + } + // ネイティブマーカーとして描くときだけ焼き込む。タイル担当なら要らない。 + val markerIcon = state.icon?.toBitmapIcon() ?: defaultMarkerIcon + + val params = + object : MarkerOverlayRendererInterface.ChangeParamsInterface { + override val current: MarkerEntityInterface = + MarkerEntity( + marker = prevEntity.marker, + state = state, + visible = prevEntity.visible, + isRendered = true, + ) + override val bitmapIcon: BitmapIcon = markerIcon + override val prev: MarkerEntityInterface = prevEntity + } + val markers = renderer.onChange(listOf(params)) + markers.firstOrNull()?.let { actual -> + markerManager.updateEntity( + MarkerEntity( + marker = actual, + state = state, + visible = prevEntity.visible, + isRendered = true, + ), + ) + if (prevFinger.animation != currentFinger.animation) { + state.getAnimation()?.let { renderer.onAnimate(markerManager.getEntity(state.id)!!) } + } + } + renderer.onPostProcess() + + if (tiledMarkerIds.isNotEmpty()) { + scheduleTileRefresh() + } else { + removeTileOverlay() + } + } + } + + private var tileRefreshJob: Job? = null + + /** + * 1 本ずつの変更からの描き直しを 1 回にまとめる。 + * + * ラベルの付け替えのように数十本が続けて変わると、1 本ごとにラスターソースを差し替え、 + * そのたびに地図が見えているタイルを全部取り直していた(MapLibre・Pixel 5a で 45 回、 + * 描き直したタイル 494 枚、最後のタイルまで 6 秒)。続けて来た変更は + * [TILE_REFRESH_COALESCE] の間に受けて、まとめて 1 回だけ差し替える。 + */ + private fun scheduleTileRefresh() { + if (tileRefreshJob?.isActive == true) return + tileRefreshJob = + (renderer as MapboxMarkerOverlayRenderer).coroutine.launch { + delay(TILE_REFRESH_COALESCE) + semaphore.withPermit { syncTiledOverlay() } + } + } + + override suspend fun onCameraChanged(mapCameraPosition: MapCameraPosition) { + // 判定と昇格は debounce したうえで切り替え器の中で走る(パン中は動かない)。 + viewportSwitch.onCameraChanged(mapCameraPosition) + lastCameraPosition = mapCameraPosition + } + + /** + * マーカータイルのラスターレイヤの表示だけを切り替える。 + * + * source(URL)には触らない。触るとタイルを取り直すことになり、切り替えのたびに + * タイルキャッシュを捨てるのと同じになる。 + */ + private suspend fun setTileLayerVisible(visible: Boolean) { + val current = markerTileRasterLayerState ?: return + if (current.visible == visible) return + val newState = current.copy(visible = visible) + markerTileRasterLayerState = newState + rasterLayerCallback?.onRasterLayerUpdate(newState) + } + + override fun destroy() { + viewportSwitch.destroy() + // 待っている描き直しが後から走ると、外したタイル経路をまた登録してしまう。 + tileRefreshJob?.cancel() + // Clean up tile server registration + // Unregister this map's route only. Never stop the server here: it is + // a process-wide singleton shared by all map controllers and overlay + // modules; stopping it breaks tile loading for every other live map. + markerTileGroupId?.let { groupId -> + tileServer.unregister(groupId) + } + markerTileGroupId = null + markerTileRenderer = null + + (renderer as MapboxMarkerOverlayRenderer).coroutine.launch { + rasterLayerCallback?.onRasterLayerUpdate(null) + } + markerTileRasterLayerState = null + super.destroy() + } + + private suspend fun updateRasterLayerSource() { + val groupId = markerTileGroupId ?: return + val tileRenderer = markerTileRenderer ?: return + val oldState = markerTileRasterLayerState ?: return + cacheVersion = (cacheVersion + 1) and 0x7fffffff + tileRenderer.invalidate() + + val newState = + oldState.copy( + source = + RasterLayerSource.UrlTemplate( + template = "${tileServer.urlTemplate(groupId, tileRenderer.tileSize)}?v=$cacheVersion", + tileSize = tileRenderer.tileSize, + maxZoom = 22, + scheme = TileScheme.XYZ, + ), + id = oldState.id, + ) + markerTileRasterLayerState = newState + rasterLayerCallback?.onRasterLayerUpdate(newState) + } + + private suspend fun syncTiledOverlay() { + if (tiledMarkerIds.isEmpty()) { + removeTileOverlay() + return + } + if (!markerTiling.enabled) { + removeTileOverlay() + tiledMarkerIds.clear() + return + } + + getOrCreateTileRenderer() + updateRasterLayerSource() + } + + private fun getOrCreateTileRenderer(): MarkerTileRenderer { + markerTileRenderer?.let { return it } + + val groupId = UUID.randomUUID().toString() + markerTileGroupId = groupId + + val tileRenderer = + MarkerTileRenderer( + markerManager = markerManager, + tileSize = 256, + cacheSizeBytes = markerTiling.cacheSize, + debugTileOverlay = markerTiling.debugTileOverlay, + iconScaleCallback = markerTiling.iconScaleCallback, + declutterPx = markerTiling.declutterPx, + ) + markerTileRenderer = tileRenderer + + tileServer.register(groupId, tileRenderer) + + markerTileRasterLayerState = + RasterLayerState( + id = "marker-tile-$groupId", + source = + RasterLayerSource.UrlTemplate( + template = tileServer.urlTemplate(groupId, tileRenderer.tileSize), + tileSize = tileRenderer.tileSize, + maxZoom = 22, + scheme = TileScheme.XYZ, + ), + opacity = 1.0f, + visible = true, + ) + + return tileRenderer + } + + private suspend fun removeTileOverlay() { + markerTileGroupId?.let { groupId -> + tileServer.unregister(groupId) + } + markerTileGroupId = null + markerTileRenderer = null + + rasterLayerCallback?.onRasterLayerUpdate(null) + markerTileRasterLayerState = null + } + + companion object { + /** 続けて来る変更を 1 回の描き直しにまとめる窓。1 フレーム分。 */ + private val TILE_REFRESH_COALESCE = 16.milliseconds + + fun create( + holder: MapboxMapViewHolder, + markerManager: MarkerManager, + markerLayer: MarkerLayer, + dragLayer: MarkerDragLayer, + markerTiling: MarkerTilingOptions = MarkerTilingOptions.Default, + ): MapboxMarkerController { + val renderer = + MapboxMarkerOverlayRenderer( + holder = holder, + markerManager = markerManager, + markerLayer = markerLayer, + dragLayer = dragLayer, + ) + val controller = + MapboxMarkerController( + renderer = renderer, + markerManager = markerManager, + markerTiling = markerTiling, + ) + controller.lastCameraPosition = holder.map.cameraState.toMapCameraPosition() + return controller + } + } +} diff --git a/src/main/java/com/mapconductor/mapbox/marker/MapboxMarkerOverlayRenderer.kt b/src/main/java/com/mapconductor/mapbox/marker/MapboxMarkerOverlayRenderer.kt index e057e2e..e73dde4 100644 --- a/src/main/java/com/mapconductor/mapbox/marker/MapboxMarkerOverlayRenderer.kt +++ b/src/main/java/com/mapconductor/mapbox/marker/MapboxMarkerOverlayRenderer.kt @@ -5,12 +5,12 @@ import com.google.gson.JsonObject import com.mapbox.geojson.Feature import com.mapbox.geojson.FeatureCollection import com.mapconductor.core.ResourceProvider -import com.mapconductor.core.calculateZIndex import com.mapconductor.core.features.GeoPoint import com.mapconductor.core.features.GeoPointInterface import com.mapconductor.core.marker.AbstractMarkerOverlayRenderer import com.mapconductor.core.marker.BitmapIcon import com.mapconductor.core.marker.DefaultMarkerIcon +import com.mapconductor.core.marker.MarkerDrawOrder import com.mapconductor.core.marker.MarkerEntityInterface import com.mapconductor.core.marker.MarkerIconInterface import com.mapconductor.core.marker.MarkerManager @@ -184,7 +184,7 @@ class MapboxMarkerOverlayRenderer( val props = (markerEntity.marker?.properties() ?: JsonObject()).deepCopy() props.addProperty( Prop.Z_INDEX, - markerEntity.state.zIndex ?: calculateZIndex(position), + MarkerDrawOrder.of(markerEntity.state.zIndex, position, drawOrderFrame), ) val feature = Feature.fromGeometry( @@ -246,7 +246,7 @@ class MapboxMarkerOverlayRenderer( add(Prop.ICON_ANCHOR, getDefaultIconOffsetProperty()) } addProperty(Prop.SCALE, it.state.icon?.scale ?: 1.0) - addProperty(Prop.Z_INDEX, it.state.zIndex ?: calculateZIndex(it.state.position)) + addProperty(Prop.Z_INDEX, drawOrderOf(it.state)) } Feature.fromGeometry(position, properties, featureId) } @@ -271,6 +271,17 @@ class MapboxMarkerOverlayRenderer( } } + // 地図が回ったら、各フィーチャーの並び順キー(symbol-sort-key)を付け直して流し直す。 + override suspend fun applyDrawOrder(entities: List>) { + val target = entities.filter { !it.tiling } + withContext(Dispatchers.Main) { + target.forEach { entity -> + entity.marker?.properties()?.addProperty(Prop.Z_INDEX, drawOrderOf(entity.state)) + } + markerLayer.draw(target) + } + } + override suspend fun onPostProcess() { val target = markerManager.allEntities().filter { !it.tiling } @@ -335,7 +346,7 @@ class MapboxMarkerOverlayRenderer( ) addProperty( Prop.Z_INDEX, - params.current.state.zIndex ?: calculateZIndex(params.current.state.position), + drawOrderOf(params.current.state), ) if (currFinger.icon == prevFinger.icon) { addProperty( 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..fff6cc5 --- /dev/null +++ b/src/main/java/com/mapconductor/mapbox/polygon/MapboxPolygonController.kt @@ -0,0 +1,24 @@ +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 + + override fun destroy() { + super.destroy() + // The outlines live in the outline renderer's manager, which core does not + // know about; a destroyed controller that is still referenced keeps none. + renderer.outline.polylineManager.clear() + } +} diff --git a/src/main/java/com/mapconductor/mapbox/polygon/MapboxPolygonOverlayRenderer.kt b/src/main/java/com/mapconductor/mapbox/polygon/MapboxPolygonOverlayRenderer.kt index 51a5bc8..0d8c449 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() { @@ -59,6 +62,16 @@ class MapboxPolygonOverlayRenderer( return prev.polygon } + // Polygons drawn as tiles (PolygonController.tiler) stay in the manager, for + // taps, with no features: their entity's polygon is null. + @Suppress("USELESS_CAST") private fun getAllPolygonEntities(): List> = - polygonManager.allEntities() + polygonManager.allEntities().filter { (it.polygon as MapboxActualPolygon?) != null } + + private fun getRemainingPolygonEntities( + removed: List>, + ): List> { + val removedIds = removed.mapTo(HashSet()) { it.state.id } + return getAllPolygonEntities().filter { it.state.id !in removedIds } + } } diff --git a/src/main/java/com/mapconductor/mapbox/polyline/MapboxPolylineOverlayRenderer.kt b/src/main/java/com/mapconductor/mapbox/polyline/MapboxPolylineOverlayRenderer.kt index d942083..b299838 100644 --- a/src/main/java/com/mapconductor/mapbox/polyline/MapboxPolylineOverlayRenderer.kt +++ b/src/main/java/com/mapconductor/mapbox/polyline/MapboxPolylineOverlayRenderer.kt @@ -65,6 +65,6 @@ class MapboxPolylineOverlayRenderer( private fun getAllPolylineEntities(): List> { // This would need access to the polyline manager // For now, we'll implement a simple workaround - return polylineManager.allEntities() + return polylineManager.allEntities().filter { (it.polyline as MapboxActualPolyline?) != null } } } diff --git a/src/main/java/com/mapconductor/mapbox/raster/MapboxRasterLayerController.kt b/src/main/java/com/mapconductor/mapbox/raster/MapboxRasterLayerController.kt index 7e65960..df62068 100644 --- a/src/main/java/com/mapconductor/mapbox/raster/MapboxRasterLayerController.kt +++ b/src/main/java/com/mapconductor/mapbox/raster/MapboxRasterLayerController.kt @@ -1,36 +1,10 @@ package com.mapconductor.mapbox.raster import com.mapconductor.core.raster.RasterLayerController -import com.mapconductor.core.raster.RasterLayerEntity import com.mapconductor.core.raster.RasterLayerManager import com.mapconductor.core.raster.RasterLayerManagerInterface -import com.mapconductor.core.raster.RasterLayerOverlayRendererInterface -import com.mapconductor.core.raster.RasterLayerState class MapboxRasterLayerController( rasterLayerManager: RasterLayerManagerInterface = RasterLayerManager(), renderer: MapboxRasterLayerOverlayRenderer, -) : RasterLayerController(rasterLayerManager, renderer) { - suspend fun reapplyStyle() { - val states = rasterLayerManager.allEntities().map { it.state } - if (states.isEmpty()) return - val addParams = - states.map { state -> - object : RasterLayerOverlayRendererInterface.AddParamsInterface { - override val state: RasterLayerState = state - } - } - val layers = renderer.onAdd(addParams) - layers.forEachIndexed { index, layer -> - layer?.let { - rasterLayerManager.registerEntity( - RasterLayerEntity( - layer = it, - state = states[index], - ), - ) - } - } - renderer.onPostProcess() - } -} +) : RasterLayerController(rasterLayerManager, renderer) diff --git a/src/main/java/com/mapconductor/mapbox/raster/MapboxRasterLayerOverlayRenderer.kt b/src/main/java/com/mapconductor/mapbox/raster/MapboxRasterLayerOverlayRenderer.kt index 850b20e..348f341 100644 --- a/src/main/java/com/mapconductor/mapbox/raster/MapboxRasterLayerOverlayRenderer.kt +++ b/src/main/java/com/mapconductor/mapbox/raster/MapboxRasterLayerOverlayRenderer.kt @@ -1,11 +1,12 @@ package com.mapconductor.mapbox.raster -import com.mapbox.maps.extension.style.layers.addLayer -import com.mapbox.maps.extension.style.layers.addLayerAbove -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.circle.CIRCLE_TILE_LAYER_ID +import com.mapconductor.core.controller.OverlayKind +import com.mapconductor.core.polygon.POLYGON_TILE_LAYER_ID +import com.mapconductor.core.polyline.POLYLINE_TILE_LAYER_ID import com.mapconductor.core.raster.RasterHeaderRuleSet import com.mapconductor.core.raster.RasterLayerEntityInterface import com.mapconductor.core.raster.RasterLayerOverlayRendererInterface @@ -13,6 +14,7 @@ import com.mapconductor.core.raster.RasterLayerSource import com.mapconductor.core.raster.RasterLayerState import com.mapconductor.core.raster.TileScheme import com.mapconductor.mapbox.MapboxMapViewHolder +import com.mapconductor.mapbox.MapboxOverlayStacking import android.util.Log import kotlinx.coroutines.CoroutineScope import kotlinx.coroutines.Dispatchers @@ -26,6 +28,12 @@ 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 + + private fun isPolylineTileRaster(state: RasterLayerState): Boolean = state.id == POLYLINE_TILE_LAYER_ID + + private fun isCircleTileRaster(state: RasterLayerState): Boolean = state.id == CIRCLE_TILE_LAYER_ID + /** Mapbox Android はタイル要求を書き換える公開 API を持たない。 */ override suspend fun onAdd( data: List, @@ -99,15 +107,7 @@ class MapboxRasterLayerOverlayRenderer( } catch (e: Exception) { Log.w("Mapbox", "Failed to add raster source: ${e.message}") } - try { - if (isMarkerTileRaster(state)) { - addLayerForMarkerTile(style, layer) - } else { - style.addLayer(layer) - } - } catch (e: Exception) { - Log.w("Mapbox", "Failed to add raster layer: ${e.message}") - } + addToStack(style, layer, state) return handle } @@ -116,10 +116,7 @@ class MapboxRasterLayerOverlayRenderer( state: RasterLayerState, ) { val style = holder.map.style ?: return - try { - style.removeStyleLayer(handle.layerId) - } catch (_: Exception) { - } + MapboxOverlayStacking.remove(holder.map, style, handle.layerId) val opacity = if (state.visible) { state.opacity.coerceIn(0.0f, 1.0f).toDouble() @@ -130,23 +127,13 @@ class MapboxRasterLayerOverlayRenderer( rasterLayer(handle.layerId, handle.sourceId) { rasterOpacity(opacity) } - try { - if (isMarkerTileRaster(state)) { - addLayerForMarkerTile(style, layer) - } else { - style.addLayer(layer) - } - } catch (_: Exception) { - } + addToStack(style, layer, state) } private fun removeLayer(entity: RasterLayerEntityInterface) { val style = holder.map.style ?: return val handle = entity.layer - try { - style.removeStyleLayer(handle.layerId) - } catch (_: Exception) { - } + MapboxOverlayStacking.remove(holder.map, style, handle.layerId) try { style.removeStyleSource(handle.sourceId) } catch (_: Exception) { @@ -182,43 +169,43 @@ class MapboxRasterLayerOverlayRenderer( } } - private fun addLayerForMarkerTile( + /** + * 帯の順([MapboxOverlayStacking])で足す。アプリのラスターはラスターの帯 + * (一番下、ベースマップのラベルの下)、マーカーのタイルはマーカーの帯の一番下、 + * ポリゴンのタイルはポリゴンの帯の一番下(画像になったポリゴンであって、地図の画像ではない)。 + */ + private fun addToStack( style: com.mapbox.maps.Style, layer: com.mapbox.maps.extension.style.layers.Layer, + state: RasterLayerState, ) { - // Insert the raster tiles below the marker symbol layer so they don't cover markers, - // but remain above vector overlays that are anchored below markers (polyline/circle/etc). - try { - style.addLayerBelow(layer, MARKERS_LAYER_ID) - return - } catch (_: Exception) { - } - - // Best-effort fallback: place above polylines if marker layer isn't present yet. - try { - style.addLayerAbove(layer, POLYLINE_LAYER_ID) - return - } catch (_: Exception) { - } - - style.addLayer(layer) + val key = + if (isMarkerTileRaster(state)) { + MapboxOverlayStacking.MARKER_TILE + } else if (isPolygonTileRaster(state)) { + MapboxOverlayStacking.POLYGON_TILE + } else if (isCircleTileRaster(state)) { + MapboxOverlayStacking.CIRCLE_TILE + } else if (isPolylineTileRaster(state)) { + MapboxOverlayStacking.POLYLINE_TILE + } else { + MapboxOverlayStacking.key(OverlayKind.RasterLayer, state.zIndex) + } + MapboxOverlayStacking.add(holder.map, style, layer, key) } private fun rebuildNonMarkerRasterLayers(style: com.mapbox.maps.Style) { val ordered = stateById.values .asSequence() - .filter { !isMarkerTileRaster(it) } + .filter { !isMarkerTileRaster(it) && !isPolygonTileRaster(it) && !isPolylineTileRaster(it) && !isCircleTileRaster(it) } .sortedBy { it.zIndex } .mapNotNull { state -> handleById[state.id]?.let { handle -> state to handle } } .toList() // Remove and re-add non-marker raster layers in zIndex order to ensure deterministic stacking. ordered.forEach { (_, handle) -> - try { - style.removeStyleLayer(handle.layerId) - } catch (_: Exception) { - } + MapboxOverlayStacking.remove(holder.map, style, handle.layerId) } ordered.forEach { (state, handle) -> @@ -232,17 +219,12 @@ class MapboxRasterLayerOverlayRenderer( rasterLayer(handle.layerId, handle.sourceId) { rasterOpacity(opacity) } - try { - style.addLayer(layer) - } catch (_: Exception) { - } + addToStack(style, layer, state) } } private companion object { 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" } }