From 3eb7d6f79a578978dfc4731ed13679ba3a090474 Mon Sep 17 00:00:00 2001 From: Masashi Katsumata Date: Wed, 11 Nov 2026 22:03:19 -0800 Subject: [PATCH 1/2] Read the tilted ArcGIS camera back at the screen center 3D: Camera holds the eye position. Under tilt the eye sits behind the point being looked at, so returning it as MapCameraPosition.position put the read-back 3 to 6 km short of the target at tilt 30 to 60. The read now returns the point at the screen center (screen-center unproject, falling back to eye + altitude * tan(pitch) along the heading). 2D: the read-back unprojected the inner MapView's size, which is twice the frame when the tilt emulation is on, so the center landed far away and the corners failed. It now uses the outer view's size. A tilt-only change also never reached the state because the Viewpoint did not change; it is emitted after the move now. The Viewpoint center was drawn half an attribution bar above the view center (25.5px on TB520FU), which read back 45m off; the center is now shifted so the position lands at the view center. --- .../mapconductor/arcgis/ArcGISCameraTarget.kt | 32 ++++++++++++++ .../arcgis/ArcGISMapView2DController.kt | 44 ++++++++++++++++++- .../arcgis/ArcGISMapView2DControllerCamera.kt | 10 ++++- .../arcgis/ArcGISMapViewControllerCamera.kt | 9 +++- .../mapconductor/arcgis/MapCameraPosition.kt | 12 ++--- 5 files changed, 97 insertions(+), 10 deletions(-) create mode 100644 src/main/java/com/mapconductor/arcgis/ArcGISCameraTarget.kt diff --git a/src/main/java/com/mapconductor/arcgis/ArcGISCameraTarget.kt b/src/main/java/com/mapconductor/arcgis/ArcGISCameraTarget.kt new file mode 100644 index 0000000..63fb9b0 --- /dev/null +++ b/src/main/java/com/mapconductor/arcgis/ArcGISCameraTarget.kt @@ -0,0 +1,32 @@ +package com.mapconductor.arcgis + +import com.arcgismaps.mapping.view.Camera +import com.mapconductor.core.InternalMapConductorApi +import com.mapconductor.core.features.GeoPoint +import kotlin.math.tan + +// 公開 API のスナップショットから外すため、注釈付きの関数だけをこのファイルに置く +// (注釈はファイル単位のクラスごと除外されるので、他の公開関数と同居させない)。 + +/** + * カメラが見ている地上の点(画面中央)。 + * + * ArcGIS の [Camera] が持つのは**目の位置**で、傾けると見ている点は目の真下から向きの方へ + * `高さ × tan(pitch)` 先になる(カメラを置くときの [calculateCameraForOrbitParameters] の逆)。 + * 目の位置をそのまま MapCameraPosition.position にすると、tilt 60° では数 km 手前の点を + * 返してしまう。地形の高さは見ないので、起伏のある所では画面中央の逆投影の方が正確。 + */ +@InternalMapConductorApi +internal fun Camera.lookAtTarget(): GeoPoint { + val altitude = location.z ?: 0.0 + val pitchDeg = pitch.coerceIn(MIN_ANGLE, ARCGIS_MAX_PITCH - 1.0) + if (pitchDeg == 0.0 || altitude <= 0.0) { + return GeoPoint.fromLongLat(longitude = location.x, latitude = location.y) + } + return calculateDestinationPoint( + lat = location.y, + lon = location.x, + bearing = heading, + distance = altitude * tan(pitchDeg.toRadians()), + ) +} diff --git a/src/main/java/com/mapconductor/arcgis/ArcGISMapView2DController.kt b/src/main/java/com/mapconductor/arcgis/ArcGISMapView2DController.kt index 6720cef..275fd89 100644 --- a/src/main/java/com/mapconductor/arcgis/ArcGISMapView2DController.kt +++ b/src/main/java/com/mapconductor/arcgis/ArcGISMapView2DController.kt @@ -5,7 +5,9 @@ import com.mapconductor.core.circle.CircleTiler import com.mapconductor.core.circle.circleTileRasterLayerState import com.arcgismaps.LoadStatus import com.arcgismaps.geometry.SpatialReference +import com.arcgismaps.geometry.Point import com.arcgismaps.mapping.Viewpoint +import com.arcgismaps.mapping.ViewpointType import com.arcgismaps.mapping.view.GraphicsOverlay import com.mapconductor.arcgis.circle.ArcGISCircleOverlayController import com.mapconductor.arcgis.groundimage.ArcGISGroundImageController @@ -46,7 +48,10 @@ import com.mapconductor.core.polyline.PolylineState import com.mapconductor.core.polyline.PolylineTiler import com.mapconductor.core.polyline.polylineTileRasterLayerState import com.mapconductor.core.projection.Earth +import com.mapconductor.core.spherical.Spherical import com.mapconductor.core.raster.RasterLayerState +import kotlin.math.abs +import kotlin.math.cos import kotlin.math.ln import kotlin.math.pow import android.util.Log @@ -316,7 +321,8 @@ class ArcGISMapView2DController( * 指定位置がそのまま画面中心に来る。 */ internal fun toViewpoint(position: MapCameraPosition): Viewpoint { - val (center, zoom) = ArcGIS2DTiltEmulation.shiftedCamera(position) + val (shifted, zoom) = ArcGIS2DTiltEmulation.shiftedCamera(position) + val center = centeredOnView(shifted, zoom, position.bearing) val point = GeoPoint.from(center).toPoint(SpatialReference.wgs84()) return Viewpoint( center = point, @@ -330,6 +336,42 @@ class ArcGISMapView2DController( ) } + /** + * ArcGIS は Viewpoint の中心をビューの中央ではなく、帰属表示バーを除いた領域の中央に描く + * (TB520FU では 25.5px 上)。そのままでは position が画面中央に来ず、読み戻すと + * 45m ほどずれていた。ずれを今の地図から測り、その分だけ中心を画面の上方向 + * (カメラの向き)へずらして、position が画面中央に来るようにする。 + */ + private fun centeredOnView( + center: GeoPointInterface, + zoom: Double, + bearing: Double, + ): GeoPointInterface { + val offsetPx = viewpointCenterOffsetPx() ?: return center + if (abs(offsetPx) < 0.5) return center + val density = holder.mapView.resources.displayMetrics.density + val metersPerDp = + WEB_MERCATOR_CIRCUMFERENCE_METERS / (TILE_SIZE * 2.0.pow(zoom)) * + cos(Math.toRadians(center.latitude)) + return Spherical.computeOffset( + center, + -offsetPx / density * metersPerDp, + CameraBearing.toNativeHeading(bearing), + ) + } + + /** Viewpoint の中心が内側の MapView の中央から縦に何 px ずれて描かれているか。上が負。 */ + private fun viewpointCenterOffsetPx(): Double? = + runCatching { + val height = holder.map.height + if (height <= 0) return null + val center = + holder.map.getCurrentViewpoint(ViewpointType.CenterAndScale)?.targetGeometry as? Point + ?: return null + val screen = holder.map.locationToScreen(center) ?: return null + screen.y - height / 2.0 + }.getOrNull() + @Deprecated("Use MarkerState.onDragStart instead.") override fun setOnMarkerDragStart(listener: OnMarkerEventHandler?) { markerDragStartListener = listener diff --git a/src/main/java/com/mapconductor/arcgis/ArcGISMapView2DControllerCamera.kt b/src/main/java/com/mapconductor/arcgis/ArcGISMapView2DControllerCamera.kt index 72de329..71cf0a6 100644 --- a/src/main/java/com/mapconductor/arcgis/ArcGISMapView2DControllerCamera.kt +++ b/src/main/java/com/mapconductor/arcgis/ArcGISMapView2DControllerCamera.kt @@ -58,8 +58,11 @@ internal fun ArcGISMapView2DController.scheduleCameraMoveEndCallback() { } internal suspend fun ArcGISMapView2DController.getMapCameraPosition(): MapCameraPosition? { - val mapWidth = holder.map.width.toFloat() - 1.0f - val mapHeight = holder.map.height.toFloat() - 1.0f + // holder の画面座標は外側のビュー(WrapMapView)のもの。傾けているときの内側の MapView は + // 2 倍の大きさで回っているので、その幅・高さで隅を取ると画面の外を逆投影してしまい、 + // 中心は数 km ずれ、隅は解けずにカメラごと読めなくなっていた。 + val mapWidth = holder.mapView.width.toFloat() - 1.0f + val mapHeight = holder.mapView.height.toFloat() - 1.0f val nearLeft = holder.fromScreenOffset(Offset(1.0f, mapHeight)) ?: return null val nearRight = holder.fromScreenOffsetSync(Offset(mapWidth, mapHeight)) ?: return null val farLeft = holder.fromScreenOffsetSync(Offset(1.0f, 1.0f)) ?: return null @@ -118,6 +121,9 @@ internal fun ArcGISMapView2DController.handleMoveCamera(position: MapCameraPosit mainCoroutine.launch { if (!holder.mapView.isAttachedToWindow) return@launch holder.map.setViewpoint(viewpoint) + // tilt だけが変わったときは Viewpoint が同じなので ArcGIS は変化を通知しない。 + // そのままだと state のカメラが古い tilt のまま残るので、ここで配る。 + if (tiltChanged) onViewpointChange() } Unit } diff --git a/src/main/java/com/mapconductor/arcgis/ArcGISMapViewControllerCamera.kt b/src/main/java/com/mapconductor/arcgis/ArcGISMapViewControllerCamera.kt index 9df02f5..07598c7 100644 --- a/src/main/java/com/mapconductor/arcgis/ArcGISMapViewControllerCamera.kt +++ b/src/main/java/com/mapconductor/arcgis/ArcGISMapViewControllerCamera.kt @@ -118,8 +118,13 @@ internal suspend fun ArcGISMapViewController.getMapCameraPosition(): MapCameraPo ) val arcCamera = holder.map.getCurrentViewpointCamera() ?: return null - val lat = arcCamera.location.y - val lon = arcCamera.location.x + // position は見ている点(画面中央)。目の位置(arcCamera.location)ではない。 + // 地形を考慮するため画面中央の逆投影を優先し、取れなければ幾何で求める。 + val target = + holder.fromScreenOffsetSync(Offset(holder.map.width / 2f, holder.map.height / 2f)) + ?: arcCamera.lookAtTarget() + val lat = target.latitude + val lon = target.longitude val alt = arcCamera.location.z ?: 0.0 val tilt = arcCamera.pitch val bearing = CameraBearing.bearingFromNativeHeading(arcCamera.heading) diff --git a/src/main/java/com/mapconductor/arcgis/MapCameraPosition.kt b/src/main/java/com/mapconductor/arcgis/MapCameraPosition.kt index 029f1ca..d3a522c 100644 --- a/src/main/java/com/mapconductor/arcgis/MapCameraPosition.kt +++ b/src/main/java/com/mapconductor/arcgis/MapCameraPosition.kt @@ -147,19 +147,20 @@ fun Camera.withZoomLevel(zoomLevel: Double): Camera { ) } -fun Camera.toMapCameraPosition(): MapCameraPosition = - MapCameraPosition( +fun Camera.toMapCameraPosition(): MapCameraPosition { + val target = lookAtTarget() + return MapCameraPosition( position = GeoPoint.fromLongLat( - longitude = this.location.x, - latitude = this.location.y, + longitude = target.longitude, + latitude = target.latitude, altitude = this.location.z ?: 0.0, ), zoom = converter .altitudeToZoomLevel( altitude = this.location.z ?: 0.0, - latitude = this.location.y, + latitude = target.latitude, tilt = this.pitch, ), bearing = CameraBearing.bearingFromNativeHeading(this.heading), @@ -167,3 +168,4 @@ fun Camera.toMapCameraPosition(): MapCameraPosition = paddings = MapPaddings.Zeros, visibleRegion = null, ) +} From 1d8f92b324150e98a6ad795ba124e2bb062dc693 Mon Sep 17 00:00:00 2001 From: Masashi Katsumata Date: Wed, 11 Nov 2026 22:29:04 -0800 Subject: [PATCH 2/2] Apply camera paddings and report fov on ArcGIS 3D: SceneView has no insets, so paddings are emulated by moving the look-at point and undone on read. fov is read from SceneView: its fieldOfView is horizontal (29.3 portrait, 68.0 landscape on TB520FU), converted to vertical (45 in both, matching the measured 45.2-45.4). 2D: setViewInsets is not used because under the tilt emulation the inner MapView is twice the frame and rotated, so insets do not match on-screen padding. Paddings are emulated on the outer view with fov 0 (the tilt is drawn without perspective). --- api/android-for-arcgis.api | 6 +++ .../arcgis/ArcGISMapView2DController.kt | 12 +++++ .../arcgis/ArcGISMapView2DControllerCamera.kt | 47 +++++++++++++++---- .../arcgis/ArcGISMapViewController.kt | 10 ++++ .../arcgis/ArcGISMapViewControllerCamera.kt | 45 ++++++++++++++++-- 5 files changed, 107 insertions(+), 13 deletions(-) diff --git a/api/android-for-arcgis.api b/api/android-for-arcgis.api index dc7901e..484ba3c 100644 --- a/api/android-for-arcgis.api +++ b/api/android-for-arcgis.api @@ -170,12 +170,15 @@ public final class com.mapconductor.arcgis.ArcGISMapView2DController extends com public final class com.mapconductor.arcgis.ArcGISMapView2DController$Companion { } public final class com.mapconductor.arcgis.ArcGISMapView2DControllerCameraKt { + public static final double ARCGIS_2D_VERTICAL_FOV; public static final java.lang.Object onViewpointChange(com.mapconductor.arcgis.ArcGISMapView2DController, kotlin.coroutines.Continuation); public static final java.lang.Object invokeCameraMoveStartCallback(com.mapconductor.arcgis.ArcGISMapView2DController, kotlin.coroutines.Continuation); public static final java.lang.Object invokeCameraMoveCallback(com.mapconductor.arcgis.ArcGISMapView2DController, kotlin.coroutines.Continuation); public static final java.lang.Object invokeCameraMoveEndCallback(com.mapconductor.arcgis.ArcGISMapView2DController, kotlin.coroutines.Continuation); public static final void scheduleCameraMoveEndCallback(com.mapconductor.arcgis.ArcGISMapView2DController); public static final java.lang.Object getMapCameraPosition(com.mapconductor.arcgis.ArcGISMapView2DController, kotlin.coroutines.Continuation); + public static final double viewportHeightDp(com.mapconductor.arcgis.ArcGISMapView2DController); + public static final com.mapconductor.core.map.MapCameraPosition toNativeCamera(com.mapconductor.arcgis.ArcGISMapView2DController, com.mapconductor.core.map.MapCameraPosition); public static final void handleMoveCamera(com.mapconductor.arcgis.ArcGISMapView2DController, com.mapconductor.core.map.MapCameraPosition); public static final void handleAnimateCamera(com.mapconductor.arcgis.ArcGISMapView2DController, com.mapconductor.core.map.MapCameraPosition, long); public static final void handleFitBounds(com.mapconductor.arcgis.ArcGISMapView2DController, com.mapconductor.core.features.GeoRectBounds, int); @@ -243,6 +246,9 @@ public final class com.mapconductor.arcgis.ArcGISMapViewController extends com.m } public final class com.mapconductor.arcgis.ArcGISMapViewControllerCameraKt { public static final com.mapconductor.core.map.MapCameraPosition getFastMapCameraPosition(com.mapconductor.arcgis.ArcGISMapViewController); + public static final java.lang.Double verticalFov(com.mapconductor.arcgis.ArcGISMapViewController); + public static final double viewportHeightDp(com.mapconductor.arcgis.ArcGISMapViewController); + public static final com.mapconductor.core.map.MapCameraPosition toNativeCamera(com.mapconductor.arcgis.ArcGISMapViewController, com.mapconductor.core.map.MapCameraPosition); public static final void invokeCameraMoveStartCallback(com.mapconductor.arcgis.ArcGISMapViewController); public static final void invokeCameraMoveCallback(com.mapconductor.arcgis.ArcGISMapViewController); public static final java.lang.Object invokeCameraMoveEndCallback(com.mapconductor.arcgis.ArcGISMapViewController, kotlin.coroutines.Continuation); diff --git a/src/main/java/com/mapconductor/arcgis/ArcGISMapView2DController.kt b/src/main/java/com/mapconductor/arcgis/ArcGISMapView2DController.kt index 275fd89..ee8744c 100644 --- a/src/main/java/com/mapconductor/arcgis/ArcGISMapView2DController.kt +++ b/src/main/java/com/mapconductor/arcgis/ArcGISMapView2DController.kt @@ -28,6 +28,8 @@ import com.mapconductor.core.groundimage.OnGroundImageEventHandler import com.mapconductor.core.map.CameraBearing import com.mapconductor.core.map.CameraRestriction import com.mapconductor.core.map.MapCameraPosition +import com.mapconductor.core.map.MapPaddings +import com.mapconductor.core.map.MapPaddingsInterface import com.mapconductor.core.map.MapGesture import com.mapconductor.core.map.MapUISettings import com.mapconductor.core.map.MapUISettingsDiagnostics @@ -86,6 +88,16 @@ class ArcGISMapView2DController( // カメラ状態の読み戻し時に元の tilt・位置・ズームを復元するヒントとして保持する // (android-for-tomtom / ios-for-arcgis と同方針)。 internal var lastLogicalCameraPosition: MapCameraPosition? = null + + /** + * 今カメラに付いているパディング(dp)。 + * + * `MapView.setViewInsets` は使わない。傾きのエミュレーションでは内側の MapView が + * 2 倍の大きさで回っているので、インセットが画面上のパディングと一致しないため。 + * 代わりに中心をずらして再現し([CameraPaddingEmulation])、読むときに元へ戻すために + * ここで覚えておく。 + */ + internal var cameraPaddings: MapPaddingsInterface = MapPaddings.Zeros internal val cameraMoveEndDebounceMs = 180L internal var mapDesignType: ArcGISDesignTypeInterface = ArcGISDesign.Streets internal var mapDesignTypeChangeListener: ArcGISDesignTypeChangeHandler? = null diff --git a/src/main/java/com/mapconductor/arcgis/ArcGISMapView2DControllerCamera.kt b/src/main/java/com/mapconductor/arcgis/ArcGISMapView2DControllerCamera.kt index 71cf0a6..3b1714c 100644 --- a/src/main/java/com/mapconductor/arcgis/ArcGISMapView2DControllerCamera.kt +++ b/src/main/java/com/mapconductor/arcgis/ArcGISMapView2DControllerCamera.kt @@ -3,6 +3,7 @@ package com.mapconductor.arcgis import androidx.compose.ui.geometry.Offset import com.mapconductor.core.features.GeoRectBounds import com.mapconductor.core.map.CameraBearing +import com.mapconductor.core.map.CameraPaddingEmulation import com.mapconductor.core.map.MapCameraPosition import com.mapconductor.core.map.MapPaddings import com.mapconductor.core.map.VisibleRegion @@ -96,13 +97,41 @@ internal suspend fun ArcGISMapView2DController.getMapCameraPosition(): MapCamera logicalTilt = logicalTilt, ) - return MapCameraPosition( - position = restoredCenter, - zoom = restoredZoom, - bearing = bearing, - tilt = logicalTilt, - paddings = MapPaddings.Zeros, - visibleRegion = visibleRegion, + return CameraPaddingEmulation.fromNativeCamera( + MapCameraPosition( + position = restoredCenter, + zoom = restoredZoom, + bearing = bearing, + tilt = logicalTilt, + paddings = MapPaddings.Zeros, + visibleRegion = visibleRegion, + ), + cameraPaddings, + ARCGIS_2D_VERTICAL_FOV, + viewportHeightDp(), + ) +} + +/** + * 2D の MapView のカメラの縦方向の視野角(度)。2D はカメラを傾けられず、傾きはビューを + * X 軸まわりに回して見せている([WrapMapView]、遠近はほぼ付けない)。TB520FU で、傾けた + * ときの縦の間隔は cos(tilt) 倍と 0.3% 以内で一致した。遠近なしの傾きとして 0 と表す。 + */ +internal const val ARCGIS_2D_VERTICAL_FOV = 0.0 + +/** 地図の高さ(dp)。外側のビュー(画面に見えている大きさ)で測る。 */ +internal fun ArcGISMapView2DController.viewportHeightDp(): Double { + val density = holder.mapView.resources.displayMetrics.density + return holder.mapView.height / density.toDouble() +} + +/** パディングを中心のずらしに置き換えたカメラ。paddings が null なら今のパディングを使い続ける。 */ +internal fun ArcGISMapView2DController.toNativeCamera(position: MapCameraPosition): MapCameraPosition { + position.paddings?.let { cameraPaddings = it } + return CameraPaddingEmulation.toNativeCamera( + position.copy(paddings = cameraPaddings), + ARCGIS_2D_VERTICAL_FOV, + viewportHeightDp(), ) } @@ -116,7 +145,7 @@ internal fun ArcGISMapView2DController.handleMoveCamera(position: MapCameraPosit // ビューを傾けると地図の中身は縦に潰れる。マーカーだけは立って見えるよう、 // アイコンを先に縦へ引き伸ばす(他のオーバーレイは寝たままでよい)。 if (tiltChanged) markerController.refreshVerticalStretch() - val viewpoint = toViewpoint(position) + val viewpoint = toViewpoint(toNativeCamera(position)) val applyViewpoint = { mainCoroutine.launch { if (!holder.mapView.isAttachedToWindow) return@launch @@ -138,7 +167,7 @@ internal fun ArcGISMapView2DController.handleAnimateCamera( val tiltChanged = holder.mapView.visualTilt != position.tilt holder.mapView.visualTilt = position.tilt if (tiltChanged) markerController.refreshVerticalStretch() - val viewpoint = toViewpoint(position) + val viewpoint = toViewpoint(toNativeCamera(position)) val animate = { defaultCoroutine.launch { invokeCameraMoveStartCallback() diff --git a/src/main/java/com/mapconductor/arcgis/ArcGISMapViewController.kt b/src/main/java/com/mapconductor/arcgis/ArcGISMapViewController.kt index a341246..60dbada 100644 --- a/src/main/java/com/mapconductor/arcgis/ArcGISMapViewController.kt +++ b/src/main/java/com/mapconductor/arcgis/ArcGISMapViewController.kt @@ -20,6 +20,8 @@ import com.mapconductor.core.features.GeoRectBounds import com.mapconductor.core.groundimage.GroundImageState import com.mapconductor.core.groundimage.OnGroundImageEventHandler import com.mapconductor.core.map.MapCameraPosition +import com.mapconductor.core.map.MapPaddings +import com.mapconductor.core.map.MapPaddingsInterface import com.mapconductor.core.map.MapGesture import com.mapconductor.core.map.MapUISettings import com.mapconductor.core.map.MapUISettingsDiagnostics @@ -67,6 +69,14 @@ class ArcGISMapViewController( // ArcGIS updates the viewpoint asynchronously; firing "move end" immediately after setViewpointCamera() // can read a stale camera and cause feedback loops in camera sync scenarios. internal var cameraMoveEndJob: Job? = null + + /** + * 今カメラに付いているパディング(dp)。 + * + * 3D の SceneView にはパディング(インセット)の口が無いので、カメラが見る点を + * ずらして再現し([CameraPaddingEmulation])、読むときに元へ戻すためにここで覚えておく。 + */ + internal var cameraPaddings: MapPaddingsInterface = MapPaddings.Zeros internal val cameraMoveEndDebounceMs = 180L /** diff --git a/src/main/java/com/mapconductor/arcgis/ArcGISMapViewControllerCamera.kt b/src/main/java/com/mapconductor/arcgis/ArcGISMapViewControllerCamera.kt index 07598c7..55f7150 100644 --- a/src/main/java/com/mapconductor/arcgis/ArcGISMapViewControllerCamera.kt +++ b/src/main/java/com/mapconductor/arcgis/ArcGISMapViewControllerCamera.kt @@ -7,9 +7,12 @@ import com.mapconductor.arcgis.zoom.ZoomAltitudeConverter import com.mapconductor.core.features.GeoPoint import com.mapconductor.core.features.GeoRectBounds import com.mapconductor.core.map.CameraBearing +import com.mapconductor.core.map.CameraPaddingEmulation import com.mapconductor.core.map.MapCameraPosition import com.mapconductor.core.map.MapPaddings import com.mapconductor.core.map.VisibleRegion +import kotlin.math.atan +import kotlin.math.tan import kotlin.time.Duration.Companion.milliseconds import kotlinx.coroutines.delay import kotlinx.coroutines.launch @@ -20,11 +23,45 @@ import kotlinx.coroutines.launch // API が無く、停止時に矩形内へクランプして再適用する。 internal fun ArcGISMapViewController.getFastMapCameraPosition(): MapCameraPosition? = try { - holder.map.getCurrentViewpointCamera()?.toMapCameraPosition() + holder.map.getCurrentViewpointCamera()?.toMapCameraPosition()?.let { camera -> + CameraPaddingEmulation.fromNativeCamera(camera, cameraPaddings, verticalFov(), viewportHeightDp()) + } } catch (_: Exception) { null } +/** + * SceneView のカメラの縦方向の視野角(度)。 + * + * [SceneView.getFieldOfView][com.arcgismaps.mapping.view.SceneView.fieldOfView] は**横方向**の + * 視野角(TB520FU の縦長の全画面で 29.3°、横長で 68.0°)。縦に直すと向きによらず 45° になり、 + * 実測(地図の大きさ・向きを変えて 45.2〜45.4°)と一致する。 + */ +internal fun ArcGISMapViewController.verticalFov(): Double? { + val width = holder.map.width + val height = holder.map.height + if (width <= 0 || height <= 0) return null + val horizontal = runCatching { holder.map.fieldOfView }.getOrNull() ?: return null + val halfVertical = atan(tan(Math.toRadians(horizontal) / 2.0) * height / width) + return Math.toDegrees(2.0 * halfVertical) +} + +/** 地図の高さ(dp)。パディングのエミュレーションで遠近を解くのに使う。 */ +internal fun ArcGISMapViewController.viewportHeightDp(): Double { + val density = holder.mapView.resources.displayMetrics.density + return holder.map.height / density.toDouble() +} + +/** パディングを見る点のずらしに置き換えたカメラ。paddings が null なら今のパディングを使い続ける。 */ +internal fun ArcGISMapViewController.toNativeCamera(position: MapCameraPosition): MapCameraPosition { + position.paddings?.let { cameraPaddings = it } + return CameraPaddingEmulation.toNativeCamera( + position.copy(paddings = cameraPaddings), + verticalFov(), + viewportHeightDp(), + ) +} + internal fun ArcGISMapViewController.invokeCameraMoveStartCallback() { cameraMoveStartHandler()?.let { cb -> getFastMapCameraPosition()?.let { mapCameraPosition -> @@ -151,11 +188,11 @@ internal suspend fun ArcGISMapViewController.getMapCameraPosition(): MapCameraPo paddings = MapPaddings.Zeros, visibleRegion = visibleRegion, ) - return camera + return CameraPaddingEmulation.fromNativeCamera(camera, cameraPaddings, verticalFov(), viewportHeightDp()) } internal fun ArcGISMapViewController.handleMoveCamera(position: MapCameraPosition) { - val dstCameraPosition = toCameraWithView(position) + val dstCameraPosition = toCameraWithView(toNativeCamera(position)) mainCoroutine.launch { if (!holder.mapView.isAttachedToWindow) return@launch @@ -167,7 +204,7 @@ internal fun ArcGISMapViewController.handleAnimateCamera( position: MapCameraPosition, duration: Long, ) { - val dstCameraPosition = toCameraWithView(position) + val dstCameraPosition = toCameraWithView(toNativeCamera(position)) defaultCoroutine.launch { invokeCameraMoveStartCallback()