Skip to content

Make bearing a clockwise map rotation - #10

Merged
wf9a5m75 merged 1 commit into
developfrom
feature/bearing-clockwise
Sep 12, 2026
Merged

wf9a5m75 merged 1 commit into
developfrom
feature/bearing-clockwise

Conversation

@wf9a5m75

@wf9a5m75 wf9a5m75 commented Sep 1, 2026

Copy link
Copy Markdown
Contributor

なぜ

MapCameraPosition.bearing は 「値を増やすと地図が時計回り(右)に回る」 向きで定義する、というのが本来の設計。
ところが実装は Google / MapLibre / Mapbox などネイティブ SDK の カメラの向き(heading) をそのまま渡していて、
値を増やすと地図が反時計回りに回っていた。両者は符号が反転している。

bearing 期待する見え方
0 北が上
90 地図が右へ 90 度回り、西が上

どう直したか

符号をプロバイダ各所に散らすと必ずどこかが漏れるので、変換はコアの CameraBearing 1 か所に集約した。

toNativeHeading(bearing)     = normalize360(-bearing)   // heading 系 SDK(Google / MapLibre / Mapbox / HERE / Cesium ほか)
bearingFromNativeHeading(h)  = normalize360(-h)
toNativeRotation(bearing)    = normalize360(bearing)    // rotation 系 SDK(ArcGIS JS / OpenLayers / MapKit JS)。恒等
bearingFromNativeRotation(r) = normalize360(r)

画面投影と、各プロバイダの負 tilt エミュレーション(カメラの前進方向)も同じ変換を通している。

見た目

同じカメラ(エッフェル塔 / zoom 17 / tilt 0 / MapLibre)で bearing だけを変えたもの。

bearing = 0 bearing = 90
bearing 0 bearing 90

bearing 0 ではセーヌ川が左上(=北が上)。bearing 90 では地図が右へ 90 度回り、セーヌ川が右上に来る(=西が上)。

確認したこと

  • Android 実機(Lenovo TB520FU)11 プロバイダ、iOS 実機(iPad Pro 11")12 プロバイダ、web 実ブラウザで向きが一致
  • ユニットテスト: android 236 / ios 229 / js-sdk-core 66 すべて通過(旧規約を固定していた 3 件は期待値を更新)
  • 公開 API サーフェスのゲートと lint も通過

注意

破壊的変更。既存利用者の bearing は逆を向く。visibleRegion / 画面投影 / fitBounds / 負 tilt がすべて bearing に依存しているため、
一部だけ取り込むと「地図は回るがマーカーが逆」になる。関連 PR はまとめて取り込むこと。

作業中に判明した非対称(コードにコメントあり):

web ネイティブ
ArcGIS MapView.rotation は rotation 系(恒等) Viewpoint.rotation は heading 系(反転)
MapKit map.rotation は rotation 系(恒等) MKMapCamera.heading は heading 系(反転)

型定義やネイティブ版からの推測ではなく、実機 / 実ブラウザで測って決めた。

bearing を「値を増やすと地図が時計回りに回る」向きに統一し、ネイティブ SDK との変換をコアの CameraBearing に集約する。
@wf9a5m75
wf9a5m75 merged commit 05bf250 into develop Sep 12, 2026
2 checks passed
@wf9a5m75 wf9a5m75 mentioned this pull request Oct 23, 2026
wf9a5m75 added a commit that referenced this pull request Oct 23, 2026
* Make bearing a clockwise map rotation (#10)

bearing を「値を増やすと地図が時計回りに回る」向きに統一し、ネイティブ SDK との変換をコアの CameraBearing に集約する。

* fix: zoom 0 is a zoom, not a missing one (#11)

moveCameraTo(cameraPosition:) ran every camera through resolveCameraPosition,
which read `zoom == 0 && bearing == 0 && tilt == 0` as "the caller only gave me
a position" and replaced the requested camera with the stored one, keeping the
position alone. A camera that really does ask for zoom 0 — the whole world —
matches that test exactly, so it was the one camera the map would not move to.

It shows up as a feature that does nothing rather than as a wrong value: the
map stays where it was, so what that invites you to conclude is that zoom 0 is
unsupported.

Position-only already has a way to be said: the `moveCameraTo(position:)`
overload. Inferring the same intent a second time from the *value* costs a
legal zoom, and cannot tell `MapCameraPosition(position:)` from one written
with `zoom: 0` anyway — zoom defaults to 0, so by the time a camera arrives
the two are one value.

android-sdk never had this; MapViewState.kt hands the camera straight to the
controller. js-sdk-core carried the same code and loses it in the same change
(MapConductor/js-sdk-core#4), so all three platforms now agree.

* Add labelPosition to the default marker icon and pick the marker drawn under the tap (#12)

* Release 1.3.2
wf9a5m75 added a commit that referenced this pull request Oct 24, 2026
* Make bearing a clockwise map rotation (#10)

bearing を「値を増やすと地図が時計回りに回る」向きに統一し、ネイティブ SDK との変換をコアの CameraBearing に集約する。

* fix: zoom 0 is a zoom, not a missing one (#11)

moveCameraTo(cameraPosition:) ran every camera through resolveCameraPosition,
which read `zoom == 0 && bearing == 0 && tilt == 0` as "the caller only gave me
a position" and replaced the requested camera with the stored one, keeping the
position alone. A camera that really does ask for zoom 0 — the whole world —
matches that test exactly, so it was the one camera the map would not move to.

It shows up as a feature that does nothing rather than as a wrong value: the
map stays where it was, so what that invites you to conclude is that zoom 0 is
unsupported.

Position-only already has a way to be said: the `moveCameraTo(position:)`
overload. Inferring the same intent a second time from the *value* costs a
legal zoom, and cannot tell `MapCameraPosition(position:)` from one written
with `zoom: 0` anyway — zoom defaults to 0, so by the time a camera arrives
the two are one value.

android-sdk never had this; MapViewState.kt hands the camera straight to the
controller. js-sdk-core carried the same code and loses it in the same change
(MapConductor/js-sdk-core#4), so all three platforms now agree.

* Add labelPosition to the default marker icon and pick the marker drawn under the tap (#12)

* Release 1.3.2
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant