Skip to content
Open
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
88 changes: 74 additions & 14 deletions modules/react-maplibre/src/maplibre/maplibre.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,9 @@
import {transformToViewState, applyViewStateToTransform} from '../utils/transform';
import {
transformToViewState,
applyViewStateToTransform,
updateZoomConstraint,
updatePitchConstraint
} from '../utils/transform';
import {normalizeStyle} from '../utils/style-utils';
import {deepEqual} from '../utils/deep-equal';

Expand Down Expand Up @@ -76,6 +81,31 @@ export type MaplibreProps = Partial<ViewState> &
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;
Expand Down Expand Up @@ -138,15 +168,8 @@ const otherEvents = {
sourcedata: 'onSourceData',
error: 'onError'
};
const settingNames = [
'minZoom',
'maxZoom',
'minPitch',
'maxPitch',
'maxBounds',
'projection',
'renderWorldCopies'
];
const constraintNames = ['minZoom', 'maxZoom', 'minPitch', 'maxPitch'] as const;
const settingNames = [...constraintNames, 'maxBounds', 'projection', 'renderWorldCopies'] as const;
const handlerNames = [
'scrollZoom',
'boxZoom',
Expand Down Expand Up @@ -414,25 +437,62 @@ export default class Maplibre {
return false;
}

/* 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 {
const didUpdateZoom = 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
}
);
const didUpdatePitch = 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 didUpdateZoom || didUpdatePitch;
}

/* Update camera constraints and projection settings to match props
@param {object} nextProps
@param {object} currProps
@returns {bool} true if anything is changed
*/
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 (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])) {
changed = true;
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 constraintsChanged = this._updateConstraints(nextProps, currProps);
return settingsChanged || constraintsChanged;
}

/* Update map style to match props */
Expand Down
77 changes: 77 additions & 0 deletions modules/react-maplibre/src/utils/transform.ts
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
import type {MaplibreProps} from '../maplibre/maplibre';
import type {ViewState} from '../types/common';
import type {TransformLike} from '../types/internal';
import type {MapInstance} from '../types/lib';
import {deepEqual} from './deep-equal';

/**
Expand Down Expand Up @@ -56,3 +57,79 @@ 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}
): 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.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);
}
}

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(
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 true;
}
132 changes: 131 additions & 1 deletion modules/react-maplibre/test/utils/transform.spec.js
Original file line number Diff line number Diff line change
@@ -1,7 +1,9 @@
import test from 'tape-promise/tape';
import {
transformToViewState,
applyViewStateToTransform
applyViewStateToTransform,
updateZoomConstraint,
updatePitchConstraint,
} from '@vis.gl/react-maplibre/utils/transform';
import maplibregl from 'maplibre-gl';

Expand Down Expand Up @@ -64,3 +66,131 @@ 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'
}
}
}

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'
}
},
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();
});