Skip to content
Open
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
47 changes: 47 additions & 0 deletions src/main/java/com/mapconductor/core/map/CameraBearing.kt
Original file line number Diff line number Diff line change
@@ -0,0 +1,47 @@
package com.mapconductor.core.map

import com.mapconductor.core.InternalMapConductorApi

/**
* [com.mapconductor.core.features.MapCameraPosition.bearing] とネイティブ SDK の回転値の変換。
*
* MapConductor の `bearing` は **「値を増やすと地図が時計回り(右)に回る」** 向きで定義する。
* 0 が北で、90 なら地図全体が右へ 90 度回り、画面の上には西が来る。
*
* ネイティブ SDK は大きく 2 系統に分かれる。
*
* - **heading 系**(Google Maps / MapLibre / Mapbox / HERE / MapKit ほか)
* 「画面の上が指す方位を北から時計回りに測る」=カメラの向き。値を増やすと地図は
* 反時計回りに回るので、MapConductor とは符号が反転する。[toNativeHeading]。
* - **rotation 系**(ArcGIS の `Viewpoint.rotation` など)「北を時計回りに回す量」=
* 地図の回転角。MapConductor と同じ向きなので変換は恒等になる。[toNativeRotation]。
*
* 符号をプロバイダ各所に散らすと必ずどこかが漏れるので、変換はここだけに置く。
*/
@InternalMapConductorApi
object CameraBearing {

/** 角度を 0 以上 360 未満へ畳む。 */
fun normalizeDegrees360(degrees: Double): Double {
if (!degrees.isFinite()) return 0.0
val wrapped = degrees % 360.0
return if (wrapped < 0.0) wrapped + 360.0 else wrapped
}

/**
* MapConductor の bearing → heading 系ネイティブ SDK の値。
*
* カメラが向いている方位でもあるので、負 tilt エミュレーションで
* 「カメラの前進方向」が要るときもこれを使う。
*/
fun toNativeHeading(bearing: Double): Double = normalizeDegrees360(-bearing)

/** heading 系ネイティブ SDK の値 → MapConductor の bearing。 */
fun bearingFromNativeHeading(heading: Double): Double = normalizeDegrees360(-heading)

/** MapConductor の bearing → rotation 系ネイティブ SDK の値(向きが同じなので恒等)。 */
fun toNativeRotation(bearing: Double): Double = normalizeDegrees360(bearing)

/** rotation 系ネイティブ SDK の値 → MapConductor の bearing(同上)。 */
fun bearingFromNativeRotation(rotation: Double): Double = normalizeDegrees360(rotation)
}
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,14 @@ data class VisibleRegion(
interface MapCameraPositionInterface {
val position: GeoPointInterface
val zoom: Double

/**
* 地図の回転角(度、0 が北)。
*
* 値を増やすと地図は**時計回り(右)**に回る。90 なら地図が右へ 90 度回り、
* 画面の上には西が来る。多くのネイティブ SDK の「カメラの向き(heading)」とは
* 符号が逆なので、変換は [CameraBearing] を通すこと。
*/
val bearing: Double
val tilt: Double
val paddings: MapPaddingsInterface?
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@ package com.mapconductor.core.projection
import androidx.compose.ui.geometry.Offset
import com.mapconductor.core.features.GeoPoint
import com.mapconductor.core.features.GeoPointInterface
import com.mapconductor.core.map.CameraBearing
import com.mapconductor.core.map.MapCameraPosition
import kotlin.math.cos
import kotlin.math.pow
Expand Down Expand Up @@ -66,8 +67,9 @@ object WebMercatorScreenProjection {
var sx = dx * worldSize
var sy = (target.y - center.y) * worldSize
if (camera.bearing != 0.0) {
// bearing は「画面の上が指す方位(北から時計回り)」。世界を -bearing 回す。
val angle = -camera.bearing * Math.PI / 180.0
// 画面の上が指す方位はカメラの heading(= bearing の符号反転)。世界を
// -heading 回すと、その方位が画面の上(-y)に来る。
val angle = -CameraBearing.toNativeHeading(camera.bearing) * Math.PI / 180.0
val rx = sx * cos(angle) - sy * sin(angle)
val ry = sx * sin(angle) + sy * cos(angle)
sx = rx
Expand All @@ -90,7 +92,7 @@ object WebMercatorScreenProjection {
var sx = offset.x - widthPx / 2.0
var sy = offset.y - heightPx / 2.0
if (camera.bearing != 0.0) {
val angle = camera.bearing * Math.PI / 180.0
val angle = CameraBearing.toNativeHeading(camera.bearing) * Math.PI / 180.0
val rx = sx * cos(angle) - sy * sin(angle)
val ry = sx * sin(angle) + sy * cos(angle)
sx = rx
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -94,12 +94,19 @@ class WebMercatorScreenProjectionGoldenTest {
assertTrue("ratio=$ratio", abs(ratio - 2f) < 1e-4)
}

/**
* bearing は「地図を時計回りに回す量」。90 なら地図が右へ 90 度回り、
* 画面の上には**西**が来る(東は画面の下)。
*/
@Test
fun `bearing=90 では東が上に来る`() {
fun `bearing=90 では西が上に来る`() {
val c = camera(bearing = 90.0)
val west = project(GeoPoint(c.position.latitude, c.position.longitude - 0.05), c)!!
assertTrue("west.y=${west.y}", west.y < 400f)
assertClose(200.0, west.x)

val east = project(GeoPoint(c.position.latitude, c.position.longitude + 0.05), c)!!
assertTrue("east.y=${east.y}", east.y < 400f)
assertClose(200.0, east.x)
assertTrue("east.y=${east.y}", east.y > 400f)
}

@Test
Expand Down
Loading