From ae2fac85ac5b5e3c2c90f94488a7190097ac9e86 Mon Sep 17 00:00:00 2001 From: Masashi Katsumata Date: Sat, 10 Oct 2026 14:18:52 -0700 Subject: [PATCH 1/2] feat: integrate vector style support and pending map fixes --- api/android-for-mapbox.api | 2 +- gradle/libs.versions.toml | 2 +- .../com/mapconductor/mapbox/MapboxMapView.kt | 38 + .../mapbox/MapboxMapViewController.kt | 877 +++++++++--------- .../mapbox/MapboxStyleMutationTarget.kt | 142 +++ 5 files changed, 638 insertions(+), 423 deletions(-) create mode 100644 src/main/java/com/mapconductor/mapbox/MapboxStyleMutationTarget.kt diff --git a/api/android-for-mapbox.api b/api/android-for-mapbox.api index 35c9a95..a3bce19 100644 --- a/api/android-for-mapbox.api +++ b/api/android-for-mapbox.api @@ -189,7 +189,7 @@ public final class com.mapconductor.mapbox.MapboxMapViewHolder implements com.ma 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.circle.MapboxCircleController getCircleController(com.mapconductor.mapbox.MapboxMapViewHolder); diff --git a/gradle/libs.versions.toml b/gradle/libs.versions.toml index beb199c..ce6f177 100644 --- a/gradle/libs.versions.toml +++ b/gradle/libs.versions.toml @@ -8,7 +8,7 @@ kotlin = "2.0.21" composeBom = "2025.02.00" lifecycleRuntimeKtx = "2.8.7" -mapboxAndroid = "11.24.3" +mapboxAndroid = "11.32.0" activityCompose = "1.9.3" ktLint = "14.2.0" diff --git a/src/main/java/com/mapconductor/mapbox/MapboxMapView.kt b/src/main/java/com/mapconductor/mapbox/MapboxMapView.kt index 4ef7cee..f2a0f8d 100644 --- a/src/main/java/com/mapconductor/mapbox/MapboxMapView.kt +++ b/src/main/java/com/mapconductor/mapbox/MapboxMapView.kt @@ -21,9 +21,13 @@ 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.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 @@ -71,6 +75,15 @@ fun MapboxMapView( 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() } @@ -146,6 +159,29 @@ fun MapboxMapView( 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 @@ -168,6 +204,8 @@ fun MapboxMapView( // 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 -> diff --git a/src/main/java/com/mapconductor/mapbox/MapboxMapViewController.kt b/src/main/java/com/mapconductor/mapbox/MapboxMapViewController.kt index 3542951..51a8118 100644 --- a/src/main/java/com/mapconductor/mapbox/MapboxMapViewController.kt +++ b/src/main/java/com/mapconductor/mapbox/MapboxMapViewController.kt @@ -1,421 +1,456 @@ -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 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) + } + + // 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() + 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 + + /** + * 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/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}") + } +} From 51f8deba81c53e107ea3b9e88084fea1bbd731ed Mon Sep 17 00:00:00 2001 From: Masashi Katsumata Date: Sat, 24 Oct 2026 03:50:11 -0700 Subject: [PATCH 2/2] Keep each file's line endings as they are on develop The checkpoint commit rewrote whole files between LF and CRLF, which buried its real changes under line-ending noise. Content is unchanged. --- .../mapbox/MapboxMapViewController.kt | 912 +++++++++--------- 1 file changed, 456 insertions(+), 456 deletions(-) diff --git a/src/main/java/com/mapconductor/mapbox/MapboxMapViewController.kt b/src/main/java/com/mapconductor/mapbox/MapboxMapViewController.kt index 51a8118..e10bc14 100644 --- a/src/main/java/com/mapconductor/mapbox/MapboxMapViewController.kt +++ b/src/main/java/com/mapconductor/mapbox/MapboxMapViewController.kt @@ -1,456 +1,456 @@ -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) - } - - // 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() - 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 - - /** - * 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) - } -} +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) + } + + // 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() + 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 + + /** + * 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) + } +}