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
Original file line number Diff line number Diff line change
Expand Up @@ -45,8 +45,9 @@ public enum WebMercatorScreenProjection {
var sx = dx * worldSize
var sy = (target.y - center.y) * worldSize
if camera.bearing != 0 {
// bearing は「画面の上が指す方位(北から時計回り)」。世界を -bearing 回す。
let angle = -camera.bearing * .pi / 180.0
// 画面の上が指す方位はカメラの heading(= bearing の符号反転)。世界を
// -heading 回すと、その方位が画面の上(-y)に来る。
let angle = -CameraBearing.toNativeHeading(camera.bearing) * .pi / 180.0
let rx = sx * cos(angle) - sy * sin(angle)
let ry = sx * sin(angle) + sy * cos(angle)
sx = rx
Expand All @@ -66,7 +67,7 @@ public enum WebMercatorScreenProjection {
var sx = offset.x - size.width / 2.0
var sy = offset.y - size.height / 2.0
if camera.bearing != 0 {
let angle = camera.bearing * .pi / 180.0
let angle = CameraBearing.toNativeHeading(camera.bearing) * .pi / 180.0
let rx = sx * cos(angle) - sy * sin(angle)
let ry = sx * sin(angle) + sy * cos(angle)
sx = rx
Expand Down
52 changes: 52 additions & 0 deletions Sources/MapConductorCore/map/CameraBearing.swift
Original file line number Diff line number Diff line change
@@ -0,0 +1,52 @@
import Foundation

/// `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(_:)``。
///
/// 符号をプロバイダ各所に散らすと必ずどこかが漏れるので、変換はここだけに置く。
///
/// ドライバー実装点なので `@_spi(MapConductorDriver)`(android の
/// `@InternalMapConductorApi`、React の `/** @internal */` に対応)。
@_spi(MapConductorDriver)
public enum CameraBearing {

/// 角度を 0 以上 360 未満へ畳む。
public static func normalizeDegrees360(_ degrees: Double) -> Double {
guard degrees.isFinite else { return 0 }
let wrapped = degrees.truncatingRemainder(dividingBy: 360.0)
return wrapped < 0 ? wrapped + 360.0 : wrapped
}

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

/// heading 系ネイティブ SDK の値 → MapConductor の bearing。
public static func bearingFromNativeHeading(_ heading: Double) -> Double {
normalizeDegrees360(-heading)
}

/// MapConductor の bearing → rotation 系ネイティブ SDK の値(向きが同じなので恒等)。
public static func toNativeRotation(_ bearing: Double) -> Double {
normalizeDegrees360(bearing)
}

/// rotation 系ネイティブ SDK の値 → MapConductor の bearing(同上)。
public static func bearingFromNativeRotation(_ rotation: Double) -> Double {
normalizeDegrees360(rotation)
}
}
5 changes: 5 additions & 0 deletions Sources/MapConductorCore/map/MapCameraPositionBase.swift
Original file line number Diff line number Diff line change
Expand Up @@ -41,6 +41,11 @@ public struct VisibleRegion: Equatable, Hashable {
public protocol MapCameraPositionProtocol {
var position: GeoPointProtocol { get }
var zoom: Double { get }
/// 地図の回転角(度、0 が北)。
///
/// 値を増やすと地図は**時計回り(右)**に回る。90 なら地図が右へ 90 度回り、
/// 画面の上には西が来る。多くのネイティブ SDK の「カメラの向き(heading)」とは
/// 符号が逆なので、変換は ``CameraBearing`` を通すこと。
var bearing: Double { get }
var tilt: Double { get }
var paddings: MapPaddingsProtocol? { get }
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -79,13 +79,20 @@ final class WebMercatorScreenProjectionGoldenTests: XCTestCase {
XCTAssertEqual(ratio, 2, accuracy: 1e-9)
}

func testBearing90PutsEastAtTheTop() throws {
/// bearing は「地図を時計回りに回す量」。90 なら地図が右へ 90 度回り、
/// 画面の上には**西**が来る(東は画面の下)。
func testBearing90PutsWestAtTheTop() throws {
let c = camera(bearing: 90)
let west = try XCTUnwrap(
project(GeoPoint(latitude: c.position.latitude, longitude: c.position.longitude - 0.05), c)
)
XCTAssertLessThan(west.y, 400)
XCTAssertEqual(west.x, 200, accuracy: 1e-6)

let east = try XCTUnwrap(
project(GeoPoint(latitude: c.position.latitude, longitude: c.position.longitude + 0.05), c)
)
XCTAssertLessThan(east.y, 400)
XCTAssertEqual(east.x, 200, accuracy: 1e-6)
XCTAssertGreaterThan(east.y, 400)
}

func testDatelineWrapsTheShortWay() throws {
Expand Down
Loading