Skip to content
Merged
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
28 changes: 25 additions & 3 deletions api/js-sdk-core.api.d.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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.
*/
Expand All @@ -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).
Expand All @@ -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 {
Expand Down
6 changes: 3 additions & 3 deletions src/map/MapViewState.ts
Original file line number Diff line number Diff line change
Expand Up @@ -187,11 +187,11 @@ export abstract class MapViewState<ActualMapDesignType extends MapDesignTypeInte
moveCameraTo(cameraPosition: MapCameraPosition, durationMillis?: number): void;
moveCameraTo(position: GeoPoint, durationMillis?: number): void;
moveCameraTo(positionOrCamera: GeoPoint | MapCameraPosition, durationMillis?: number): void {
// カメラを渡されたらそのまま使う。「位置だけ動かして縮尺は保つ」は
// `moveCameraTo(position: GeoPoint)` のオーバーロードが担う(下の行)。
// 明示されたプロパティは 0 でもそのまま使い、未指定のものだけ現在のカメラから
// 引き継ぐ。`createMapCameraPosition({ position })` なら位置だけが動く。
const next =
'zoom' in positionOrCamera
? (positionOrCamera as MapCameraPosition)
? (positionOrCamera as MapCameraPosition).resolvedOnto(this._cameraPosition)
: this._cameraPosition.copy({ position: positionOrCamera as GeoPoint });

const controller = this.attachedMapController;
Expand Down
70 changes: 59 additions & 11 deletions src/types/MapCamera.ts
Original file line number Diff line number Diff line change
Expand Up @@ -47,7 +47,12 @@ export 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).
Expand All @@ -62,8 +67,25 @@ export 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;
}

/**
Expand Down Expand Up @@ -102,12 +124,18 @@ export type MapCameraPositionInterface = {
*/
export function createMapCameraPosition({
position,
zoom = 0,
bearing = 0,
tilt = 0,
zoom: givenZoom,
bearing: givenBearing,
tilt: givenTilt,
paddings = MapPaddings.Zeros,
visibleRegion = null,
}: MapCameraPositionInterface): MapCameraPosition {
const isZoomSpecified = givenZoom != null;
const isBearingSpecified = givenBearing != null;
const isTiltSpecified = givenTilt != null;
const zoom = givenZoom ?? 0;
const bearing = givenBearing ?? 0;
const tilt = givenTilt ?? 0;
const normalizedPosition = fromGeoPoint(position);
const normalizedPaddings = paddings == null ? null : MapPaddings.from(paddings);

Expand All @@ -131,6 +159,9 @@ export function createMapCameraPosition({
return visibleRegion;
},
paddings: normalizedPaddings,
isZoomSpecified,
isBearingSpecified,
isTiltSpecified,
equals(other: MapCameraPosition | null, tolerance: number = 1e-2): boolean {
if (!other) return false;
return (
Expand All @@ -143,13 +174,26 @@ export function createMapCameraPosition({
copy(partial: MapCameraPositionCopyParams = {}): MapCameraPosition {
return createMapCameraPosition({
position: partial.position ?? partial.center ?? normalizedPosition,
zoom: partial.zoom ?? zoom,
bearing: partial.bearing ?? bearing,
tilt: partial.tilt ?? tilt,
zoom: partial.zoom ?? givenZoom,
bearing: partial.bearing ?? givenBearing,
tilt: partial.tilt ?? givenTilt,
paddings: partial.paddings ?? normalizedPaddings,
visibleRegion: partial.visibleRegion ?? visibleRegion,
});
},
resolvedOnto(current: MapCameraPosition): MapCameraPosition {
if (isZoomSpecified && isBearingSpecified && isTiltSpecified) {
return this;
}
return createMapCameraPosition({
position: normalizedPosition,
zoom: isZoomSpecified ? zoom : current.zoom,
bearing: isBearingSpecified ? bearing : current.bearing,
tilt: isTiltSpecified ? tilt : current.tilt,
paddings: normalizedPaddings,
visibleRegion,
});
},
};
}

Expand All @@ -165,12 +209,16 @@ export namespace MapCameraPosition {
bearing: 0,
});

export function from(other: MapCameraPositionInterface): MapCameraPosition {
export function from(
other: MapCameraPositionInterface | MapCameraPosition,
): MapCameraPosition {
// A MapCameraPosition reads 0 for an unset property; keep it unset.
const camera = other as Partial<MapCameraPosition>;
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,
});
Expand Down
75 changes: 75 additions & 0 deletions test/CameraPositionResolve.test.mjs
Original file line number Diff line number Diff line change
@@ -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);
});