diff --git a/api/js-sdk-core.api.d.ts b/api/js-sdk-core.api.d.ts index 08a5ad8..8e328e1 100644 --- a/api/js-sdk-core.api.d.ts +++ b/api/js-sdk-core.api.d.ts @@ -187,7 +187,7 @@ type MapCameraPositionInterface = { /** * Creates a default camera position. */ -declare function createMapCameraPosition({ position, zoom, bearing, tilt, paddings, visibleRegion, }: MapCameraPositionInterface): MapCameraPosition; +declare function createMapCameraPosition({ position, zoom: givenZoom, bearing: givenBearing, tilt: givenTilt, paddings, visibleRegion, }: MapCameraPositionInterface): MapCameraPosition; /** * Represents the camera position and orientation for the map view. */ @@ -196,7 +196,12 @@ interface MapCameraPosition { position: GeoPoint; /** Backward-compatible alias for position. */ center: GeoPoint; - /** Zoom level (typically 0-22, where higher is more zoomed in). */ + /** + * Zoom level (typically 0-22, where higher is more zoomed in). + * + * Reads 0 when the camera was built without one; {@link isZoomSpecified} + * tells the two apart. + */ zoom: number; /** * Map rotation in degrees (0-360, where 0 is north). @@ -211,12 +216,29 @@ interface MapCameraPosition { tilt: number; visibleRegion: VisibleRegion | null; paddings: MapPaddings | null; + /** + * Whether `zoom` / `bearing` / `tilt` were given. + * + * Kept so an explicit 0 (zoom 0 is the whole world, bearing 0 is north up, + * tilt 0 is straight down) is not mistaken for "not given". + * `MapViewState.moveCameraTo` takes only the unset ones from the current + * camera ({@link resolvedOnto}). + */ + readonly isZoomSpecified: boolean; + readonly isBearingSpecified: boolean; + readonly isTiltSpecified: boolean; equals(other: MapCameraPosition | null, tolerance?: number): boolean; + /** Keeps a property unset unless `partial` gives it. */ copy(partial?: MapCameraPositionCopyParams): MapCameraPosition; + /** + * Returns this camera with the unset `zoom` / `bearing` / `tilt` filled in + * from `current`. A property that was given is used as is, 0 included. + */ + resolvedOnto(current: MapCameraPosition): MapCameraPosition; } declare namespace MapCameraPosition { const Default: MapCameraPosition; - function from(other: MapCameraPositionInterface): MapCameraPosition; + function from(other: MapCameraPositionInterface | MapCameraPosition): MapCameraPosition; } interface Offset { diff --git a/src/map/MapViewState.ts b/src/map/MapViewState.ts index 5dcfb10..3f25f83 100644 --- a/src/map/MapViewState.ts +++ b/src/map/MapViewState.ts @@ -187,11 +187,11 @@ export abstract class MapViewState; return createMapCameraPosition({ position: other.position, - zoom: other.zoom, - bearing: other.bearing, - tilt: other.tilt, + zoom: camera.isZoomSpecified === false ? undefined : other.zoom, + bearing: camera.isBearingSpecified === false ? undefined : other.bearing, + tilt: camera.isTiltSpecified === false ? undefined : other.tilt, paddings: other.paddings, visibleRegion: other.visibleRegion, }); diff --git a/test/CameraPositionResolve.test.mjs b/test/CameraPositionResolve.test.mjs new file mode 100644 index 0000000..b9f0fcf --- /dev/null +++ b/test/CameraPositionResolve.test.mjs @@ -0,0 +1,75 @@ +// 未指定の zoom / bearing / tilt だけが現在のカメラから引き継がれ、明示された 0 は +// そのまま使われること。android-sdk の `CameraPositionResolveTest`、ios-sdk の +// `CameraPositionResolveTests` と同じ観点を見る。 + +import { test } from 'node:test'; +import assert from 'node:assert/strict'; + +import { MapCameraPosition, MapViewState, createMapCameraPosition } from '../dist/index.mjs'; + +const tokyo = { latitude: 35.6812, longitude: 139.7671 }; +const osaka = { latitude: 34.7025, longitude: 135.4959 }; +const current = createMapCameraPosition({ position: tokyo, zoom: 12, bearing: 30, tilt: 45 }); + +test('a position-only camera keeps everything else', () => { + const resolved = createMapCameraPosition({ position: osaka }).resolvedOnto(current); + assert.equal(resolved.position.latitude, osaka.latitude); + assert.equal(resolved.zoom, 12); + assert.equal(resolved.bearing, 30); + assert.equal(resolved.tilt, 45); +}); + +test('an explicit 0 is honoured', () => { + const resolved = createMapCameraPosition({ position: osaka, zoom: 0, bearing: 0, tilt: 0 }) + .resolvedOnto(current); + assert.equal(resolved.zoom, 0); + assert.equal(resolved.bearing, 0); + assert.equal(resolved.tilt, 0); +}); + +test('each property is inherited on its own', () => { + const zoomOnly = createMapCameraPosition({ position: osaka, zoom: 0 }).resolvedOnto(current); + assert.deepEqual([zoomOnly.zoom, zoomOnly.bearing, zoomOnly.tilt], [0, 30, 45]); + + const bearingOnly = createMapCameraPosition({ position: osaka, bearing: 0 }).resolvedOnto(current); + assert.deepEqual([bearingOnly.zoom, bearingOnly.bearing, bearingOnly.tilt], [12, 0, 45]); + + const tiltOnly = createMapCameraPosition({ position: osaka, tilt: 0 }).resolvedOnto(current); + assert.deepEqual([tiltOnly.zoom, tiltOnly.bearing, tiltOnly.tilt], [12, 30, 0]); +}); + +test('an unset property reads 0 before resolving', () => { + const camera = createMapCameraPosition({ position: osaka }); + assert.equal(camera.zoom, 0); + assert.equal(camera.isZoomSpecified, false); + assert.equal(camera.isBearingSpecified, false); + assert.equal(camera.isTiltSpecified, false); + assert.equal(MapCameraPosition.Default.isZoomSpecified, true); +}); + +test('copy and from keep an unset property unset', () => { + const unset = createMapCameraPosition({ position: osaka }); + assert.equal(unset.copy({ position: tokyo }).isZoomSpecified, false); + assert.equal(MapCameraPosition.from(unset).isZoomSpecified, false); + + const withZoom = unset.copy({ zoom: 0 }); + assert.equal(withZoom.isZoomSpecified, true); + assert.equal(withZoom.isBearingSpecified, false); +}); + +class TestMapViewState extends MapViewState { + mapDesignType = null; +} + +test('MapViewState fills unset properties from the current camera', () => { + const state = new TestMapViewState({ cameraPosition: current }); + + state.moveCameraTo(createMapCameraPosition({ position: osaka, bearing: 0 })); + assert.deepEqual( + [state.cameraPosition.zoom, state.cameraPosition.bearing, state.cameraPosition.tilt], + [12, 0, 45], + ); + + state.moveCameraTo(createMapCameraPosition({ position: osaka, zoom: 0 })); + assert.equal(state.cameraPosition.zoom, 0); +});