Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
6 changes: 6 additions & 0 deletions api/android-for-arcgis.api
Original file line number Diff line number Diff line change
Expand Up @@ -175,12 +175,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<? super kotlin.Unit>);
public static final java.lang.Object invokeCameraMoveStartCallback(com.mapconductor.arcgis.ArcGISMapView2DController, kotlin.coroutines.Continuation<? super kotlin.Unit>);
public static final java.lang.Object invokeCameraMoveCallback(com.mapconductor.arcgis.ArcGISMapView2DController, kotlin.coroutines.Continuation<? super kotlin.Unit>);
public static final java.lang.Object invokeCameraMoveEndCallback(com.mapconductor.arcgis.ArcGISMapView2DController, kotlin.coroutines.Continuation<? super kotlin.Unit>);
public static final void scheduleCameraMoveEndCallback(com.mapconductor.arcgis.ArcGISMapView2DController);
public static final java.lang.Object getMapCameraPosition(com.mapconductor.arcgis.ArcGISMapView2DController, kotlin.coroutines.Continuation<? super com.mapconductor.core.map.MapCameraPosition>);
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);
Expand Down Expand Up @@ -251,6 +254,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<? super kotlin.Unit>);
Expand Down
32 changes: 32 additions & 0 deletions src/main/java/com/mapconductor/arcgis/ArcGISCameraTarget.kt
Original file line number Diff line number Diff line change
@@ -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()),
)
}
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand All @@ -26,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
Expand All @@ -46,7 +50,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
Expand Down Expand Up @@ -81,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
Expand Down Expand Up @@ -316,7 +333,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,
Expand All @@ -330,6 +348,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
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down Expand Up @@ -58,8 +59,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
Expand Down Expand Up @@ -93,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(),
)
}

Expand All @@ -113,11 +145,14 @@ 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
holder.map.setViewpoint(viewpoint)
// tilt だけが変わったときは Viewpoint が同じなので ArcGIS は変化を通知しない。
// そのままだと state のカメラが古い tilt のまま残るので、ここで配る。
if (tiltChanged) onViewpointChange()
}
Unit
}
Expand All @@ -132,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()
Expand Down
10 changes: 10 additions & 0 deletions src/main/java/com/mapconductor/arcgis/ArcGISMapViewController.kt
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down Expand Up @@ -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

/**
Expand Down
Loading
Loading