diff --git a/docs/api-reference/mapbox/types.md b/docs/api-reference/mapbox/types.md index 47d0b9634..668fe6ea2 100644 --- a/docs/api-reference/mapbox/types.md +++ b/docs/api-reference/mapbox/types.md @@ -168,6 +168,7 @@ An object with the following fields: - `zoom`: number - The zoom level. - `pitch`: number - The pitch (tilt) of the map, in degrees. - `bearing`: number - The bearing (rotation) of the map, in degrees. +- `elevation`: number|undefined - The map center elevation from sea leavel on terrain surface, if any ## Events diff --git a/examples/vite.config.local.js b/examples/vite.config.local.js index d6642c298..8ab680e10 100644 --- a/examples/vite.config.local.js +++ b/examples/vite.config.local.js @@ -9,8 +9,8 @@ export default defineConfig(async () => { resolve: { alias: { // Use root dependencies - 'react-map-gl/mapbox': join(rootDir, './modules/main/src/mapbox.ts'), - 'react-map-gl/maplibre': join(rootDir, './modules/main/src/maplibre.ts'), + 'react-map-gl/mapbox': join(rootDir, './modules/react-mapbox/src'), + 'react-map-gl/maplibre': join(rootDir, './modules/react-maplibre/src'), react: join(rootDir, './node_modules/react'), 'react-dom': join(rootDir, './node_modules/react-dom') } diff --git a/modules/react-mapbox/src/components/source.ts b/modules/react-mapbox/src/components/source.ts index 6dad8a8ab..7f9ea8b46 100644 --- a/modules/react-mapbox/src/components/source.ts +++ b/modules/react-mapbox/src/components/source.ts @@ -25,7 +25,6 @@ export type SourceProps = (SourceSpecification | CanvasSourceSpecification) & { let sourceCounter = 0; function createSource(map: MapInstance, id: string, props: SourceProps) { - // @ts-ignore if (map.isStyleLoaded()) { const options = {...props}; delete options.id; @@ -89,10 +88,12 @@ export function Source(props: SourceProps) { if (map) { /* global setTimeout */ const forceUpdate = () => setTimeout(() => setStyleLoaded(version => version + 1), 0); + map.on('load', forceUpdate); map.on('styledata', forceUpdate); forceUpdate(); return () => { + map.off('load', forceUpdate); map.off('styledata', forceUpdate); // @ts-ignore if (map.style && map.style._loaded && map.getSource(id)) { diff --git a/modules/react-mapbox/src/mapbox/mapbox.ts b/modules/react-mapbox/src/mapbox/mapbox.ts index bf7c87773..83af78999 100644 --- a/modules/react-mapbox/src/mapbox/mapbox.ts +++ b/modules/react-mapbox/src/mapbox/mapbox.ts @@ -1,9 +1,5 @@ -import { - transformToViewState, - applyViewStateToTransform, - cloneTransform, - syncProjection -} from '../utils/transform'; +import {transformToViewState, compareViewStateWithTransform} from '../utils/transform'; +import {ProxyTransform, createProxyTransform} from './proxy-transform'; import {normalizeStyle} from '../utils/style-utils'; import {deepEqual} from '../utils/deep-equal'; @@ -162,23 +158,27 @@ const handlerNames = [ */ export default class Mapbox { private _MapClass: {new (options: any): MapInstance}; - // mapboxgl.Map instance + /** mapboxgl.Map instance */ private _map: MapInstance = null; - // User-supplied props + /** User-supplied props */ props: MapboxProps; - // Mapbox map is stateful. - // During method calls/user interactions, map.transform is mutated and - // deviate from user-supplied props. - // In order to control the map reactively, we shadow the transform - // with the one below, which reflects the view state resolved from - // both user-supplied props and the underlying state - private _renderTransform: Transform; + /** The transform that replaces native map.transform to resolve changes vs. React props + * See proxy-transform.ts + */ + private _proxyTransform: ProxyTransform; // Internal states + /** Making updates driven by React props. Do not trigger React callbacks to avoid infinite loop */ private _internalUpdate: boolean = false; + /** Map is currently rendering */ private _inRender: boolean = false; + /** Map features under the pointer */ private _hoveredFeatures: MapGeoJSONFeature[] = null; + /** View state changes driven by React props + * They still need to fire move/etc. events because controls such as marker/popup + * subscribe to the move event internally to update their position + * React callbacks like onMove are not called for these */ private _deferredEvents: { move: boolean; zoom: boolean; @@ -208,7 +208,7 @@ export default class Mapbox { } get transform(): Transform { - return this._renderTransform; + return this._map.transform; } setProps(props: MapboxProps) { @@ -217,7 +217,7 @@ export default class Mapbox { const settingsChanged = this._updateSettings(props, oldProps); if (settingsChanged) { - this._createShadowTransform(this._map); + this._createProxyTransform(this._map); } const sizeChanged = this._updateSize(props); const viewStateChanged = this._updateViewState(props, true); @@ -318,13 +318,14 @@ export default class Mapbox { if (props.cursor) { map.getCanvas().style.cursor = props.cursor; } - this._createShadowTransform(map); + this._createProxyTransform(map); // Hack // Insert code into map's render cycle // eslint-disable-next-line @typescript-eslint/unbound-method const renderMap = map._render; map._render = (arg: number) => { + // Hijacked to set this state flag this._inRender = true; renderMap.call(map, arg); this._inRender = false; @@ -332,25 +333,25 @@ export default class Mapbox { // eslint-disable-next-line @typescript-eslint/unbound-method const runRenderTaskQueue = map._renderTaskQueue.run; map._renderTaskQueue.run = (arg: number) => { + // This is where camera updates from input handler/animation happens + // And where all view state change events are fired + this._proxyTransform.$internalUpdate = true; runRenderTaskQueue.call(map._renderTaskQueue, arg); - this._onBeforeRepaint(); + this._proxyTransform.$internalUpdate = false; + this._fireDefferedEvents(); }; - map.on('render', () => this._onAfterRepaint()); // Insert code into map's event pipeline // eslint-disable-next-line @typescript-eslint/unbound-method const fireEvent = map.fire; map.fire = this._fireEvent.bind(this, fireEvent); // add listeners - map.on('resize', () => { - this._renderTransform.resize(map.transform.width, map.transform.height); - }); map.on('styledata', () => { this._updateStyleComponents(this.props, {}); - // Projection can be set in stylesheet - syncProjection(map.transform, this._renderTransform); }); - map.on('sourcedata', () => this._updateStyleComponents(this.props, {})); + map.on('sourcedata', () => { + this._updateStyleComponents(this.props, {}); + }); for (const eventName in pointerEvents) { map.on(eventName, this._onPointerEvent); } @@ -396,11 +397,11 @@ export default class Mapbox { } } - _createShadowTransform(map: any) { - const renderTransform = cloneTransform(map.transform); - map.painter.transform = renderTransform; - - this._renderTransform = renderTransform; + _createProxyTransform(map: any) { + const proxyTransform = createProxyTransform(map.transform); + map.transform = proxyTransform; + map.painter.transform = proxyTransform; + this._proxyTransform = proxyTransform; } /* Trigger map resize if size is controlled @@ -427,28 +428,11 @@ export default class Mapbox { @returns {bool} true if anything is changed */ _updateViewState(nextProps: MapboxProps, triggerEvents: boolean): boolean { - if (this._internalUpdate) { - return false; - } - const map = this._map; - - const tr = this._renderTransform; - // Take a snapshot of the transform before mutation + const viewState: Partial = nextProps.viewState || nextProps; + const tr = this._proxyTransform; const {zoom, pitch, bearing} = tr; - const isMoving = map.isMoving(); - - if (isMoving) { - // All movement of the camera is done relative to the sea level - tr.cameraElevationReference = 'sea'; - } - const changed = applyViewStateToTransform(tr, { - ...transformToViewState(map.transform), - ...nextProps - }); - if (isMoving) { - // Reset camera reference - tr.cameraElevationReference = 'ground'; - } + const changed = compareViewStateWithTransform(this._proxyTransform, viewState); + tr.$reactViewState = viewState; if (changed && triggerEvents) { const deferredEvents = this._deferredEvents; @@ -459,12 +443,6 @@ export default class Mapbox { deferredEvents.pitch ||= pitch !== tr.pitch; } - // Avoid manipulating the real transform when interaction/animation is ongoing - // as it would interfere with Mapbox's handlers - if (!isMoving) { - applyViewStateToTransform(map.transform, nextProps); - } - return changed; } @@ -576,18 +554,14 @@ export default class Mapbox { private _queryRenderedFeatures(point: Point) { const map = this._map; - const tr = map.transform; const {interactiveLayerIds = []} = this.props; try { - map.transform = this._renderTransform; return map.queryRenderedFeatures(point, { layers: interactiveLayerIds.filter(map.getLayer.bind(map)) }); } catch { // May fail if style is not loaded return []; - } finally { - map.transform = tr; } } @@ -637,9 +611,14 @@ export default class Mapbox { if (!this._internalUpdate) { // @ts-ignore const cb = this.props[cameraEvents[e.type]]; + const tr = this._proxyTransform; if (cb) { + e.viewState = transformToViewState(tr.$proposedTransform ?? tr); cb(e); } + if (e.type === 'moveend') { + tr.$proposedTransform = null; + } } if (e.type in this._deferredEvents) { this._deferredEvents[e.type] = false; @@ -648,35 +627,23 @@ export default class Mapbox { _fireEvent(baseFire: Function, event: string | MapEvent, properties?: object) { const map = this._map; - const tr = map.transform; + const tr = this._proxyTransform; - const eventType = typeof event === 'string' ? event : event.type; - if (eventType === 'move') { - this._updateViewState(this.props, false); - } - if (eventType in cameraEvents) { - if (typeof event === 'object') { - (event as unknown as ViewStateChangeEvent).viewState = transformToViewState(tr); - } - if (this._map.isMoving()) { - // Replace map.transform with ours during the callbacks - map.transform = this._renderTransform; - baseFire.call(map, event, properties); - map.transform = tr; - - return map; - } + // Always expose the controlled transform to controls/end user + const internal = tr.$internalUpdate; + try { + tr.$internalUpdate = false; + baseFire.call(map, event, properties); + } finally { + tr.$internalUpdate = internal; } - baseFire.call(map, event, properties); return map; } - // All camera manipulations are complete, ready to repaint - _onBeforeRepaint() { + // If there are camera changes driven by props, invoke camera events so that DOM controls are synced + _fireDefferedEvents() { const map = this._map; - - // If there are camera changes driven by props, invoke camera events so that DOM controls are synced this._internalUpdate = true; for (const eventType in this._deferredEvents) { if (this._deferredEvents[eventType]) { @@ -684,21 +651,7 @@ export default class Mapbox { } } this._internalUpdate = false; - - const tr = this._map.transform; - // Make sure camera matches the current props - map.transform = this._renderTransform; - - this._onAfterRepaint = () => { - // Mapbox transitions between non-mercator projection and mercator during render time - // Copy it back to the other - syncProjection(this._renderTransform, tr); - // Restores camera state before render/load events are fired - map.transform = tr; - }; } - - _onAfterRepaint: () => void; } /** diff --git a/modules/react-mapbox/src/mapbox/proxy-transform.ts b/modules/react-mapbox/src/mapbox/proxy-transform.ts new file mode 100644 index 000000000..7dd94ed11 --- /dev/null +++ b/modules/react-mapbox/src/mapbox/proxy-transform.ts @@ -0,0 +1,150 @@ +import type {Transform} from '../types/internal'; +import type {ViewState, LngLat} from '../types/common'; +import {applyViewStateToTransform, isViewStateControlled} from '../utils/transform'; + +/** + * Mapbox map is stateful. + * During method calls/user interactions, map.transform is mutated and deviate from user-supplied props. + * In order to control the map reactively, we trap the transform mutations with a proxy, + * which reflects the view state resolved from both user-supplied props and the underlying state + */ +export type ProxyTransform = Transform & { + $internalUpdate: boolean; + $proposedTransform: Transform | null; + $reactViewState: Partial; +}; + +// These are Transform class methods that: +// + do not mutate any view state properties +// + populate private members derived from view state properties +// They should always reflect the state of their owning instance and NOT trigger any proxied getter/setter +const unproxiedMethods = new Set([ + '_calcMatrices', + '_calcFogMatrices', + '_updateCameraState', + '_updateSeaLevelZoom' +]); + +export function createProxyTransform(tr: Transform): ProxyTransform { + let internalUpdate = false; + let reactViewState: Partial = {}; + /** + * Reflects view state set by react props + * This is the transform seen by painter, style etc. + */ + const controlledTransform: Transform = tr; + /** Populated during camera move (handler/easeTo) if there is a discrepency between react props and proposed view state + * This is the transform seen by Mapbox's input handlers + */ + let proposedTransform: Transform | null = null; + + const handlers: ProxyHandler = { + get(target: Transform, prop: string) { + // Props added by us + if (prop === '$reactViewState') { + return reactViewState; + } + if (prop === '$proposedTransform') { + return proposedTransform; + } + if (prop === '$internalUpdate') { + return internalUpdate; + } + + // Ugly - this method is called from HandlerManager bypassing zoom setter + if (prop === '_setZoom') { + return (z: number) => { + if (internalUpdate) { + proposedTransform?.[prop](z); + } + if (!Number.isFinite(reactViewState.zoom)) { + controlledTransform[prop](z); + } + }; + } + + // Ugly - this method is called from HandlerManager and mutates transform._camera + if ( + internalUpdate && + prop === '_translateCameraConstrained' && + isViewStateControlled(reactViewState) + ) { + proposedTransform = proposedTransform || controlledTransform.clone(); + } + + if (unproxiedMethods.has(prop)) { + // When this function is executed, it updates both transforms respectively + return function (...parms: unknown[]) { + proposedTransform?.[prop](...parms); + controlledTransform[prop](...parms); + }; + } + + // Expose the proposed transform to input handlers + if (internalUpdate && proposedTransform) { + return proposedTransform[prop]; + } + + // Expose the controlled transform to renderer, markers, and event listeners + return controlledTransform[prop]; + }, + + set(target: Transform, prop: string, value: unknown) { + // Props added by us + if (prop === '$reactViewState') { + reactViewState = value as Partial; + applyViewStateToTransform(controlledTransform, reactViewState); + return true; + } + if (prop === '$proposedTransform') { + proposedTransform = value as Transform; + return true; + } + if (prop === '$internalUpdate') { + internalUpdate = value as boolean; + return true; + } + + // Controlled props + let controlledValue = value; + if (prop === 'center' || prop === '_center') { + if (Number.isFinite(reactViewState.longitude) || Number.isFinite(reactViewState.latitude)) { + // @ts-expect-error LngLat constructor is not typed + controlledValue = new value.constructor( + reactViewState.longitude ?? (value as LngLat).lng, + reactViewState.latitude ?? (value as LngLat).lat + ); + } + } else if (prop === 'zoom' || prop === '_zoom' || prop === '_seaLevelZoom') { + if (Number.isFinite(reactViewState.zoom)) { + controlledValue = controlledTransform[prop]; + } + } else if (prop === '_centerAltitude') { + if (Number.isFinite(reactViewState.elevation)) { + controlledValue = controlledTransform[prop]; + } + } else if (prop === 'pitch' || prop === '_pitch') { + if (Number.isFinite(reactViewState.pitch)) { + controlledValue = controlledTransform[prop]; + } + } else if (prop === 'bearing' || prop === 'rotation' || prop === 'angle') { + if (Number.isFinite(reactViewState.bearing)) { + controlledValue = controlledTransform[prop]; + } + } + + // During camera update, we save view states that are overriden by controlled values in proposedTransform + if (internalUpdate && controlledValue !== value) { + proposedTransform = proposedTransform || controlledTransform.clone(); + } + if (internalUpdate && proposedTransform) { + proposedTransform[prop] = value; + } + + // controlledTransform is not exposed to view state mutation + controlledTransform[prop] = controlledValue; + return true; + } + }; + return new Proxy(tr, handlers) as ProxyTransform; +} diff --git a/modules/react-mapbox/src/types/common.ts b/modules/react-mapbox/src/types/common.ts index 19245cf81..3dea80df1 100644 --- a/modules/react-mapbox/src/types/common.ts +++ b/modules/react-mapbox/src/types/common.ts @@ -27,6 +27,8 @@ export type ViewState = { pitch: number; /** Dimensions in pixels applied on each side of the viewport for shifting the vanishing point. */ padding: PaddingOptions; + /** Center elevation on terrain */ + elevation?: number; }; export interface ImmutableLike { diff --git a/modules/react-mapbox/src/utils/transform.ts b/modules/react-mapbox/src/utils/transform.ts index e9e8c962a..778224474 100644 --- a/modules/react-mapbox/src/utils/transform.ts +++ b/modules/react-mapbox/src/utils/transform.ts @@ -1,35 +1,5 @@ -import type {MapboxProps} from '../mapbox/mapbox'; import type {ViewState} from '../types/common'; import type {Transform} from '../types/internal'; -import {deepEqual} from './deep-equal'; - -/** - * Make a copy of a transform - * @param tr - */ -export function cloneTransform(tr: Transform): Transform { - const newTransform = tr.clone(); - // Work around mapbox bug - this value is not assigned in clone(), only in resize() - newTransform.pixelsToGLUnits = tr.pixelsToGLUnits; - return newTransform; -} - -/** - * Copy projection from one transform to another. This only applies to mapbox-gl transforms - * @param src the transform to copy projection settings from - * @param dest to transform to copy projection settings to - */ -export function syncProjection(src: Transform, dest: Transform): void { - if (!src.getProjection) { - return; - } - const srcProjection = src.getProjection(); - const destProjection = dest.getProjection(); - - if (!deepEqual(srcProjection, destProjection)) { - dest.setProjection(srcProjection); - } -} /** * Capture a transform's current state @@ -40,48 +10,101 @@ export function transformToViewState(tr: Transform): ViewState { return { longitude: tr.center.lng, latitude: tr.center.lat, - zoom: tr.zoom, + zoom: tr._seaLevelZoom ?? tr.zoom, pitch: tr.pitch, bearing: tr.bearing, - padding: tr.padding + padding: tr.padding, + elevation: tr._centerAltitude }; } +/** Returns `true` if the given props can potentially override view state updates */ +export function isViewStateControlled(v: Partial): boolean { + return ( + Number.isFinite(v.longitude) || + Number.isFinite(v.latitude) || + Number.isFinite(v.zoom) || + Number.isFinite(v.pitch) || + Number.isFinite(v.bearing) + ); +} + +/** + * Returns `true` if transform needs to be updated to match view state + */ +export function compareViewStateWithTransform(tr: Transform, v: Partial): boolean { + if (Number.isFinite(v.longitude) && tr.center.lng !== v.longitude) { + return true; + } + if (Number.isFinite(v.latitude) && tr.center.lat !== v.latitude) { + return true; + } + if (Number.isFinite(v.bearing) && tr.bearing !== v.bearing) { + return true; + } + if (Number.isFinite(v.pitch) && tr.pitch !== v.pitch) { + return true; + } + if (Number.isFinite(v.zoom) && (tr._seaLevelZoom ?? tr.zoom) !== v.zoom) { + return true; + } + if (v.padding && !tr.isPaddingEqual(v.padding)) { + return true; + } + return false; +} + +function noOp() {} + /* eslint-disable complexity */ /** - * Mutate a transform to match the given view state + * Mutate a transform to match the given view state. Should reverse `transformToViewState` * @param transform * @param viewState - * @returns true if the transform has changed */ -export function applyViewStateToTransform(tr: Transform, props: MapboxProps): boolean { - const v: Partial = props.viewState || props; - let changed = false; +export function applyViewStateToTransform(tr: Transform, v: Partial) { + // prevent constrain from running until all properties are set + // eslint-disable-next-line @typescript-eslint/unbound-method + const constrain = tr._constrain; + // eslint-disable-next-line @typescript-eslint/unbound-method + const calcMatrices = tr._calcMatrices; + tr._constrain = noOp; + tr._calcMatrices = noOp; - if ('zoom' in v) { - const zoom = tr.zoom; - tr.zoom = v.zoom; - changed = changed || zoom !== tr.zoom; - } - if ('bearing' in v) { - const bearing = tr.bearing; + if (Number.isFinite(v.bearing)) { tr.bearing = v.bearing; - changed = changed || bearing !== tr.bearing; } - if ('pitch' in v) { - const pitch = tr.pitch; + if (Number.isFinite(v.pitch)) { tr.pitch = v.pitch; - changed = changed || pitch !== tr.pitch; } if (v.padding && !tr.isPaddingEqual(v.padding)) { - changed = true; tr.padding = v.padding; } - if ('longitude' in v && 'latitude' in v) { + if (Number.isFinite(v.longitude) || Number.isFinite(v.latitude)) { const center = tr.center; - // @ts-ignore - tr.center = new center.constructor(v.longitude, v.latitude); - changed = changed || center !== tr.center; + // @ts-expect-error LngLat constructor is not typed + tr._center = new center.constructor(v.longitude ?? center.lng, v.latitude ?? center.lat); + } + if (Number.isFinite(v.zoom)) { + tr._centerAltitude = v.elevation ?? 0; + if (tr.elevation) { + tr._seaLevelZoom = v.zoom; + const mercatorElevation = (tr.pixelsPerMeter / tr.worldSize) * tr._centerAltitude; + const altitude = tr._mercatorZfromZoom(v.zoom); + const minHeight = tr._mercatorZfromZoom(tr._maxZoom); + const height = Math.max(altitude - mercatorElevation, minHeight); + tr._setZoom(tr._zoomFromMercatorZ(height)); + } else { + tr._seaLevelZoom = null; + tr.zoom = v.zoom; + } + } + + // restore methods + tr._constrain = constrain; + tr._calcMatrices = calcMatrices; + if (!tr._unmodified) { + tr._constrain(); + tr._calcMatrices(); } - return changed; } diff --git a/modules/react-mapbox/test/utils/mapbox-gl-mock/transform.js b/modules/react-mapbox/test/utils/mapbox-gl-mock/transform.js index 593ecd7d3..2de043b86 100644 --- a/modules/react-mapbox/test/utils/mapbox-gl-mock/transform.js +++ b/modules/react-mapbox/test/utils/mapbox-gl-mock/transform.js @@ -20,6 +20,7 @@ export default class Transform { this.angle = 0; this._pitch = 0; this._edgeInsets = new EdgeInsets(); + this._centerAltitude = 0; } get bearing() { @@ -88,4 +89,8 @@ export default class Transform { isPaddingEqual(padding) { return this._edgeInsets.equals(padding); } + + _constrain() {} + + _calcMatrices() {} } diff --git a/modules/react-mapbox/test/utils/transform.spec.js b/modules/react-mapbox/test/utils/transform.spec.js index b81941cd6..142072d52 100644 --- a/modules/react-mapbox/test/utils/transform.spec.js +++ b/modules/react-mapbox/test/utils/transform.spec.js @@ -1,6 +1,7 @@ import test from 'tape-promise/tape'; import { transformToViewState, + compareViewStateWithTransform, applyViewStateToTransform } from '@vis.gl/react-mapbox/utils/transform'; @@ -8,11 +9,14 @@ import Transform from './mapbox-gl-mock/transform'; test('applyViewStateToTransform', t => { const tr = new Transform(); - - let changed = applyViewStateToTransform(tr, {}); + let viewState = {}; + let changed = compareViewStateWithTransform(tr, viewState); + applyViewStateToTransform(tr, viewState); t.notOk(changed, 'empty view state'); - changed = applyViewStateToTransform(tr, {longitude: -10, latitude: 5}); + viewState = {longitude: -10, latitude: 5}; + changed = compareViewStateWithTransform(tr, viewState); + applyViewStateToTransform(tr, viewState); t.ok(changed, 'center changed'); t.deepEqual( transformToViewState(tr), @@ -22,15 +26,15 @@ test('applyViewStateToTransform', t => { zoom: 0, pitch: 0, bearing: 0, - padding: {left: 0, right: 0, top: 0, bottom: 0} + padding: {left: 0, right: 0, top: 0, bottom: 0}, + elevation: 0 }, 'view state is correct' ); - changed = applyViewStateToTransform(tr, {zoom: -1}); - t.notOk(changed, 'zoom is clamped'); - - changed = applyViewStateToTransform(tr, {zoom: 10}); + viewState = {zoom: 10}; + changed = compareViewStateWithTransform(tr, viewState); + applyViewStateToTransform(tr, viewState); t.ok(changed, 'zoom changed'); t.deepEqual( transformToViewState(tr), @@ -40,12 +44,15 @@ test('applyViewStateToTransform', t => { zoom: 10, pitch: 0, bearing: 0, - padding: {left: 0, right: 0, top: 0, bottom: 0} + padding: {left: 0, right: 0, top: 0, bottom: 0}, + elevation: 0 }, 'view state is correct' ); - changed = applyViewStateToTransform(tr, {pitch: 30}); + viewState = {pitch: 30}; + changed = compareViewStateWithTransform(tr, viewState); + applyViewStateToTransform(tr, viewState); t.ok(changed, 'pitch changed'); t.deepEqual( transformToViewState(tr), @@ -55,12 +62,15 @@ test('applyViewStateToTransform', t => { zoom: 10, pitch: 30, bearing: 0, - padding: {left: 0, right: 0, top: 0, bottom: 0} + padding: {left: 0, right: 0, top: 0, bottom: 0}, + elevation: 0 }, 'view state is correct' ); - changed = applyViewStateToTransform(tr, {bearing: 270}); + viewState = {bearing: 270}; + changed = compareViewStateWithTransform(tr, viewState); + applyViewStateToTransform(tr, viewState); t.ok(changed, 'bearing changed'); t.deepEqual( transformToViewState(tr), @@ -70,12 +80,15 @@ test('applyViewStateToTransform', t => { zoom: 10, pitch: 30, bearing: -90, - padding: {left: 0, right: 0, top: 0, bottom: 0} + padding: {left: 0, right: 0, top: 0, bottom: 0}, + elevation: 0 }, 'view state is correct' ); - changed = applyViewStateToTransform(tr, {padding: {left: 10, right: 10, top: 10, bottom: 10}}); + viewState = {padding: {left: 10, right: 10, top: 10, bottom: 10}}; + changed = compareViewStateWithTransform(tr, viewState); + applyViewStateToTransform(tr, viewState); t.ok(changed, 'padding changed'); t.deepEqual( transformToViewState(tr), @@ -85,16 +98,22 @@ test('applyViewStateToTransform', t => { zoom: 10, pitch: 30, bearing: -90, - padding: {left: 10, right: 10, top: 10, bottom: 10} + padding: {left: 10, right: 10, top: 10, bottom: 10}, + elevation: 0 }, 'view state is correct' ); - changed = applyViewStateToTransform(tr, {viewState: {pitch: 30}}); + viewState = {pitch: 30}; + changed = compareViewStateWithTransform(tr, viewState); + applyViewStateToTransform(tr, viewState); t.notOk(changed, 'nothing changed'); applyViewStateToTransform(tr, {longitude: 0, latitude: 0, zoom: 0}); - changed = applyViewStateToTransform(tr, {longitude: 12, latitude: 34, zoom: 15}); + + viewState = {longitude: 12, latitude: 34, zoom: 15}; + changed = compareViewStateWithTransform(tr, viewState); + applyViewStateToTransform(tr, viewState); t.ok(changed, 'center and zoom changed'); t.equal(tr.zoom, 15, 'zoom is correct'); t.equal(tr.center.lat, 34, 'center latitude is correct');