diff --git a/api/android-for-mapbox.api b/api/android-for-mapbox.api index 496c34f..67b49fb 100644 --- a/api/android-for-mapbox.api +++ b/api/android-for-mapbox.api @@ -158,6 +158,8 @@ 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); diff --git a/src/main/java/com/mapconductor/mapbox/MapboxMapView.kt b/src/main/java/com/mapconductor/mapbox/MapboxMapView.kt index 2b54385..0f60a3b 100644 --- a/src/main/java/com/mapconductor/mapbox/MapboxMapView.kt +++ b/src/main/java/com/mapconductor/mapbox/MapboxMapView.kt @@ -23,6 +23,7 @@ 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 @@ -107,7 +108,8 @@ fun MapboxMapView( modifier = modifier, viewProvider = { val cameraOptions = - state.cameraPosition.toCameraOptions(context.resources.displayMetrics.density) + // パディングはネイティブに渡さない。接続時の moveCamera が中心のずらしで適用する。 + state.cameraPosition.copy(paddings = MapPaddings.Zeros).toCameraOptions(context.resources.displayMetrics.density) val styleUri = state.mapDesignType.getValue() diff --git a/src/main/java/com/mapconductor/mapbox/MapboxMapViewController.kt b/src/main/java/com/mapconductor/mapbox/MapboxMapViewController.kt index aedd292..2b044f4 100644 --- a/src/main/java/com/mapconductor/mapbox/MapboxMapViewController.kt +++ b/src/main/java/com/mapconductor/mapbox/MapboxMapViewController.kt @@ -1,505 +1,510 @@ -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.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 - - 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) - } -} +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 157b233..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,11 +31,63 @@ internal fun MapboxMapViewController.getMapCameraPosition(): MapCameraPosition? } internal fun MapboxMapViewController.readLogicalCameraPosition(): MapCameraPosition = - MapboxCameraStateSnapshot( - cameraState = holder.map.cameraState, - logicalTiltHint = lastLogicalCameraPosition?.tilt, - density = density(), - ).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 @@ -46,10 +101,13 @@ internal suspend fun MapboxMapViewController.getMapCameraPositionAsync(): MapCam } internal fun MapboxMapViewController.handleMoveCamera(position: MapCameraPosition) { - val cameraOptions = position.toCameraOptions(density()) lastLogicalCameraPosition = position mainCoroutine.launch { - holder.map.setCamera(cameraOptions) + if (needsLayoutFor(position)) { + afterFirstLayout { handleMoveCamera(position) } + return@launch + } + holder.map.setCamera(toNativeCamera(position).toCameraOptions(density())) } } @@ -57,7 +115,11 @@ internal fun MapboxMapViewController.handleAnimateCamera( position: MapCameraPosition, duration: Long, ) { - val targetCamera = position.toCameraOptions(density()) + if (needsLayoutFor(position)) { + afterFirstLayout { handleAnimateCamera(position, duration) } + return + } + val targetCamera = toNativeCamera(position).toCameraOptions(density()) lastLogicalCameraPosition = position val animationOptions =