From 3eb7d6f79a578978dfc4731ed13679ba3a090474 Mon Sep 17 00:00:00 2001 From: Masashi Katsumata Date: Wed, 11 Nov 2026 22:03:19 -0800 Subject: [PATCH] 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, ) +}