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
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 Down Expand Up @@ -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
Expand Down Expand Up @@ -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,
Expand All @@ -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
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down Expand Up @@ -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
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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)
Expand Down
12 changes: 7 additions & 5 deletions src/main/java/com/mapconductor/arcgis/MapCameraPosition.kt
Original file line number Diff line number Diff line change
Expand Up @@ -147,23 +147,25 @@ 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),
tilt = this.pitch,
paddings = MapPaddings.Zeros,
visibleRegion = null,
)
}
Loading