From fe6d0069203bf521a794723567649dfc40ce42b6 Mon Sep 17 00:00:00 2001 From: Auspicus Date: Tue, 23 Dec 2025 16:43:34 +1100 Subject: [PATCH 1/7] Add updateZoomConstraint helper --- .../react-maplibre/src/maplibre/maplibre.ts | 22 ++++-- modules/react-maplibre/src/utils/transform.ts | 33 ++++++++ .../test/utils/transform.spec.js | 75 ++++++++++++++++++- 3 files changed, 123 insertions(+), 7 deletions(-) diff --git a/modules/react-maplibre/src/maplibre/maplibre.ts b/modules/react-maplibre/src/maplibre/maplibre.ts index 4ef1122ad..e81339a25 100644 --- a/modules/react-maplibre/src/maplibre/maplibre.ts +++ b/modules/react-maplibre/src/maplibre/maplibre.ts @@ -1,4 +1,4 @@ -import {transformToViewState, applyViewStateToTransform} from '../utils/transform'; +import {transformToViewState, applyViewStateToTransform, updateZoomConstraint} from '../utils/transform'; import {normalizeStyle} from '../utils/style-utils'; import {deepEqual} from '../utils/deep-equal'; @@ -146,7 +146,7 @@ const settingNames = [ 'maxBounds', 'projection', 'renderWorldCopies' -]; +] as const; const handlerNames = [ 'scrollZoom', 'boxZoom', @@ -427,9 +427,21 @@ export default class Maplibre { if (propPresent && !deepEqual(nextProps[propName], currProps[propName])) { changed = true; - const nextValue = propName in nextProps ? nextProps[propName] : DEFAULT_SETTINGS[propName]; - const setter = map[`set${propName[0].toUpperCase()}${propName.slice(1)}`]; - setter?.call(map, nextValue); + if (propName === 'minZoom' || propName === 'maxZoom') { + const next = { + min: 'minZoom' in nextProps ? nextProps.minZoom as number : DEFAULT_SETTINGS.minZoom, + max: 'maxZoom' in nextProps ? nextProps.maxZoom as number : DEFAULT_SETTINGS.maxZoom + } + const curr = { + min: 'minZoom' in currProps ? currProps.minZoom as number : DEFAULT_SETTINGS.minZoom, + max: 'maxZoom' in currProps ? currProps.maxZoom as number : DEFAULT_SETTINGS.maxZoom + } + updateZoomConstraint(map, next, curr) + } else { + const nextValue = propName in nextProps ? nextProps[propName] : DEFAULT_SETTINGS[propName]; + const setter = map[`set${propName[0].toUpperCase()}${propName.slice(1)}`]; + setter?.call(map, nextValue); + } } } return changed; diff --git a/modules/react-maplibre/src/utils/transform.ts b/modules/react-maplibre/src/utils/transform.ts index bd7f74880..e8f8ee095 100644 --- a/modules/react-maplibre/src/utils/transform.ts +++ b/modules/react-maplibre/src/utils/transform.ts @@ -1,6 +1,7 @@ import type {MaplibreProps} from '../maplibre/maplibre'; import type {ViewState} from '../types/common'; import type {TransformLike} from '../types/internal'; +import { MapInstance } from '../types/lib'; import {deepEqual} from './deep-equal'; /** @@ -56,3 +57,35 @@ export function applyViewStateToTransform( } return changes; } + +/** + * Update zoom constraints to match props by calling + * `setMinZoom` and `setMaxZoom` in the right order + * @param {object} nextRange + * @param {object} currRange + **/ +export function updateZoomConstraint(map: MapInstance, nextRange: { min: number; max: number}, currentRange: { min: number; max: number }): void { + if (nextRange.min === currentRange.min && nextRange.max === currentRange.max) { + return + } + + // if moving up ie. 1 - 3 -> 5 - 10 + if (nextRange.min >= currentRange.min) { + if (nextRange.max !== currentRange.max) { + map.setMaxZoom(nextRange.max) + } + if (nextRange.min !== currentRange.min) { + map.setMinZoom(nextRange.min) + } + } + + // if moving down ie. 5 - 10 -> 1 - 3 + if (nextRange.min < currentRange.min) { + if (nextRange.min !== currentRange.min) { + map.setMinZoom(nextRange.min) + } + if (nextRange.max !== currentRange.max) { + map.setMaxZoom(nextRange.max) + } + } + } \ No newline at end of file diff --git a/modules/react-maplibre/test/utils/transform.spec.js b/modules/react-maplibre/test/utils/transform.spec.js index 25e575540..a686cd052 100644 --- a/modules/react-maplibre/test/utils/transform.spec.js +++ b/modules/react-maplibre/test/utils/transform.spec.js @@ -1,7 +1,8 @@ -import test from 'tape-promise/tape'; +import test from 'tape'; import { transformToViewState, - applyViewStateToTransform + applyViewStateToTransform, + updateZoomConstraint } from '@vis.gl/react-maplibre/utils/transform'; import maplibregl from 'maplibre-gl'; @@ -64,3 +65,73 @@ test('applyViewStateToTransform', t => { t.end(); }); + +test('updateZoomConstraint', t => { + let first = null + let currentMinZoom = 0 + let currentMaxZoom = 0 + const map = { + setMinZoom: (nextMinZoom) => { + if (nextMinZoom > currentMaxZoom) { + throw new Error('Setting minZoom > maxZoom') + } + currentMinZoom = nextMinZoom + if (!first) { + first = 'min' + } + }, + setMaxZoom: (nextMaxZoom) => { + if (nextMaxZoom < currentMinZoom) { + throw new Error('Setting maxZoom < minZoom') + } + currentMaxZoom = nextMaxZoom + if (!first) { + first = 'max' + } + } + } + + // moving down. 5 - 10 -> 1 - 3 + currentMinZoom = 5 + currentMaxZoom = 10 + updateZoomConstraint(map, { min: 1, max: 3 }, { min: 5, max: 10 }); + t.equal(first, 'min', 'min first') + first = null + + // moving up. 1 - 3 -> 5 - 10 + currentMinZoom = 1 + currentMaxZoom = 3 + updateZoomConstraint(map, { min: 5, max: 10 }, { min: 1, max: 3 }); + t.equal(first, 'max', 'max first') + first = null + + // expanding. 5 - 18 -> 3 - 22 + currentMinZoom = 5 + currentMaxZoom = 18 + updateZoomConstraint(map, { min: 3, max: 22 }, { min: 5, max: 18 }); + t.equal(first, 'min', 'min first') + first = null + + // expanding down. 5 - 18 -> 3 - 18 + currentMinZoom = 5 + currentMaxZoom = 18 + updateZoomConstraint(map, { min: 3, max: 18 }, { min: 5, max: 18 }); + t.equal(first, 'min', 'min first') + first = null + + // contracting. 3 - 22 -> 5 - 18 + currentMinZoom = 5 + currentMaxZoom = 18 + updateZoomConstraint(map, { min: 5, max: 18 }, { min: 3, max: 22 }); + t.equal(first, 'max', 'max first') + first = null + + // contracting down. 12 - 22 -> 5 - 10 + currentMinZoom = 12 + currentMaxZoom = 22 + updateZoomConstraint(map, { min: 5, max: 10 }, { min: 12, max: 22 }); + t.equal(first, 'min', 'min first') + first = null + + t.end(); +}); From 310aa49721d8840c30de64c619a002b11fb13c7e Mon Sep 17 00:00:00 2001 From: Auspicus Date: Wed, 24 Dec 2025 15:31:08 +1100 Subject: [PATCH 2/7] Fix types, prevent duplicate call to setMinZoom or setMaxZoom --- .../react-maplibre/src/maplibre/maplibre.ts | 77 +++++++++++++------ 1 file changed, 55 insertions(+), 22 deletions(-) diff --git a/modules/react-maplibre/src/maplibre/maplibre.ts b/modules/react-maplibre/src/maplibre/maplibre.ts index e81339a25..54d76abec 100644 --- a/modules/react-maplibre/src/maplibre/maplibre.ts +++ b/modules/react-maplibre/src/maplibre/maplibre.ts @@ -76,7 +76,32 @@ export type MaplibreProps = Partial & interactiveLayerIds?: string[]; /** CSS cursor */ cursor?: string; - }; + + /** Minimum zoom available to the map. + * @default 0 + */ + minZoom?: number + /** Maximum zoom available to the map. + * @default 22 + */ + maxZoom?: number + /** Minimum pitch available to the map. + * @default 0 + */ + minPitch?: number + /** Maximum pitch available to the map. + * @default 85 + */ + maxPitch?: number + /** Bounds of the map. + * @default [-180, -85.051129, 180, 85.051129] + */ + maxBounds?: [number, number, number, number] + /** Whether to render copies of the world or not. + * @default true + */ + renderWorldCopies?: boolean + } const DEFAULT_STYLE = {version: 8, sources: {}, layers: []} as StyleSpecification; @@ -414,6 +439,22 @@ export default class Maplibre { return false; } + private _updateZoomConstraint(nextProps: MaplibreProps, currProps: MaplibreProps): boolean { + if (!('minZoom' in nextProps) && !('maxZoom' in nextProps)) { + return false + } + + updateZoomConstraint(this._map, { + min: nextProps.minZoom ?? DEFAULT_SETTINGS.minZoom, + max: nextProps.maxZoom ?? DEFAULT_SETTINGS.maxZoom + }, { + min: currProps.minZoom ?? DEFAULT_SETTINGS.minZoom, + max: currProps.maxZoom ?? DEFAULT_SETTINGS.maxZoom, + }) + + return true + } + /* Update camera constraints and projection settings to match props @param {object} nextProps @param {object} currProps @@ -421,30 +462,22 @@ export default class Maplibre { */ private _updateSettings(nextProps: MaplibreProps, currProps: MaplibreProps): boolean { const map = this._map; - let changed = false; + let settingsChanged = false; for (const propName of settingNames) { - const propPresent = propName in nextProps || propName in currProps; - - if (propPresent && !deepEqual(nextProps[propName], currProps[propName])) { - changed = true; - if (propName === 'minZoom' || propName === 'maxZoom') { - const next = { - min: 'minZoom' in nextProps ? nextProps.minZoom as number : DEFAULT_SETTINGS.minZoom, - max: 'maxZoom' in nextProps ? nextProps.maxZoom as number : DEFAULT_SETTINGS.maxZoom - } - const curr = { - min: 'minZoom' in currProps ? currProps.minZoom as number : DEFAULT_SETTINGS.minZoom, - max: 'maxZoom' in currProps ? currProps.maxZoom as number : DEFAULT_SETTINGS.maxZoom - } - updateZoomConstraint(map, next, curr) - } else { - const nextValue = propName in nextProps ? nextProps[propName] : DEFAULT_SETTINGS[propName]; - const setter = map[`set${propName[0].toUpperCase()}${propName.slice(1)}`]; - setter?.call(map, nextValue); - } + if (propName === 'minZoom' || propName === 'maxZoom') { + // eslint-disable-next-line no-continue + continue + } + + if (propName in nextProps && !deepEqual(nextProps[propName], currProps[propName])) { + settingsChanged = true; + const nextValue = propName in nextProps ? nextProps[propName] : DEFAULT_SETTINGS[propName] + const setter = map[`set${propName[0].toUpperCase()}${propName.slice(1)}`]; + setter?.call(map, nextValue); } } - return changed; + const zoomChanged = this._updateZoomConstraint(nextProps, currProps) + return settingsChanged || zoomChanged; } /* Update map style to match props */ From e4931200a2ca23d6fc8b82094cf828250848e8cd Mon Sep 17 00:00:00 2001 From: Auspicus Date: Tue, 20 Jan 2026 23:36:19 +1100 Subject: [PATCH 3/7] Add the same logic for pitch, format code --- .../react-maplibre/src/maplibre/maplibre.ts | 94 +++++++++------- modules/react-maplibre/src/utils/transform.ts | 84 ++++++++++---- .../test/utils/transform.spec.js | 103 ++++++++++++++---- 3 files changed, 197 insertions(+), 84 deletions(-) diff --git a/modules/react-maplibre/src/maplibre/maplibre.ts b/modules/react-maplibre/src/maplibre/maplibre.ts index 54d76abec..6bfe1e1f1 100644 --- a/modules/react-maplibre/src/maplibre/maplibre.ts +++ b/modules/react-maplibre/src/maplibre/maplibre.ts @@ -1,4 +1,9 @@ -import {transformToViewState, applyViewStateToTransform, updateZoomConstraint} from '../utils/transform'; +import { + transformToViewState, + applyViewStateToTransform, + updateZoomConstraint, + updatePitchConstraint +} from '../utils/transform'; import {normalizeStyle} from '../utils/style-utils'; import {deepEqual} from '../utils/deep-equal'; @@ -80,28 +85,28 @@ export type MaplibreProps = Partial & /** Minimum zoom available to the map. * @default 0 */ - minZoom?: number + minZoom?: number; /** Maximum zoom available to the map. * @default 22 - */ - maxZoom?: number + */ + maxZoom?: number; /** Minimum pitch available to the map. * @default 0 - */ - minPitch?: number + */ + minPitch?: number; /** Maximum pitch available to the map. * @default 85 - */ - maxPitch?: number + */ + maxPitch?: number; /** Bounds of the map. * @default [-180, -85.051129, 180, 85.051129] - */ - maxBounds?: [number, number, number, number] + */ + maxBounds?: [number, number, number, number]; /** Whether to render copies of the world or not. * @default true - */ - renderWorldCopies?: boolean - } + */ + renderWorldCopies?: boolean; + }; const DEFAULT_STYLE = {version: 8, sources: {}, layers: []} as StyleSpecification; @@ -163,15 +168,8 @@ const otherEvents = { sourcedata: 'onSourceData', error: 'onError' }; -const settingNames = [ - 'minZoom', - 'maxZoom', - 'minPitch', - 'maxPitch', - 'maxBounds', - 'projection', - 'renderWorldCopies' -] as const; +const constraintNames = ['minZoom', 'maxZoom', 'minPitch', 'maxPitch'] as const; +const settingNames = [...constraintNames, 'maxBounds', 'projection', 'renderWorldCopies'] as const; const handlerNames = [ 'scrollZoom', 'boxZoom', @@ -439,20 +437,36 @@ export default class Maplibre { return false; } - private _updateZoomConstraint(nextProps: MaplibreProps, currProps: MaplibreProps): boolean { - if (!('minZoom' in nextProps) && !('maxZoom' in nextProps)) { - return false - } - - updateZoomConstraint(this._map, { - min: nextProps.minZoom ?? DEFAULT_SETTINGS.minZoom, - max: nextProps.maxZoom ?? DEFAULT_SETTINGS.maxZoom - }, { - min: currProps.minZoom ?? DEFAULT_SETTINGS.minZoom, - max: currProps.maxZoom ?? DEFAULT_SETTINGS.maxZoom, - }) + /* Update camera constraints to match props + @param {object} nextProps + @param {object} currProps + @returns {bool} true if anything is changed + */ + private _updateConstraints(nextProps: MaplibreProps, currProps: MaplibreProps): boolean { + updateZoomConstraint( + this._map, + { + min: nextProps.minZoom ?? DEFAULT_SETTINGS.minZoom, + max: nextProps.maxZoom ?? DEFAULT_SETTINGS.maxZoom + }, + { + min: currProps.minZoom ?? DEFAULT_SETTINGS.minZoom, + max: currProps.maxZoom ?? DEFAULT_SETTINGS.maxZoom + } + ); + updatePitchConstraint( + this._map, + { + min: nextProps.minPitch ?? DEFAULT_SETTINGS.minPitch, + max: nextProps.maxPitch ?? DEFAULT_SETTINGS.maxPitch + }, + { + min: currProps.minPitch ?? DEFAULT_SETTINGS.minPitch, + max: currProps.maxPitch ?? DEFAULT_SETTINGS.maxPitch + } + ); - return true + return true; } /* Update camera constraints and projection settings to match props @@ -464,20 +478,20 @@ export default class Maplibre { const map = this._map; let settingsChanged = false; for (const propName of settingNames) { - if (propName === 'minZoom' || propName === 'maxZoom') { + if (constraintNames.includes(propName as (typeof constraintNames)[number])) { // eslint-disable-next-line no-continue - continue + continue; } if (propName in nextProps && !deepEqual(nextProps[propName], currProps[propName])) { settingsChanged = true; - const nextValue = propName in nextProps ? nextProps[propName] : DEFAULT_SETTINGS[propName] + const nextValue = propName in nextProps ? nextProps[propName] : DEFAULT_SETTINGS[propName]; const setter = map[`set${propName[0].toUpperCase()}${propName.slice(1)}`]; setter?.call(map, nextValue); } } - const zoomChanged = this._updateZoomConstraint(nextProps, currProps) - return settingsChanged || zoomChanged; + const constraintsChanged = this._updateConstraints(nextProps, currProps); + return settingsChanged || constraintsChanged; } /* Update map style to match props */ diff --git a/modules/react-maplibre/src/utils/transform.ts b/modules/react-maplibre/src/utils/transform.ts index e8f8ee095..b44823d3e 100644 --- a/modules/react-maplibre/src/utils/transform.ts +++ b/modules/react-maplibre/src/utils/transform.ts @@ -1,7 +1,7 @@ import type {MaplibreProps} from '../maplibre/maplibre'; import type {ViewState} from '../types/common'; import type {TransformLike} from '../types/internal'; -import { MapInstance } from '../types/lib'; +import type {MapInstance} from '../types/lib'; import {deepEqual} from './deep-equal'; /** @@ -64,28 +64,68 @@ export function applyViewStateToTransform( * @param {object} nextRange * @param {object} currRange **/ -export function updateZoomConstraint(map: MapInstance, nextRange: { min: number; max: number}, currentRange: { min: number; max: number }): void { - if (nextRange.min === currentRange.min && nextRange.max === currentRange.max) { - return +export function updateZoomConstraint( + map: MapInstance, + nextRange: {min: number; max: number}, + currentRange: {min: number; max: number} +): void { + if (nextRange.min === currentRange.min && nextRange.max === currentRange.max) { + return; + } + + // if moving up ie. 1 - 3 -> 5 - 10 + if (nextRange.min >= currentRange.min) { + if (nextRange.max !== currentRange.max) { + map.setMaxZoom(nextRange.max); } - - // if moving up ie. 1 - 3 -> 5 - 10 - if (nextRange.min >= currentRange.min) { - if (nextRange.max !== currentRange.max) { - map.setMaxZoom(nextRange.max) - } - if (nextRange.min !== currentRange.min) { - map.setMinZoom(nextRange.min) - } + if (nextRange.min !== currentRange.min) { + map.setMinZoom(nextRange.min); } + } - // if moving down ie. 5 - 10 -> 1 - 3 - if (nextRange.min < currentRange.min) { - if (nextRange.min !== currentRange.min) { - map.setMinZoom(nextRange.min) - } - if (nextRange.max !== currentRange.max) { - map.setMaxZoom(nextRange.max) - } + // if moving down ie. 5 - 10 -> 1 - 3 + if (nextRange.min < currentRange.min) { + if (nextRange.min !== currentRange.min) { + map.setMinZoom(nextRange.min); } - } \ No newline at end of file + if (nextRange.max !== currentRange.max) { + map.setMaxZoom(nextRange.max); + } + } +} + +/** + * Update pitch constraints to match props by calling + * `setMinPitch` and `setMaxPitch` in the right order + * @param {object} nextRange + * @param {object} currRange + **/ +export function updatePitchConstraint( + map: MapInstance, + nextRange: {min: number; max: number}, + currentRange: {min: number; max: number} +): void { + if (nextRange.min === currentRange.min && nextRange.max === currentRange.max) { + return; + } + + // if moving up ie. 1 - 3 -> 5 - 10 + if (nextRange.min >= currentRange.min) { + if (nextRange.max !== currentRange.max) { + map.setMaxPitch(nextRange.max); + } + if (nextRange.min !== currentRange.min) { + map.setMinPitch(nextRange.min); + } + } + + // if moving down ie. 5 - 10 -> 1 - 3 + if (nextRange.min < currentRange.min) { + if (nextRange.min !== currentRange.min) { + map.setMinPitch(nextRange.min); + } + if (nextRange.max !== currentRange.max) { + map.setMaxPitch(nextRange.max); + } + } +} diff --git a/modules/react-maplibre/test/utils/transform.spec.js b/modules/react-maplibre/test/utils/transform.spec.js index a686cd052..e9173627a 100644 --- a/modules/react-maplibre/test/utils/transform.spec.js +++ b/modules/react-maplibre/test/utils/transform.spec.js @@ -1,8 +1,9 @@ -import test from 'tape'; +import test from 'tape-promise/tape'; import { transformToViewState, applyViewStateToTransform, - updateZoomConstraint + updateZoomConstraint, + updatePitchConstraint, } from '@vis.gl/react-maplibre/utils/transform'; import maplibregl from 'maplibre-gl'; @@ -91,46 +92,104 @@ test('updateZoomConstraint', t => { } } - // moving down. 5 - 10 -> 1 - 3 currentMinZoom = 5 currentMaxZoom = 10 - updateZoomConstraint(map, { min: 1, max: 3 }, { min: 5, max: 10 }); - t.equal(first, 'min', 'min first') + updateZoomConstraint(map, { min: 1, max: 3 }, { min: currentMinZoom, max: currentMaxZoom }); + t.equal(first, 'min', '5 - 10 -> 1 - 3, update min first') first = null - // moving up. 1 - 3 -> 5 - 10 currentMinZoom = 1 currentMaxZoom = 3 - updateZoomConstraint(map, { min: 5, max: 10 }, { min: 1, max: 3 }); - t.equal(first, 'max', 'max first') + updateZoomConstraint(map, { min: 5, max: 10 }, { min: currentMinZoom, max: currentMaxZoom }); + t.equal(first, 'max', '1 - 3 -> 5 - 10, update max first') first = null - // expanding. 5 - 18 -> 3 - 22 currentMinZoom = 5 currentMaxZoom = 18 - updateZoomConstraint(map, { min: 3, max: 22 }, { min: 5, max: 18 }); - t.equal(first, 'min', 'min first') + updateZoomConstraint(map, { min: 3, max: 22 }, { min: currentMinZoom, max: currentMaxZoom }); + t.equal(first, 'min', '5 - 18 -> 3 - 22, update min first') first = null - // expanding down. 5 - 18 -> 3 - 18 currentMinZoom = 5 currentMaxZoom = 18 - updateZoomConstraint(map, { min: 3, max: 18 }, { min: 5, max: 18 }); - t.equal(first, 'min', 'min first') + updateZoomConstraint(map, { min: 3, max: 18 }, { min: currentMinZoom, max: currentMaxZoom }); + t.equal(first, 'min', '5 - 18 -> 3 - 18, update min first') first = null - // contracting. 3 - 22 -> 5 - 18 - currentMinZoom = 5 - currentMaxZoom = 18 - updateZoomConstraint(map, { min: 5, max: 18 }, { min: 3, max: 22 }); - t.equal(first, 'max', 'max first') + currentMinZoom = 3 + currentMaxZoom = 22 + updateZoomConstraint(map, { min: 5, max: 18 }, { min: currentMinZoom, max: currentMaxZoom }); + t.equal(first, 'max', '3 - 22 -> 5 - 18, update max first') first = null - // contracting down. 12 - 22 -> 5 - 10 currentMinZoom = 12 currentMaxZoom = 22 - updateZoomConstraint(map, { min: 5, max: 10 }, { min: 12, max: 22 }); - t.equal(first, 'min', 'min first') + updateZoomConstraint(map, { min: 5, max: 10 }, { min: currentMinZoom, max: currentMaxZoom }); + t.equal(first, 'min', '12 - 22 -> 5 - 10, update min first') + first = null + + t.end(); +}); + +test('updatePitchConstraint', t => { + let first = null + let currentMinPitch = 0 + let currentMaxPitch = 0 + const map = { + setMinPitch: (nextMinPitch) => { + if (nextMinPitch > currentMaxPitch) { + throw new Error('Setting minPitch > maxPitch') + } + currentMinPitch = nextMinPitch + if (!first) { + first = 'min' + } + }, + setMaxPitch: (nextMaxPitch) => { + if (nextMaxPitch < currentMinPitch) { + throw new Error('Setting maxPitch < minPitch') + } + currentMaxPitch = nextMaxPitch + if (!first) { + first = 'max' + } + } + } + + currentMinPitch = 5 + currentMaxPitch = 10 + updatePitchConstraint(map, { min: 1, max: 3 }, { min: currentMinPitch, max: currentMaxPitch }); + t.equal(first, 'min', '5 - 10 -> 1 - 3, update min first') + first = null + + currentMinPitch = 1 + currentMaxPitch = 3 + updatePitchConstraint(map, { min: 5, max: 10 }, { min: currentMinPitch, max: currentMaxPitch }); + t.equal(first, 'max', '1 - 3 -> 5 - 10, update max first') + first = null + + currentMinPitch = 5 + currentMaxPitch = 18 + updatePitchConstraint(map, { min: 3, max: 22 }, { min: currentMinPitch, max: currentMaxPitch }); + t.equal(first, 'min', '5 - 18 -> 3 - 22, update min first') + first = null + + currentMinPitch = 5 + currentMaxPitch = 18 + updatePitchConstraint(map, { min: 3, max: 18 }, { min: currentMinPitch, max: currentMaxPitch }); + t.equal(first, 'min', '5 - 18 -> 3 - 18, update min first') + first = null + + currentMinPitch = 3 + currentMaxPitch = 22 + updatePitchConstraint(map, { min: 5, max: 18 }, { min: currentMinPitch, max: currentMaxPitch }); + t.equal(first, 'max', '3 - 22 -> 5 - 18, update max first') + first = null + + currentMinPitch = 12 + currentMaxPitch = 22 + updatePitchConstraint(map, { min: 5, max: 10 }, { min: currentMinPitch, max: currentMaxPitch }); + t.equal(first, 'min', '12 - 22 -> 5 - 10, update min first') first = null t.end(); From e86b75d371b7ee7ca6bdb8e7669d637abbcd2202 Mon Sep 17 00:00:00 2001 From: Auspicus Date: Tue, 20 Jan 2026 23:41:03 +1100 Subject: [PATCH 4/7] Return whether or not changes were applied from the updatePitchConstraint and updateZoomConstraint func --- modules/react-maplibre/src/maplibre/maplibre.ts | 6 +++--- modules/react-maplibre/src/utils/transform.ts | 12 ++++++++---- 2 files changed, 11 insertions(+), 7 deletions(-) diff --git a/modules/react-maplibre/src/maplibre/maplibre.ts b/modules/react-maplibre/src/maplibre/maplibre.ts index 6bfe1e1f1..346143b87 100644 --- a/modules/react-maplibre/src/maplibre/maplibre.ts +++ b/modules/react-maplibre/src/maplibre/maplibre.ts @@ -443,7 +443,7 @@ export default class Maplibre { @returns {bool} true if anything is changed */ private _updateConstraints(nextProps: MaplibreProps, currProps: MaplibreProps): boolean { - updateZoomConstraint( + const didUpdateZoom = updateZoomConstraint( this._map, { min: nextProps.minZoom ?? DEFAULT_SETTINGS.minZoom, @@ -454,7 +454,7 @@ export default class Maplibre { max: currProps.maxZoom ?? DEFAULT_SETTINGS.maxZoom } ); - updatePitchConstraint( + const didUpdatePitch = updatePitchConstraint( this._map, { min: nextProps.minPitch ?? DEFAULT_SETTINGS.minPitch, @@ -466,7 +466,7 @@ export default class Maplibre { } ); - return true; + return didUpdateZoom || didUpdatePitch; } /* Update camera constraints and projection settings to match props diff --git a/modules/react-maplibre/src/utils/transform.ts b/modules/react-maplibre/src/utils/transform.ts index b44823d3e..a317402f6 100644 --- a/modules/react-maplibre/src/utils/transform.ts +++ b/modules/react-maplibre/src/utils/transform.ts @@ -68,9 +68,9 @@ export function updateZoomConstraint( map: MapInstance, nextRange: {min: number; max: number}, currentRange: {min: number; max: number} -): void { +): boolean { if (nextRange.min === currentRange.min && nextRange.max === currentRange.max) { - return; + return false; } // if moving up ie. 1 - 3 -> 5 - 10 @@ -92,6 +92,8 @@ export function updateZoomConstraint( map.setMaxZoom(nextRange.max); } } + + return true; } /** @@ -104,9 +106,9 @@ export function updatePitchConstraint( map: MapInstance, nextRange: {min: number; max: number}, currentRange: {min: number; max: number} -): void { +): boolean { if (nextRange.min === currentRange.min && nextRange.max === currentRange.max) { - return; + return false; } // if moving up ie. 1 - 3 -> 5 - 10 @@ -128,4 +130,6 @@ export function updatePitchConstraint( map.setMaxPitch(nextRange.max); } } + + return true; } From 3a1ddfd1d51fd599e6df1829cf01f278d56198eb Mon Sep 17 00:00:00 2001 From: Auspicus Date: Tue, 20 Jan 2026 23:47:35 +1100 Subject: [PATCH 5/7] Add back propPresent --- modules/react-maplibre/src/maplibre/maplibre.ts | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/modules/react-maplibre/src/maplibre/maplibre.ts b/modules/react-maplibre/src/maplibre/maplibre.ts index 346143b87..751a32d40 100644 --- a/modules/react-maplibre/src/maplibre/maplibre.ts +++ b/modules/react-maplibre/src/maplibre/maplibre.ts @@ -483,7 +483,8 @@ export default class Maplibre { continue; } - if (propName in nextProps && !deepEqual(nextProps[propName], currProps[propName])) { + const propPresent = propName in nextProps || propName in currProps; + if (propPresent && !deepEqual(nextProps[propName], currProps[propName])) { settingsChanged = true; const nextValue = propName in nextProps ? nextProps[propName] : DEFAULT_SETTINGS[propName]; const setter = map[`set${propName[0].toUpperCase()}${propName.slice(1)}`]; From c818589106af6ad1b9818b041018dd6b136a262c Mon Sep 17 00:00:00 2001 From: Auspicus Date: Tue, 14 Apr 2026 08:39:50 +1000 Subject: [PATCH 6/7] yarn lint fix --- .../test/utils/transform.spec.js | 194 +++++++++--------- 1 file changed, 97 insertions(+), 97 deletions(-) diff --git a/modules/react-maplibre/test/utils/transform.spec.js b/modules/react-maplibre/test/utils/transform.spec.js index e9173627a..64f19c655 100644 --- a/modules/react-maplibre/test/utils/transform.spec.js +++ b/modules/react-maplibre/test/utils/transform.spec.js @@ -3,7 +3,7 @@ import { transformToViewState, applyViewStateToTransform, updateZoomConstraint, - updatePitchConstraint, + updatePitchConstraint } from '@vis.gl/react-maplibre/utils/transform'; import maplibregl from 'maplibre-gl'; @@ -68,129 +68,129 @@ test('applyViewStateToTransform', t => { }); test('updateZoomConstraint', t => { - let first = null - let currentMinZoom = 0 - let currentMaxZoom = 0 + let first = null; + let currentMinZoom = 0; + let currentMaxZoom = 0; const map = { - setMinZoom: (nextMinZoom) => { + setMinZoom: nextMinZoom => { if (nextMinZoom > currentMaxZoom) { - throw new Error('Setting minZoom > maxZoom') + throw new Error('Setting minZoom > maxZoom'); } - currentMinZoom = nextMinZoom + currentMinZoom = nextMinZoom; if (!first) { - first = 'min' + first = 'min'; } }, - setMaxZoom: (nextMaxZoom) => { + setMaxZoom: nextMaxZoom => { if (nextMaxZoom < currentMinZoom) { - throw new Error('Setting maxZoom < minZoom') + throw new Error('Setting maxZoom < minZoom'); } - currentMaxZoom = nextMaxZoom + currentMaxZoom = nextMaxZoom; if (!first) { - first = 'max' + first = 'max'; } } - } - - currentMinZoom = 5 - currentMaxZoom = 10 - updateZoomConstraint(map, { min: 1, max: 3 }, { min: currentMinZoom, max: currentMaxZoom }); - t.equal(first, 'min', '5 - 10 -> 1 - 3, update min first') - first = null - - currentMinZoom = 1 - currentMaxZoom = 3 - updateZoomConstraint(map, { min: 5, max: 10 }, { min: currentMinZoom, max: currentMaxZoom }); - t.equal(first, 'max', '1 - 3 -> 5 - 10, update max first') - first = null - - currentMinZoom = 5 - currentMaxZoom = 18 - updateZoomConstraint(map, { min: 3, max: 22 }, { min: currentMinZoom, max: currentMaxZoom }); - t.equal(first, 'min', '5 - 18 -> 3 - 22, update min first') - first = null - - currentMinZoom = 5 - currentMaxZoom = 18 - updateZoomConstraint(map, { min: 3, max: 18 }, { min: currentMinZoom, max: currentMaxZoom }); - t.equal(first, 'min', '5 - 18 -> 3 - 18, update min first') - first = null - - currentMinZoom = 3 - currentMaxZoom = 22 - updateZoomConstraint(map, { min: 5, max: 18 }, { min: currentMinZoom, max: currentMaxZoom }); - t.equal(first, 'max', '3 - 22 -> 5 - 18, update max first') - first = null - - currentMinZoom = 12 - currentMaxZoom = 22 - updateZoomConstraint(map, { min: 5, max: 10 }, { min: currentMinZoom, max: currentMaxZoom }); - t.equal(first, 'min', '12 - 22 -> 5 - 10, update min first') - first = null + }; + + currentMinZoom = 5; + currentMaxZoom = 10; + updateZoomConstraint(map, {min: 1, max: 3}, {min: currentMinZoom, max: currentMaxZoom}); + t.equal(first, 'min', '5 - 10 -> 1 - 3, update min first'); + first = null; + + currentMinZoom = 1; + currentMaxZoom = 3; + updateZoomConstraint(map, {min: 5, max: 10}, {min: currentMinZoom, max: currentMaxZoom}); + t.equal(first, 'max', '1 - 3 -> 5 - 10, update max first'); + first = null; + + currentMinZoom = 5; + currentMaxZoom = 18; + updateZoomConstraint(map, {min: 3, max: 22}, {min: currentMinZoom, max: currentMaxZoom}); + t.equal(first, 'min', '5 - 18 -> 3 - 22, update min first'); + first = null; + + currentMinZoom = 5; + currentMaxZoom = 18; + updateZoomConstraint(map, {min: 3, max: 18}, {min: currentMinZoom, max: currentMaxZoom}); + t.equal(first, 'min', '5 - 18 -> 3 - 18, update min first'); + first = null; + + currentMinZoom = 3; + currentMaxZoom = 22; + updateZoomConstraint(map, {min: 5, max: 18}, {min: currentMinZoom, max: currentMaxZoom}); + t.equal(first, 'max', '3 - 22 -> 5 - 18, update max first'); + first = null; + + currentMinZoom = 12; + currentMaxZoom = 22; + updateZoomConstraint(map, {min: 5, max: 10}, {min: currentMinZoom, max: currentMaxZoom}); + t.equal(first, 'min', '12 - 22 -> 5 - 10, update min first'); + first = null; t.end(); }); test('updatePitchConstraint', t => { - let first = null - let currentMinPitch = 0 - let currentMaxPitch = 0 + let first = null; + let currentMinPitch = 0; + let currentMaxPitch = 0; const map = { - setMinPitch: (nextMinPitch) => { + setMinPitch: nextMinPitch => { if (nextMinPitch > currentMaxPitch) { - throw new Error('Setting minPitch > maxPitch') + throw new Error('Setting minPitch > maxPitch'); } - currentMinPitch = nextMinPitch + currentMinPitch = nextMinPitch; if (!first) { - first = 'min' + first = 'min'; } }, - setMaxPitch: (nextMaxPitch) => { + setMaxPitch: nextMaxPitch => { if (nextMaxPitch < currentMinPitch) { - throw new Error('Setting maxPitch < minPitch') + throw new Error('Setting maxPitch < minPitch'); } - currentMaxPitch = nextMaxPitch + currentMaxPitch = nextMaxPitch; if (!first) { - first = 'max' + first = 'max'; } } - } - - currentMinPitch = 5 - currentMaxPitch = 10 - updatePitchConstraint(map, { min: 1, max: 3 }, { min: currentMinPitch, max: currentMaxPitch }); - t.equal(first, 'min', '5 - 10 -> 1 - 3, update min first') - first = null - - currentMinPitch = 1 - currentMaxPitch = 3 - updatePitchConstraint(map, { min: 5, max: 10 }, { min: currentMinPitch, max: currentMaxPitch }); - t.equal(first, 'max', '1 - 3 -> 5 - 10, update max first') - first = null - - currentMinPitch = 5 - currentMaxPitch = 18 - updatePitchConstraint(map, { min: 3, max: 22 }, { min: currentMinPitch, max: currentMaxPitch }); - t.equal(first, 'min', '5 - 18 -> 3 - 22, update min first') - first = null - - currentMinPitch = 5 - currentMaxPitch = 18 - updatePitchConstraint(map, { min: 3, max: 18 }, { min: currentMinPitch, max: currentMaxPitch }); - t.equal(first, 'min', '5 - 18 -> 3 - 18, update min first') - first = null - - currentMinPitch = 3 - currentMaxPitch = 22 - updatePitchConstraint(map, { min: 5, max: 18 }, { min: currentMinPitch, max: currentMaxPitch }); - t.equal(first, 'max', '3 - 22 -> 5 - 18, update max first') - first = null - - currentMinPitch = 12 - currentMaxPitch = 22 - updatePitchConstraint(map, { min: 5, max: 10 }, { min: currentMinPitch, max: currentMaxPitch }); - t.equal(first, 'min', '12 - 22 -> 5 - 10, update min first') - first = null + }; + + currentMinPitch = 5; + currentMaxPitch = 10; + updatePitchConstraint(map, {min: 1, max: 3}, {min: currentMinPitch, max: currentMaxPitch}); + t.equal(first, 'min', '5 - 10 -> 1 - 3, update min first'); + first = null; + + currentMinPitch = 1; + currentMaxPitch = 3; + updatePitchConstraint(map, {min: 5, max: 10}, {min: currentMinPitch, max: currentMaxPitch}); + t.equal(first, 'max', '1 - 3 -> 5 - 10, update max first'); + first = null; + + currentMinPitch = 5; + currentMaxPitch = 18; + updatePitchConstraint(map, {min: 3, max: 22}, {min: currentMinPitch, max: currentMaxPitch}); + t.equal(first, 'min', '5 - 18 -> 3 - 22, update min first'); + first = null; + + currentMinPitch = 5; + currentMaxPitch = 18; + updatePitchConstraint(map, {min: 3, max: 18}, {min: currentMinPitch, max: currentMaxPitch}); + t.equal(first, 'min', '5 - 18 -> 3 - 18, update min first'); + first = null; + + currentMinPitch = 3; + currentMaxPitch = 22; + updatePitchConstraint(map, {min: 5, max: 18}, {min: currentMinPitch, max: currentMaxPitch}); + t.equal(first, 'max', '3 - 22 -> 5 - 18, update max first'); + first = null; + + currentMinPitch = 12; + currentMaxPitch = 22; + updatePitchConstraint(map, {min: 5, max: 10}, {min: currentMinPitch, max: currentMaxPitch}); + t.equal(first, 'min', '12 - 22 -> 5 - 10, update min first'); + first = null; t.end(); }); From ffe693798322f89087095f4a1cbbf3e93210fe56 Mon Sep 17 00:00:00 2001 From: Auspicus Date: Tue, 14 Apr 2026 08:57:23 +1000 Subject: [PATCH 7/7] refactor: cleanup tests, prop names, code duplication, comments --- .../react-maplibre/src/maplibre/maplibre.ts | 8 +- modules/react-maplibre/src/utils/transform.ts | 85 ++++---- .../test/utils/transform.spec.js | 192 ++++++++---------- 3 files changed, 123 insertions(+), 162 deletions(-) diff --git a/modules/react-maplibre/src/maplibre/maplibre.ts b/modules/react-maplibre/src/maplibre/maplibre.ts index d60233d45..f4656dab6 100644 --- a/modules/react-maplibre/src/maplibre/maplibre.ts +++ b/modules/react-maplibre/src/maplibre/maplibre.ts @@ -168,8 +168,7 @@ const otherEvents = { sourcedata: 'onSourceData', error: 'onError' }; -const constraintNames = ['minZoom', 'maxZoom', 'minPitch', 'maxPitch'] as const; -const settingNames = [...constraintNames, 'maxBounds', 'projection', 'renderWorldCopies'] as const; +const settingNames = ['maxBounds', 'projection', 'renderWorldCopies'] as const; const handlerNames = [ 'scrollZoom', 'boxZoom', @@ -478,11 +477,6 @@ export default class Maplibre { const map = this._map; let settingsChanged = false; for (const propName of settingNames) { - if (constraintNames.includes(propName as (typeof constraintNames)[number])) { - // eslint-disable-next-line no-continue - continue; - } - const propPresent = propName in nextProps || propName in currProps; if (propPresent && !deepEqual(nextProps[propName], currProps[propName])) { settingsChanged = true; diff --git a/modules/react-maplibre/src/utils/transform.ts b/modules/react-maplibre/src/utils/transform.ts index a317402f6..742302c7b 100644 --- a/modules/react-maplibre/src/utils/transform.ts +++ b/modules/react-maplibre/src/utils/transform.ts @@ -59,77 +59,66 @@ export function applyViewStateToTransform( } /** - * Update zoom constraints to match props by calling - * `setMinZoom` and `setMaxZoom` in the right order - * @param {object} nextRange - * @param {object} currRange - **/ -export function updateZoomConstraint( - map: MapInstance, + * Update a min/max constraint pair in the right order to avoid + * temporarily setting min > max (which maplibre rejects). + * @param nextRange - the desired constraint range + * @param currentRange - the current constraint range + * @param setMin - setter for the minimum value + * @param setMax - setter for the maximum value + */ +function updateConstraint( nextRange: {min: number; max: number}, - currentRange: {min: number; max: number} + currentRange: {min: number; max: number}, + setMin: (v: number) => void, + setMax: (v: number) => void ): boolean { if (nextRange.min === currentRange.min && nextRange.max === currentRange.max) { return false; } - // if moving up ie. 1 - 3 -> 5 - 10 + // When moving up (min increasing), update max first to make room if (nextRange.min >= currentRange.min) { if (nextRange.max !== currentRange.max) { - map.setMaxZoom(nextRange.max); + setMax(nextRange.max); } if (nextRange.min !== currentRange.min) { - map.setMinZoom(nextRange.min); + setMin(nextRange.min); } - } - - // if moving down ie. 5 - 10 -> 1 - 3 - if (nextRange.min < currentRange.min) { + } else { + // When moving down (min decreasing), update min first to make room if (nextRange.min !== currentRange.min) { - map.setMinZoom(nextRange.min); + setMin(nextRange.min); } if (nextRange.max !== currentRange.max) { - map.setMaxZoom(nextRange.max); + setMax(nextRange.max); } } return true; } -/** - * Update pitch constraints to match props by calling - * `setMinPitch` and `setMaxPitch` in the right order - * @param {object} nextRange - * @param {object} currRange - **/ -export function updatePitchConstraint( +export function updateZoomConstraint( map: MapInstance, nextRange: {min: number; max: number}, currentRange: {min: number; max: number} ): boolean { - if (nextRange.min === currentRange.min && nextRange.max === currentRange.max) { - return false; - } - - // if moving up ie. 1 - 3 -> 5 - 10 - if (nextRange.min >= currentRange.min) { - if (nextRange.max !== currentRange.max) { - map.setMaxPitch(nextRange.max); - } - if (nextRange.min !== currentRange.min) { - map.setMinPitch(nextRange.min); - } - } - - // if moving down ie. 5 - 10 -> 1 - 3 - if (nextRange.min < currentRange.min) { - if (nextRange.min !== currentRange.min) { - map.setMinPitch(nextRange.min); - } - if (nextRange.max !== currentRange.max) { - map.setMaxPitch(nextRange.max); - } - } + return updateConstraint( + nextRange, + currentRange, + v => map.setMinZoom(v), + v => map.setMaxZoom(v) + ); +} - return true; +export function updatePitchConstraint( + map: MapInstance, + nextRange: {min: number; max: number}, + currentRange: {min: number; max: number} +): boolean { + return updateConstraint( + nextRange, + currentRange, + v => map.setMinPitch(v), + v => map.setMaxPitch(v) + ); } diff --git a/modules/react-maplibre/test/utils/transform.spec.js b/modules/react-maplibre/test/utils/transform.spec.js index 64f19c655..506dd0acd 100644 --- a/modules/react-maplibre/test/utils/transform.spec.js +++ b/modules/react-maplibre/test/utils/transform.spec.js @@ -67,130 +67,108 @@ test('applyViewStateToTransform', t => { t.end(); }); -test('updateZoomConstraint', t => { +function createConstraintMap(setMinName, setMaxName) { let first = null; - let currentMinZoom = 0; - let currentMaxZoom = 0; + let currentMin = 0; + let currentMax = 0; const map = { - setMinZoom: nextMinZoom => { - if (nextMinZoom > currentMaxZoom) { - throw new Error('Setting minZoom > maxZoom'); + [setMinName]: nextMin => { + if (nextMin > currentMax) { + throw new Error(`Setting ${setMinName} (${nextMin}) > current max (${currentMax})`); } - currentMinZoom = nextMinZoom; + currentMin = nextMin; if (!first) { first = 'min'; } }, - setMaxZoom: nextMaxZoom => { - if (nextMaxZoom < currentMinZoom) { - throw new Error('Setting maxZoom < minZoom'); + [setMaxName]: nextMax => { + if (nextMax < currentMin) { + throw new Error(`Setting ${setMaxName} (${nextMax}) < current min (${currentMin})`); } - currentMaxZoom = nextMaxZoom; + currentMax = nextMax; if (!first) { first = 'max'; } } }; - - currentMinZoom = 5; - currentMaxZoom = 10; - updateZoomConstraint(map, {min: 1, max: 3}, {min: currentMinZoom, max: currentMaxZoom}); - t.equal(first, 'min', '5 - 10 -> 1 - 3, update min first'); - first = null; - - currentMinZoom = 1; - currentMaxZoom = 3; - updateZoomConstraint(map, {min: 5, max: 10}, {min: currentMinZoom, max: currentMaxZoom}); - t.equal(first, 'max', '1 - 3 -> 5 - 10, update max first'); - first = null; - - currentMinZoom = 5; - currentMaxZoom = 18; - updateZoomConstraint(map, {min: 3, max: 22}, {min: currentMinZoom, max: currentMaxZoom}); - t.equal(first, 'min', '5 - 18 -> 3 - 22, update min first'); - first = null; - - currentMinZoom = 5; - currentMaxZoom = 18; - updateZoomConstraint(map, {min: 3, max: 18}, {min: currentMinZoom, max: currentMaxZoom}); - t.equal(first, 'min', '5 - 18 -> 3 - 18, update min first'); - first = null; - - currentMinZoom = 3; - currentMaxZoom = 22; - updateZoomConstraint(map, {min: 5, max: 18}, {min: currentMinZoom, max: currentMaxZoom}); - t.equal(first, 'max', '3 - 22 -> 5 - 18, update max first'); - first = null; - - currentMinZoom = 12; - currentMaxZoom = 22; - updateZoomConstraint(map, {min: 5, max: 10}, {min: currentMinZoom, max: currentMaxZoom}); - t.equal(first, 'min', '12 - 22 -> 5 - 10, update min first'); - first = null; - - t.end(); -}); - -test('updatePitchConstraint', t => { - let first = null; - let currentMinPitch = 0; - let currentMaxPitch = 0; - const map = { - setMinPitch: nextMinPitch => { - if (nextMinPitch > currentMaxPitch) { - throw new Error('Setting minPitch > maxPitch'); - } - currentMinPitch = nextMinPitch; - if (!first) { - first = 'min'; - } + return { + map, + reset(min, max) { + currentMin = min; + currentMax = max; + first = null; }, - setMaxPitch: nextMaxPitch => { - if (nextMaxPitch < currentMinPitch) { - throw new Error('Setting maxPitch < minPitch'); - } - currentMaxPitch = nextMaxPitch; - if (!first) { - first = 'max'; - } + getFirst() { + return first; } }; +} + +function testConstraintUpdate(t, updateFn, setMinName, setMaxName, label) { + const helper = createConstraintMap(setMinName, setMaxName); + + // Range shifting down + helper.reset(5, 10); + updateFn(helper.map, {min: 1, max: 3}, {min: 5, max: 10}); + t.equal(helper.getFirst(), 'min', `${label}: 5 - 10 -> 1 - 3, update min first`); + + // Range shifting up + helper.reset(1, 3); + updateFn(helper.map, {min: 5, max: 10}, {min: 1, max: 3}); + t.equal(helper.getFirst(), 'max', `${label}: 1 - 3 -> 5 - 10, update max first`); + + // Range expanding + helper.reset(5, 18); + updateFn(helper.map, {min: 3, max: 22}, {min: 5, max: 18}); + t.equal(helper.getFirst(), 'min', `${label}: 5 - 18 -> 3 - 22, update min first`); + + // Only min changing (decreasing) + helper.reset(5, 18); + updateFn(helper.map, {min: 3, max: 18}, {min: 5, max: 18}); + t.equal(helper.getFirst(), 'min', `${label}: 5 - 18 -> 3 - 18, update min first`); + + // Range contracting + helper.reset(3, 22); + updateFn(helper.map, {min: 5, max: 18}, {min: 3, max: 22}); + t.equal(helper.getFirst(), 'max', `${label}: 3 - 22 -> 5 - 18, update max first`); + + // Range shifting down with high start + helper.reset(12, 22); + updateFn(helper.map, {min: 5, max: 10}, {min: 12, max: 22}); + t.equal(helper.getFirst(), 'min', `${label}: 12 - 22 -> 5 - 10, update min first`); + + // Locked to single value (min === max) + helper.reset(3, 10); + updateFn(helper.map, {min: 5, max: 5}, {min: 3, max: 10}); + t.equal(helper.getFirst(), 'max', `${label}: 3 - 10 -> 5 - 5, lock to single value`); + + // Unlock from single value + helper.reset(5, 5); + updateFn(helper.map, {min: 3, max: 10}, {min: 5, max: 5}); + t.equal(helper.getFirst(), 'min', `${label}: 5 - 5 -> 3 - 10, unlock from single value`); + + // Partial overlap (shifting up) + helper.reset(3, 8); + updateFn(helper.map, {min: 6, max: 10}, {min: 3, max: 8}); + t.equal(helper.getFirst(), 'max', `${label}: 3 - 8 -> 6 - 10, partial overlap shifting up`); + + // Partial overlap (shifting down) + helper.reset(6, 10); + updateFn(helper.map, {min: 3, max: 8}, {min: 6, max: 10}); + t.equal(helper.getFirst(), 'min', `${label}: 6 - 10 -> 3 - 8, partial overlap shifting down`); + + // No change returns false + helper.reset(3, 10); + const changed = updateFn(helper.map, {min: 3, max: 10}, {min: 3, max: 10}); + t.equal(changed, false, `${label}: no change returns false`); +} - currentMinPitch = 5; - currentMaxPitch = 10; - updatePitchConstraint(map, {min: 1, max: 3}, {min: currentMinPitch, max: currentMaxPitch}); - t.equal(first, 'min', '5 - 10 -> 1 - 3, update min first'); - first = null; - - currentMinPitch = 1; - currentMaxPitch = 3; - updatePitchConstraint(map, {min: 5, max: 10}, {min: currentMinPitch, max: currentMaxPitch}); - t.equal(first, 'max', '1 - 3 -> 5 - 10, update max first'); - first = null; - - currentMinPitch = 5; - currentMaxPitch = 18; - updatePitchConstraint(map, {min: 3, max: 22}, {min: currentMinPitch, max: currentMaxPitch}); - t.equal(first, 'min', '5 - 18 -> 3 - 22, update min first'); - first = null; - - currentMinPitch = 5; - currentMaxPitch = 18; - updatePitchConstraint(map, {min: 3, max: 18}, {min: currentMinPitch, max: currentMaxPitch}); - t.equal(first, 'min', '5 - 18 -> 3 - 18, update min first'); - first = null; - - currentMinPitch = 3; - currentMaxPitch = 22; - updatePitchConstraint(map, {min: 5, max: 18}, {min: currentMinPitch, max: currentMaxPitch}); - t.equal(first, 'max', '3 - 22 -> 5 - 18, update max first'); - first = null; - - currentMinPitch = 12; - currentMaxPitch = 22; - updatePitchConstraint(map, {min: 5, max: 10}, {min: currentMinPitch, max: currentMaxPitch}); - t.equal(first, 'min', '12 - 22 -> 5 - 10, update min first'); - first = null; +test('updateZoomConstraint', t => { + testConstraintUpdate(t, updateZoomConstraint, 'setMinZoom', 'setMaxZoom', 'zoom'); + t.end(); +}); +test('updatePitchConstraint', t => { + testConstraintUpdate(t, updatePitchConstraint, 'setMinPitch', 'setMaxPitch', 'pitch'); t.end(); });