From dde9d3aadbcb72ce6cdb53866c5beac444b03425 Mon Sep 17 00:00:00 2001 From: masashi Date: Tue, 1 Sep 2026 11:36:29 +0900 Subject: [PATCH 01/17] Make bearing a clockwise map rotation MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit bearing を「値を増やすと地図が時計回りに回る」向きに統一し、ネイティブ SDK との変換をコアの CameraBearing に集約する。 --- api/js-sdk-core.api.d.ts | 15 +++++++-- src/map/CameraBearing.ts | 66 ++++++++++++++++++++++++++++++++++++++ src/map/FitBoundsCamera.ts | 7 ++-- src/map/index.ts | 1 + src/types/MapCamera.ts | 9 +++++- 5 files changed, 93 insertions(+), 5 deletions(-) create mode 100644 src/map/CameraBearing.ts diff --git a/api/js-sdk-core.api.d.ts b/api/js-sdk-core.api.d.ts index 0b2f0c1..bcc05de 100644 --- a/api/js-sdk-core.api.d.ts +++ b/api/js-sdk-core.api.d.ts @@ -198,7 +198,14 @@ interface MapCameraPosition { center: GeoPoint; /** Zoom level (typically 0-22, where higher is more zoomed in). */ zoom: number; - /** Bearing/rotation in degrees (0-360, where 0 is north). */ + /** + * Map rotation in degrees (0-360, where 0 is north). + * + * Increasing it turns the map **clockwise**: at 90 the map has rotated 90 + * degrees to the right, so west is at the top of the screen. This is the + * opposite sign from the "camera heading" used by most native SDKs — see + * `CameraBearing` for the conversion. + */ bearing: number; /** Kotlin-compatible tilt in degrees. */ tilt: number; @@ -2673,6 +2680,10 @@ declare class MarkerTileRenderer) fall back to the default icon's index rather than being dropped, * so they still render (as the default icon) instead of silently vanishing. + * + * The returned bitmaps belong to the caller, who must release them with + * `closeSWIcons()` once they have been posted. `sendSWRegisterAndWait()` + * already does this, so the usual one-line call site needs nothing extra. */ toSWData(): Promise<{ items: { @@ -4484,4 +4495,4 @@ declare function segmentIntersectsRegion({ start, end, region, geodesic, }: { declare function splitByMeridian(points: GeoPoint[], geodesic: boolean): GeoPoint[][]; -export { AbstractCircleOverlayRenderer, AbstractGroundImageOverlayRenderer, AbstractMarkerController, AbstractMarkerIcon, AbstractMarkerOverlayRenderer, AbstractMarkerRenderingStrategy, AbstractPolygonOverlayRenderer, AbstractPolylineOverlayRenderer, AbstractViewportStrategy, AbstractZoomAltitudeConverter, type AddParams, type AnyMarkerRenderingSupport, type AttributionRule, BaseMapViewController, type BitmapIcon, CANONICAL_ORDER, type CameraOptions, type CameraRestriction, type ChangeParams, type ChangeParamsInterface, type CircleAddParams, type CircleCapable, type CircleChangeParams, CircleController, type CircleEntity, type CircleEvent, type CircleFingerPrint, CircleManager, type CircleManagerInterface, type CircleOptions, CircleOverlay, type CircleOverlayRenderer, type CircleState, type CircleStateCopyParams, CollectorMarkerOverlayRenderer, ColorDefaultIcon, type CreateMarkerStateParams, DEFAULT_CIRCLE_SEGMENTS, DEFAULT_RASTER_LAYER_USER_AGENT, DefaultMarkerEventController, DefaultMarkerIcon, type DefaultMarkerIconOptions, Direction6, Direction6Delta, DriverConformanceViolation, Earth, EmptyMapServiceRegistry, type FitBoundsCameraResult, GROUND_IMAGE_DEFAULT_TILE_SIZE, GeoGridIndex, GeoPoint, type GeoPointInterface, type GeoRectBounds, type GlGestureHandler, type GlGestureHandlers, type GlMarkerDragMap, type GlTouchZoomRotateHandler, type GroundImageAddParams, type GroundImageCapable, type GroundImageChangeParams, GroundImageController, type GroundImageEntity, type GroundImageEvent, type GroundImageFingerPrint, GroundImageManager, type GroundImageManagerInterface, GroundImageOverlay, type GroundImageOverlayRenderer, type GroundImageState, type GroundImageStateCopyParams, type GroundOverlayOptions, GroundScaleZoomAltitudeConverter, type HexCell, HexCellRegistry, type HexCellWithDistance, type HexCoord, type HexGeocell, HexGeocellImpl, type HexGeocell as HexGeocellInterface, IconImageCache, type IdentifiedHexCell, ImageDefaultIcon, type ImageDefaultIconOptions, ImageIcon, type ImageIconOptions, type ImageSource, type InfoBubbleEntry, InitState, KDTree, type KDTreeStats, LocalTileServer, MAP_CAPABILITIES, MARKER_HIT_RADIUS_MOUSE_PX, MARKER_HIT_RADIUS_TOUCH_PX, MARKER_RENDER_BATCH_SIZE, MapCameraPosition, type MapCameraPositionCopyParams, type MapCameraPositionInterface, type MapCapability, MapCapabilityStatus, type MapConfig, type MapDesignTypeInterface, type MapDiagnosticLevel, MapDiagnostics, type MapDiagnosticsSink, MapDriverConformance, type MapGesture, type MapOverlayInterface, MapOverlayRegistry, MapPaddings, type MapPaddingsInterface, MapProjection, MapProvider, MapProviderType, type MapServiceKey, type MapServiceRegistration, MapServiceRegistrations, type MapServiceRegistry, MapUISettings, MapUISettingsDiagnostics, MapViewBase, type MapViewBaseProps, type MapViewControllerInterface, type MapViewHolder, MapViewHolderBase, MapViewState, type MapViewStateInterface, type MapViewStateInternal, type MapViewStateOptions, MarkerAnimation, type MarkerAnimationOverlayEntry, type MarkerAnimationOverlayHost, type MarkerCapable, type MarkerEntity, type MarkerEventController, type MarkerEventHost, type MarkerFingerPrint, type MarkerIcon, MarkerIconSize, type MarkerIngestionResult, MarkerManager, type MarkerManagerStats, type MarkerOptions, MarkerOverlay, type MarkerOverlayRenderer, type MarkerRenderingStrategy, type MarkerRenderingSupport, MarkerRenderingSupportKey, type MarkerState, type MarkerStateCopyParams, MarkerTileRenderer, MarkerTilingOptions, MutableMapServiceRegistry, Mutex, type NativeMapExtensionCapable, type NativeMapExtensionDescriptor, type NativeMapExtensionEvent, type NativeMapExtensionEventHandler, NoCameraRestriction, OVERLAY_KINDS, type Offset, type OnCameraMoveHandler, type OnCircleEventHandler, type OnGroundImageEventHandler, type OnMapEventHandler, type OnMapInitializedHandler, type OnMapLoadedHandler, type OnMarkerEventHandler, type OnPolygonEventHandler, type OnPolylineEventHandler, type OnRasterLayerEventHandler, OverlayCollector, type OverlayController, type OverlayControllerLike, OverlayGeoJson, type OverlayHit, OverlayHitResolver, type OverlayKind, type OverlayRendererInterface, Planar, type PolygonAddParams, type PolygonCapable, type PolygonChangeParams, PolygonController, type PolygonEntity, type PolygonEvent, type PolygonFingerPrint, PolygonManager, type PolygonManagerInterface, type PolygonOptions, PolygonOverlay, type PolygonOverlayRenderer, type PolygonRings, type PolygonState, type PolygonStateCopyParams, type PolylineAddParams, type PolylineCapable, type PolylineChangeParams, PolylineController, type PolylineEntity, type PolylineEvent, type PolylineFingerPrint, type PolylineHitResult, PolylineManager, type PolylineManagerInterface, type PolylineOptions, PolylineOverlay, type PolylineOverlayRenderer, type PolylineState, type PolylineStateCopyParams, type PreparedMarker, type Projection, type RasterAttributionRule, type RasterHeaderRule, RasterHeaderRuleSet, type RasterHeaderSupport, type RasterLayerAddParams, type RasterLayerCapable, type RasterLayerChangeParams, RasterLayerController, type RasterLayerEntity, type RasterLayerEvent, type RasterLayerFingerPrint, RasterLayerManager, type RasterLayerManagerInterface, RasterLayerOverlay, type RasterLayerOverlayRenderer, RasterLayerSource, type RasterLayerState, type RasterLayerStateCopyParams, type RasterRequestParameters, type RasterTransformRequest, type RegistryStats, type ScreenOffsetResult, ScreenProjectionRequirement, type Serializable, Settings, type SlottedOverlayController, type SpatialIndex, type SpatialIndexConfig, Spherical, StrategyMarkerController, type TileProvider, type TileRenderRequest, type TileRenderResponse, type TileRequest, TileScheme, TileServerRegistry, type ViewportSize, type VisibleRegion, WEB_MERCATOR_MAX_EXTENT_METERS, WGS84, WGS84Geodesic, WebMercator, WebMercatorZoomAltitudeConverter, applyGlMapUISettings, bounceInterpolation, bridgeHolesIntoSingleRing, bridgeHolesIntoSingleRingWrapAware, buildPolygonRings, buildPolylineSegments, buildUnwrappedPolygonRings, buildUnwrappedPolylinePath, buildVisibleRegion, calculateMetersPerPixel, capabilityOfGesture, circleToRing, closeRing, closestPointOnSegment, combineHash, computeArea$1 as computeArea, computeDistanceBetween$1 as computeDistanceBetween, computeFitBoundsCameraPosition, computeHeading$1 as computeHeading, computeLength$1 as computeLength, computeOffset$1 as computeOffset, computeOffsetOrigin$1 as computeOffsetOrigin, computeSignedArea$1 as computeSignedArea, computeDistanceBetween as computeWGS84DistanceBetween, createCircleEntity, createCircleState, createCollectorMarkerRenderingSupport, createDefaultIcon, createFingerPrint, createGeoPoint, createGeoRectBounds, createGroundImageEntity, createGroundImageState, createHexCell, createHexCellWithDistance, createHexCoord, createIdentifiedHexCell, createMapCameraPosition, createMapServiceKey, createMarkerEntity, createMarkerFingerPrint, createMarkerState, createOffset, createOppositeMeridianPoint, createOverlayHit, createPolygonEntity, createPolygonState, createPolylineEntity, createPolylineState, createRandomId, createRasterLayerEntity, createRasterLayerState, createSegmentBounds, createSubject, createTileWorkerHandler, densifyAndNormalize, expandBounds, fingerPrintEquals, fromGeoPoint, fromLatLng, fromLatLong, fromLngLat, fromLongLat, generateIdFromHashes, getNeighbors, hashBool, hashDefaultMarkerIcon, hashGeoPoint, hashNullable, hashNum, hashObj, hashStr, hexCellToIdPrefix, hexCoordToString, ingestMarkers, interpolate$1 as interpolate, interpolateAtMeridianGeodesic, interpolateAtMeridianLinear, interpolate as interpolateWGS84, isEmptyCameraRestriction, isFullySupported, isKnownUnsupported, isNativeMapExtensionCapable, isSlottedOverlayController, isUsable, longHashCode, mapCapabilityFromId, mapViewStateInternal, markerIconHashCode, normalizeLngDegrees, resolveAttributionRules, resolveCameraRestriction, resolveHoles, resolveMapUISettings, resolveRasterAttributions, segmentIntersectsRegion, splitByMeridian, splitRingByMeridian, toDegrees, toInt, toRadians, unionHoleRings, unionHoles, unionHolesInPlace, viewportSizeOf, withRasterHeaderTransform, wrapClickedPoint }; +export { AbstractCircleOverlayRenderer, AbstractGroundImageOverlayRenderer, AbstractMarkerController, AbstractMarkerIcon, AbstractMarkerOverlayRenderer, AbstractMarkerRenderingStrategy, AbstractPolygonOverlayRenderer, AbstractPolylineOverlayRenderer, AbstractViewportStrategy, AbstractZoomAltitudeConverter, type AddParams, type AnyMarkerRenderingSupport, type AttributionRule, BaseMapViewController, type BitmapIcon, CANONICAL_ORDER, type CameraOptions, type CameraRestriction, type ChangeParams, type ChangeParamsInterface, type CircleAddParams, type CircleCapable, type CircleChangeParams, CircleController, type CircleEntity, type CircleEvent, type CircleFingerPrint, CircleManager, type CircleManagerInterface, type CircleOptions, CircleOverlay, type CircleOverlayRenderer, type CircleState, type CircleStateCopyParams, CollectorMarkerOverlayRenderer, ColorDefaultIcon, type CreateMarkerStateParams, DEFAULT_CIRCLE_SEGMENTS, DEFAULT_RASTER_LAYER_USER_AGENT, DefaultMarkerEventController, DefaultMarkerIcon, type DefaultMarkerIconOptions, Direction6, Direction6Delta, DriverConformanceViolation, Earth, EmptyMapServiceRegistry, type FitBoundsCameraResult, GROUND_IMAGE_DEFAULT_TILE_SIZE, GeoGridIndex, GeoPoint, type GeoPointInterface, type GeoRectBounds, type GlGestureHandler, type GlGestureHandlers, type GlMarkerDragMap, type GlTouchZoomRotateHandler, type GroundImageAddParams, type GroundImageCapable, type GroundImageChangeParams, GroundImageController, type GroundImageEntity, type GroundImageEvent, type GroundImageFingerPrint, GroundImageManager, type GroundImageManagerInterface, GroundImageOverlay, type GroundImageOverlayRenderer, type GroundImageState, type GroundImageStateCopyParams, type GroundOverlayOptions, GroundScaleZoomAltitudeConverter, type HexCell, HexCellRegistry, type HexCellWithDistance, type HexCoord, type HexGeocell, HexGeocellImpl, type HexGeocell as HexGeocellInterface, IconImageCache, type IdentifiedHexCell, ImageDefaultIcon, type ImageDefaultIconOptions, ImageIcon, type ImageIconOptions, type ImageSource, type InfoBubbleEntry, InitState, KDTree, type KDTreeStats, LocalTileServer, MAP_CAPABILITIES, MARKER_HIT_RADIUS_MOUSE_PX, MARKER_HIT_RADIUS_TOUCH_PX, MARKER_RENDER_BATCH_SIZE, MapCameraPosition, type MapCameraPositionCopyParams, type MapCameraPositionInterface, type MapCapability, MapCapabilityStatus, type MapConfig, type MapDesignTypeInterface, type MapDiagnosticLevel, MapDiagnostics, type MapDiagnosticsSink, MapDriverConformance, type MapGesture, type MapOverlayInterface, MapOverlayRegistry, MapPaddings, type MapPaddingsInterface, MapProjection, MapProvider, MapProviderType, type MapServiceKey, type MapServiceRegistration, MapServiceRegistrations, type MapServiceRegistry, MapUISettings, MapUISettingsDiagnostics, MapViewBase, type MapViewBaseProps, type MapViewControllerInterface, type MapViewHolder, MapViewHolderBase, MapViewState, type MapViewStateInterface, type MapViewStateInternal, type MapViewStateOptions, MarkerAnimation, type MarkerAnimationOverlayEntry, type MarkerAnimationOverlayHost, type MarkerCapable, type MarkerEntity, type MarkerEventController, type MarkerEventHost, type MarkerFingerPrint, type MarkerIcon, MarkerIconSize, type MarkerIngestionResult, MarkerManager, type MarkerManagerStats, type MarkerOptions, MarkerOverlay, type MarkerOverlayRenderer, type MarkerRenderingStrategy, type MarkerRenderingSupport, MarkerRenderingSupportKey, type MarkerState, type MarkerStateCopyParams, MarkerTileRenderer, MarkerTilingOptions, MutableMapServiceRegistry, Mutex, type NativeMapExtensionCapable, type NativeMapExtensionDescriptor, type NativeMapExtensionEvent, type NativeMapExtensionEventHandler, NoCameraRestriction, OVERLAY_KINDS, type Offset, type OnCameraMoveHandler, type OnCircleEventHandler, type OnGroundImageEventHandler, type OnMapEventHandler, type OnMapInitializedHandler, type OnMapLoadedHandler, type OnMarkerEventHandler, type OnPolygonEventHandler, type OnPolylineEventHandler, type OnRasterLayerEventHandler, OverlayCollector, type OverlayController, type OverlayControllerLike, OverlayGeoJson, type OverlayHit, OverlayHitResolver, type OverlayKind, type OverlayRendererInterface, Planar, type PolygonAddParams, type PolygonCapable, type PolygonChangeParams, PolygonController, type PolygonEntity, type PolygonEvent, type PolygonFingerPrint, PolygonManager, type PolygonManagerInterface, type PolygonOptions, PolygonOverlay, type PolygonOverlayRenderer, type PolygonRings, type PolygonState, type PolygonStateCopyParams, type PolylineAddParams, type PolylineCapable, type PolylineChangeParams, PolylineController, type PolylineEntity, type PolylineEvent, type PolylineFingerPrint, type PolylineHitResult, PolylineManager, type PolylineManagerInterface, type PolylineOptions, PolylineOverlay, type PolylineOverlayRenderer, type PolylineState, type PolylineStateCopyParams, type PreparedMarker, type Projection, type RasterAttributionRule, type RasterHeaderRule, RasterHeaderRuleSet, type RasterHeaderSupport, type RasterLayerAddParams, type RasterLayerCapable, type RasterLayerChangeParams, RasterLayerController, type RasterLayerEntity, type RasterLayerEvent, type RasterLayerFingerPrint, RasterLayerManager, type RasterLayerManagerInterface, RasterLayerOverlay, type RasterLayerOverlayRenderer, RasterLayerSource, type RasterLayerState, type RasterLayerStateCopyParams, type RasterRequestParameters, type RasterTransformRequest, type RegistryStats, type ScreenOffsetResult, ScreenProjectionRequirement, type Serializable, Settings, type SlottedOverlayController, type SpatialIndex, type SpatialIndexConfig, Spherical, StrategyMarkerController, type TileProvider, type TileRenderRequest, type TileRenderResponse, type TileRequest, TileScheme, TileServerRegistry, type ViewportSize, type VisibleRegion, WEB_MERCATOR_MAX_EXTENT_METERS, WGS84, WGS84Geodesic, WebMercator, WebMercatorZoomAltitudeConverter, applyGlMapUISettings, bearingFromNativeHeading, bearingFromNativeRotation, bounceInterpolation, bridgeHolesIntoSingleRing, bridgeHolesIntoSingleRingWrapAware, buildPolygonRings, buildPolylineSegments, buildUnwrappedPolygonRings, buildUnwrappedPolylinePath, buildVisibleRegion, calculateMetersPerPixel, capabilityOfGesture, circleToRing, closeRing, closestPointOnSegment, combineHash, computeArea$1 as computeArea, computeDistanceBetween$1 as computeDistanceBetween, computeFitBoundsCameraPosition, computeHeading$1 as computeHeading, computeLength$1 as computeLength, computeOffset$1 as computeOffset, computeOffsetOrigin$1 as computeOffsetOrigin, computeSignedArea$1 as computeSignedArea, computeDistanceBetween as computeWGS84DistanceBetween, createCircleEntity, createCircleState, createCollectorMarkerRenderingSupport, createDefaultIcon, createFingerPrint, createGeoPoint, createGeoRectBounds, createGroundImageEntity, createGroundImageState, createHexCell, createHexCellWithDistance, createHexCoord, createIdentifiedHexCell, createMapCameraPosition, createMapServiceKey, createMarkerEntity, createMarkerFingerPrint, createMarkerState, createOffset, createOppositeMeridianPoint, createOverlayHit, createPolygonEntity, createPolygonState, createPolylineEntity, createPolylineState, createRandomId, createRasterLayerEntity, createRasterLayerState, createSegmentBounds, createSubject, createTileWorkerHandler, densifyAndNormalize, expandBounds, fingerPrintEquals, fromGeoPoint, fromLatLng, fromLatLong, fromLngLat, fromLongLat, generateIdFromHashes, getNeighbors, hashBool, hashDefaultMarkerIcon, hashGeoPoint, hashNullable, hashNum, hashObj, hashStr, hexCellToIdPrefix, hexCoordToString, ingestMarkers, interpolate$1 as interpolate, interpolateAtMeridianGeodesic, interpolateAtMeridianLinear, interpolate as interpolateWGS84, isEmptyCameraRestriction, isFullySupported, isKnownUnsupported, isNativeMapExtensionCapable, isSlottedOverlayController, isUsable, longHashCode, mapCapabilityFromId, mapViewStateInternal, markerIconHashCode, normalizeLngDegrees, resolveAttributionRules, resolveCameraRestriction, resolveHoles, resolveMapUISettings, resolveRasterAttributions, segmentIntersectsRegion, splitByMeridian, splitRingByMeridian, toDegrees, toInt, toNativeHeading, toNativeRotation, toRadians, unionHoleRings, unionHoles, unionHolesInPlace, viewportSizeOf, withRasterHeaderTransform, wrapClickedPoint }; diff --git a/src/map/CameraBearing.ts b/src/map/CameraBearing.ts new file mode 100644 index 0000000..44e5e10 --- /dev/null +++ b/src/map/CameraBearing.ts @@ -0,0 +1,66 @@ +// `MapCameraPosition.bearing` とネイティブ SDK の回転値の変換。 +// +// MapConductor の `bearing` は **「値を増やすと地図が時計回り(右)に回る」** 向きで +// 定義する。0 が北で、90 なら地図全体が右へ 90 度回り、画面の上には西が来る。 +// +// ネイティブ SDK は大きく 2 系統に分かれる。 +// +// - **heading 系**(Google Maps / MapLibre / Mapbox / HERE / Cesium / MapKit ほか) +// 「画面の上が指す方位を北から時計回りに測る」=カメラの向き。値を増やすと地図は +// 反時計回りに回るので、MapConductor とは符号が反転する。{@link toNativeHeading}。 +// - **rotation 系**(ArcGIS の `Viewpoint.rotation` / OpenLayers の `View.rotation`) +// 「北を時計回りに回す量」=地図の回転角。MapConductor と同じ向きなので変換は +// 恒等になる。{@link toNativeRotation}。 +// +// 符号をプロバイダ各所に散らすと必ずどこかが漏れるので、変換はここだけに置く。 +// +// ここはドライバー実装点で、アプリ開発者向けの公開 API ではない(android の +// `@InternalMapConductorApi`、iOS の `@_spi(MapConductorDriver)` に対応)。 +// 各関数の JSDoc に付けたタグで公開サーフェスから除外している。 +// +// NOTE: このモジュールレベルのコメントに除外タグを**文字列としても書かないこと**。 +// `scripts/api-surface.mjs` はタグを含む JSDoc の直後の宣言を丸ごと読み飛ばすので、 +// 説明のつもりで書くと関係のない宣言が 1 つ消え、残りの JSDoc だけが漏れる。 + + +/** 角度を 0 以上 360 未満へ畳む。外へは出さない(下の 4 つだけがドライバー向けの口)。 */ +function normalizeDegrees360(degrees: number): number { + if (!Number.isFinite(degrees)) return 0; + const wrapped = degrees % 360; + return wrapped < 0 ? wrapped + 360 : wrapped; +} + +/** + * MapConductor の bearing → heading 系ネイティブ SDK の値。 + * + * カメラが向いている方位でもあるので、負 tilt エミュレーションで + * 「カメラの前進方向」が要るときもこれを使う。 + * @internal + */ +export function toNativeHeading(bearing: number): number { + return normalizeDegrees360(-bearing); +} + +/** + * heading 系ネイティブ SDK の値 → MapConductor の bearing。 + * @internal + */ +export function bearingFromNativeHeading(heading: number): number { + return normalizeDegrees360(-heading); +} + +/** + * MapConductor の bearing → rotation 系ネイティブ SDK の値(向きが同じなので恒等)。 + * @internal + */ +export function toNativeRotation(bearing: number): number { + return normalizeDegrees360(bearing); +} + +/** + * rotation 系ネイティブ SDK の値 → MapConductor の bearing(同上)。 + * @internal + */ +export function bearingFromNativeRotation(rotation: number): number { + return normalizeDegrees360(rotation); +} diff --git a/src/map/FitBoundsCamera.ts b/src/map/FitBoundsCamera.ts index afd9fc8..87c3a60 100644 --- a/src/map/FitBoundsCamera.ts +++ b/src/map/FitBoundsCamera.ts @@ -1,6 +1,7 @@ import { GeoPoint, createGeoPoint } from "../features/GeoPoint"; import type { GeoRectBounds } from "../features/GeoRectBounds"; import { WebMercator, WEB_MERCATOR_MAX_EXTENT_METERS } from "../projection/WebMercator"; +import { toNativeHeading } from "./CameraBearing"; /** * Unified, provider-independent "fit bounds" camera computation. @@ -67,8 +68,10 @@ export function computeFitBoundsCameraPosition({ const centerX = (corners[0].x + corners[3].x) / 2; const centerY = (corners[0].y + corners[3].y) / 2; - // Rotate corners by -bearing about the center → screen-aligned extent. - const rad = (-bearing * Math.PI) / 180; + // Rotate the corners about the center so the direction that is *up* on + // screen lands on +y → screen-aligned extent. Screen-up is the camera + // heading, which is the opposite of MapConductor's map-rotation bearing. + const rad = (-toNativeHeading(bearing) * Math.PI) / 180; const cos = Math.cos(rad); const sin = Math.sin(rad); let minX = Infinity; diff --git a/src/map/index.ts b/src/map/index.ts index be27fb9..124ccee 100644 --- a/src/map/index.ts +++ b/src/map/index.ts @@ -1,5 +1,6 @@ export * from "./MapDesignTypeInterface"; export * from "./MapProjection"; +export * from "./CameraBearing"; export * from './AttributionRule'; export * from "./CameraRestriction"; export * from "./MapViewHolder"; diff --git a/src/types/MapCamera.ts b/src/types/MapCamera.ts index 38fd340..a4d75e5 100644 --- a/src/types/MapCamera.ts +++ b/src/types/MapCamera.ts @@ -49,7 +49,14 @@ export interface MapCameraPosition { center: GeoPoint; /** Zoom level (typically 0-22, where higher is more zoomed in). */ zoom: number; - /** Bearing/rotation in degrees (0-360, where 0 is north). */ + /** + * Map rotation in degrees (0-360, where 0 is north). + * + * Increasing it turns the map **clockwise**: at 90 the map has rotated 90 + * degrees to the right, so west is at the top of the screen. This is the + * opposite sign from the "camera heading" used by most native SDKs — see + * `CameraBearing` for the conversion. + */ bearing: number; /** Kotlin-compatible tilt in degrees. */ tilt: number; From f081ae0bfe030fd46063e745ac3e56f6afd9032e Mon Sep 17 00:00:00 2001 From: Masashi Katsumata Date: Sat, 5 Sep 2026 08:15:05 +0900 Subject: [PATCH 02/17] Draw marker tiles on whole pixels The destination came straight out of the projection, so it landed on a fraction of a pixel almost every time, and a non-integer destination makes the canvas resample every marker. Measured in Chromium with 20k markers: 277 ms unaligned against 38 ms aligned. Rounding moves a pin by at most half a pixel, which is not visible at icon scale. The same change is worth 20x on Android and 4.6x on iOS, measured on each. Both draw loops needed it. tile-sw.js carries its own copy of the renderer and is the path that actually runs in a browser, so fixing only the TypeScript one would have left the fix unreachable. Also stop the second query+prepare pass from firing on every tile. The fixed 32px assumption is smaller than the real icons, so the "conservative first pass" never paid off and simply doubled the work; starting from the largest extent a tile has actually needed costs one extra pass in total. --- src/marker/MarkerTileRenderer.ts | 27 ++++++++++++++++++++++----- src/tileserver/tile-sw.js | 11 ++++++++++- 2 files changed, 32 insertions(+), 6 deletions(-) diff --git a/src/marker/MarkerTileRenderer.ts b/src/marker/MarkerTileRenderer.ts index a79f14b..d7029c4 100644 --- a/src/marker/MarkerTileRenderer.ts +++ b/src/marker/MarkerTileRenderer.ts @@ -67,6 +67,9 @@ export class MarkerTileRenderer(); + /** Largest icon half-extent any tile has needed so far, in px. */ + private observedHalfExtentPx = 32; + // android-sdk / ios-sdk の MarkerTileRenderer と同じく位置引数で受ける // (名前付きオプション型は持たない)。 constructor( @@ -193,9 +196,18 @@ export class MarkerTileRenderer assumedHalfExtentPx + 1) { + this.observedHalfExtentPx = maxHalfExtentPx; candidates = this.queryCandidates(z, x, y, maxHalfExtentPx); await Promise.all([...new Set(candidates.map((c) => this.bitmapIconOf(c).url))].map((url) => this.icons.ensure(url))); ({ markers, maxHalfExtentPx } = this.prepareMarkers(candidates, tileOriginWx, tileOriginWy, z, tilePx)); @@ -296,7 +313,7 @@ export class MarkerTileRenderer Date: Sat, 5 Sep 2026 14:14:38 +0900 Subject: [PATCH 03/17] Draw markers that sit exactly on top of one another once MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Zoom out far enough and a whole city collapses onto a few hundred pixels: at z6 a dataset of 20k markers resolves to a couple of thousand distinct positions, and the rest are drawn underneath copies of themselves. Keeping the last of each group is what would have been visible anyway, since drawing is in painters order. Only exact agreement counts — same rounded rectangle, same icon — so nothing that could peek out from behind another is dropped. Where markers share a rectangle but not an icon, none are dropped: a different icon may be transparent where the one above it is not, and then the one underneath does show through. This is deliberately not the decluttering the docs describe. That thins markers that are merely close, which changes what the map shows and is a product decision; this only removes drawing that has no effect. A test renders fifty stacked copies and one marker and asserts the two tiles are identical. Per tile at z6 with 20k markers: 414 ms -> 261 ms on an iPad Pro, 852 ms -> 617 ms on a Pixel 5a. The Android gain is smaller because its tile is 1344px against 512px, so fewer markers land on the same pixel. Both draw loops needed it. tile-sw.js carries its own copy of the renderer and is the path that actually runs in a browser. --- src/marker/MarkerTileRenderer.ts | 45 +++++++++++++++++++++++++++----- src/tileserver/tile-sw.js | 38 ++++++++++++++++++--------- 2 files changed, 64 insertions(+), 19 deletions(-) diff --git a/src/marker/MarkerTileRenderer.ts b/src/marker/MarkerTileRenderer.ts index d7029c4..7ef69f7 100644 --- a/src/marker/MarkerTileRenderer.ts +++ b/src/marker/MarkerTileRenderer.ts @@ -193,7 +193,21 @@ export class MarkerTileRenderer