Skip to content

Commit cbbbbfc

Browse files
authored
min and max pitch options (#8834)
1 parent 80fb531 commit cbbbbfc

File tree

5 files changed

+223
-11
lines changed

5 files changed

+223
-11
lines changed

src/geo/transform.js

Lines changed: 22 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -46,19 +46,24 @@ class Transform {
4646
_renderWorldCopies: boolean;
4747
_minZoom: number;
4848
_maxZoom: number;
49+
_minPitch: number;
50+
_maxPitch: number;
4951
_center: LngLat;
5052
_constraining: boolean;
5153
_posMatrixCache: {[number]: Float32Array};
5254
_alignedPosMatrixCache: {[number]: Float32Array};
5355

54-
constructor(minZoom: ?number, maxZoom: ?number, renderWorldCopies: boolean | void) {
56+
constructor(minZoom: ?number, maxZoom: ?number, minPitch: ?number, maxPitch: ?number, renderWorldCopies: boolean | void) {
5557
this.tileSize = 512; // constant
5658
this.maxValidLatitude = 85.051129; // constant
5759

5860
this._renderWorldCopies = renderWorldCopies === undefined ? true : renderWorldCopies;
5961
this._minZoom = minZoom || 0;
6062
this._maxZoom = maxZoom || 22;
6163

64+
this._minPitch = (minPitch === undefined || minPitch === null) ? 0 : minPitch;
65+
this._maxPitch = (maxPitch === undefined || maxPitch === null) ? 60 : maxPitch;
66+
6267
this.setMaxBounds();
6368

6469
this.width = 0;
@@ -74,7 +79,7 @@ class Transform {
7479
}
7580

7681
clone(): Transform {
77-
const clone = new Transform(this._minZoom, this._maxZoom, this._renderWorldCopies);
82+
const clone = new Transform(this._minZoom, this._maxZoom, this._minPitch, this.maxPitch, this._renderWorldCopies);
7883
clone.tileSize = this.tileSize;
7984
clone.latRange = this.latRange;
8085
clone.width = this.width;
@@ -103,6 +108,20 @@ class Transform {
103108
this.zoom = Math.min(this.zoom, zoom);
104109
}
105110

111+
get minPitch(): number { return this._minPitch; }
112+
set minPitch(pitch: number) {
113+
if (this._minPitch === pitch) return;
114+
this._minPitch = pitch;
115+
this.pitch = Math.max(this.pitch, pitch);
116+
}
117+
118+
get maxPitch(): number { return this._maxPitch; }
119+
set maxPitch(pitch: number) {
120+
if (this._maxPitch === pitch) return;
121+
this._maxPitch = pitch;
122+
this.pitch = Math.min(this.pitch, pitch);
123+
}
124+
106125
get renderWorldCopies(): boolean { return this._renderWorldCopies; }
107126
set renderWorldCopies(renderWorldCopies?: ?boolean) {
108127
if (renderWorldCopies === undefined) {
@@ -145,7 +164,7 @@ class Transform {
145164
return this._pitch / Math.PI * 180;
146165
}
147166
set pitch(pitch: number) {
148-
const p = clamp(pitch, 0, 60) / 180 * Math.PI;
167+
const p = clamp(pitch, this.minPitch, this.maxPitch) / 180 * Math.PI;
149168
if (this._pitch === p) return;
150169
this._unmodified = false;
151170
this._pitch = p;

src/ui/map.js

Lines changed: 96 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -80,6 +80,8 @@ type MapOptions = {
8080
scrollZoom?: boolean,
8181
minZoom?: ?number,
8282
maxZoom?: ?number,
83+
minPitch?: ?number,
84+
maxPitch?: ?number,
8385
boxZoom?: boolean,
8486
dragRotate?: boolean,
8587
dragPan?: DragPanOptions,
@@ -99,6 +101,11 @@ type MapOptions = {
99101

100102
const defaultMinZoom = 0;
101103
const defaultMaxZoom = 22;
104+
105+
// the default values, but also the valid range
106+
const defaultMinPitch = 0;
107+
const defaultMaxPitch = 60;
108+
102109
const defaultOptions = {
103110
center: [0, 0],
104111
zoom: 0,
@@ -108,6 +115,9 @@ const defaultOptions = {
108115
minZoom: defaultMinZoom,
109116
maxZoom: defaultMaxZoom,
110117

118+
minPitch: defaultMinPitch,
119+
maxPitch: defaultMaxPitch,
120+
111121
interactive: true,
112122
scrollZoom: true,
113123
boxZoom: true,
@@ -150,6 +160,8 @@ const defaultOptions = {
150160
* @param {HTMLElement|string} options.container The HTML element in which Mapbox GL JS will render the map, or the element's string `id`. The specified element must have no children.
151161
* @param {number} [options.minZoom=0] The minimum zoom level of the map (0-24).
152162
* @param {number} [options.maxZoom=22] The maximum zoom level of the map (0-24).
163+
* @param {number} [options.minPitch=0] The minimum pitch of the map (0-60).
164+
* @param {number} [options.maxPitch=60] The maximum pitch of the map (0-60).
153165
* @param {Object|string} [options.style] The map's Mapbox style. This must be an a JSON object conforming to
154166
* the schema described in the [Mapbox Style Specification](https://mapbox.com/mapbox-gl-style-spec/), or a URL to
155167
* such JSON.
@@ -329,10 +341,22 @@ class Map extends Camera {
329341
options = extend({}, defaultOptions, options);
330342

331343
if (options.minZoom != null && options.maxZoom != null && options.minZoom > options.maxZoom) {
332-
throw new Error(`maxZoom must be greater than minZoom`);
344+
throw new Error(`maxZoom must be greater than or equal to minZoom`);
345+
}
346+
347+
if (options.minPitch != null && options.maxPitch != null && options.minPitch > options.maxPitch) {
348+
throw new Error(`maxPitch must be greater than or equal to minPitch`);
349+
}
350+
351+
if (options.minPitch != null && options.minPitch < defaultMinPitch) {
352+
throw new Error(`minPitch must be greater than or equal to ${defaultMinPitch}`);
353+
}
354+
355+
if (options.maxPitch != null && options.maxPitch > defaultMaxPitch) {
356+
throw new Error(`maxPitch must be less than or equal to ${defaultMaxPitch}`);
333357
}
334358

335-
const transform = new Transform(options.minZoom, options.maxZoom, options.renderWorldCopies);
359+
const transform = new Transform(options.minZoom, options.maxZoom, options.minPitch, options.maxPitch, options.renderWorldCopies);
336360
super(transform, options);
337361

338362
this._interactive = options.interactive;
@@ -651,6 +675,76 @@ class Map extends Camera {
651675
*/
652676
getMaxZoom() { return this.transform.maxZoom; }
653677

678+
/**
679+
* Sets or clears the map's minimum pitch.
680+
* If the map's current pitch is lower than the new minimum,
681+
* the map will pitch to the new minimum.
682+
*
683+
* @param {number | null | undefined} minPitch The minimum pitch to set (0-60).
684+
* If `null` or `undefined` is provided, the function removes the current minimum pitch (i.e. sets it to 0).
685+
* @returns {Map} `this`
686+
*/
687+
setMinPitch(minPitch?: ?number) {
688+
689+
minPitch = minPitch === null || minPitch === undefined ? defaultMinPitch : minPitch;
690+
691+
if (minPitch < defaultMinPitch) {
692+
throw new Error(`minPitch must be greater than or equal to ${defaultMinPitch}`);
693+
}
694+
695+
if (minPitch >= defaultMinPitch && minPitch <= this.transform.maxPitch) {
696+
this.transform.minPitch = minPitch;
697+
this._update();
698+
699+
if (this.getPitch() < minPitch) this.setPitch(minPitch);
700+
701+
return this;
702+
703+
} else throw new Error(`minPitch must be between ${defaultMinPitch} and the current maxPitch, inclusive`);
704+
}
705+
706+
/**
707+
* Returns the map's minimum allowable pitch.
708+
*
709+
* @returns {number} minPitch
710+
*/
711+
getMinPitch() { return this.transform.minPitch; }
712+
713+
/**
714+
* Sets or clears the map's maximum pitch.
715+
* If the map's current pitch is higher than the new maximum,
716+
* the map will pitch to the new maximum.
717+
*
718+
* @param {number | null | undefined} maxPitch The maximum pitch to set.
719+
* If `null` or `undefined` is provided, the function removes the current maximum pitch (sets it to 60).
720+
* @returns {Map} `this`
721+
*/
722+
setMaxPitch(maxPitch?: ?number) {
723+
724+
maxPitch = maxPitch === null || maxPitch === undefined ? defaultMaxPitch : maxPitch;
725+
726+
if (maxPitch > defaultMaxPitch) {
727+
throw new Error(`maxPitch must be less than or equal to ${defaultMaxPitch}`);
728+
}
729+
730+
if (maxPitch >= this.transform.minPitch) {
731+
this.transform.maxPitch = maxPitch;
732+
this._update();
733+
734+
if (this.getPitch() > maxPitch) this.setPitch(maxPitch);
735+
736+
return this;
737+
738+
} else throw new Error(`maxPitch must be greater than the current minPitch`);
739+
}
740+
741+
/**
742+
* Returns the map's maximum allowable pitch.
743+
*
744+
* @returns {number} maxPitch
745+
*/
746+
getMaxPitch() { return this.transform.maxPitch; }
747+
654748
/**
655749
* Returns the state of `renderWorldCopies`. If `true`, multiple copies of the world will be rendered side by side beyond -180 and 180 degrees longitude. If set to `false`:
656750
* - When the map is zoomed out far enough that a single representation of the world does not fill the map's entire

test/unit/geo/transform.test.js

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -16,6 +16,7 @@ test('transform', (t) => {
1616
t.equal(transform.worldSize, 512, 'worldSize');
1717
t.equal(transform.width, 500, 'width');
1818
t.equal(transform.minZoom, 0, 'minZoom');
19+
t.equal(transform.minPitch, 0, 'minPitch');
1920
t.equal(transform.bearing, 0, 'bearing');
2021
t.equal(transform.bearing = 1, 1, 'set bearing');
2122
t.equal(transform.bearing, 1, 'bearing');
@@ -26,6 +27,8 @@ test('transform', (t) => {
2627
t.equal(transform.minZoom, 10);
2728
t.deepEqual(transform.center, {lng: 0, lat: 0});
2829
t.equal(transform.maxZoom, 10);
30+
t.equal(transform.minPitch = 10, 10);
31+
t.equal(transform.maxPitch = 10, 10);
2932
t.equal(transform.size.equals(new Point(500, 500)), true);
3033
t.equal(transform.centerPoint.equals(new Point(250, 250)), true);
3134
t.equal(transform.scaleZoom(0), -Infinity);

test/unit/ui/camera.test.js

Lines changed: 4 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -18,7 +18,7 @@ test('camera', (t) => {
1818
function createCamera(options) {
1919
options = options || {};
2020

21-
const transform = new Transform(0, 20, options.renderWorldCopies);
21+
const transform = new Transform(0, 20, 0, 60, options.renderWorldCopies);
2222
transform.resize(512, 512);
2323

2424
const camera = attachSimulateFrame(new Camera(transform, {}))
@@ -950,7 +950,7 @@ test('camera', (t) => {
950950
});
951951

952952
t.test('does not throw when cameras current zoom is above maxzoom and an offset creates infinite zoom out factor', (t) => {
953-
const transform = new Transform(0, 20.9999, true);
953+
const transform = new Transform(0, 20.9999, 0, 60, true);
954954
transform.resize(512, 512);
955955
const camera = attachSimulateFrame(new Camera(transform, {}))
956956
.jumpTo({zoom: 21, center:[0, 0]});
@@ -1518,7 +1518,7 @@ test('camera', (t) => {
15181518
});
15191519

15201520
t.test('respects transform\'s maxZoom', (t) => {
1521-
const transform = new Transform(2, 10, false);
1521+
const transform = new Transform(2, 10, 0, 60, false);
15221522
transform.resize(512, 512);
15231523

15241524
const camera = attachSimulateFrame(new Camera(transform, {}));
@@ -1544,7 +1544,7 @@ test('camera', (t) => {
15441544
});
15451545

15461546
t.test('respects transform\'s minZoom', (t) => {
1547-
const transform = new Transform(2, 10, false);
1547+
const transform = new Transform(2, 10, 0, 60, false);
15481548
transform.resize(512, 512);
15491549

15501550
const camera = attachSimulateFrame(new Camera(transform, {}));

test/unit/ui/map.test.js

Lines changed: 98 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -807,14 +807,110 @@ test('Map', (t) => {
807807
t.test('throw on maxZoom smaller than minZoom at init', (t) => {
808808
t.throws(() => {
809809
createMap(t, {minZoom:10, maxZoom:5});
810-
}, new Error(`maxZoom must be greater than minZoom`));
810+
}, new Error(`maxZoom must be greater than or equal to minZoom`));
811811
t.end();
812812
});
813813

814814
t.test('throw on maxZoom smaller than minZoom at init with falsey maxZoom', (t) => {
815815
t.throws(() => {
816816
createMap(t, {minZoom:1, maxZoom:0});
817-
}, new Error(`maxZoom must be greater than minZoom`));
817+
}, new Error(`maxZoom must be greater than or equal to minZoom`));
818+
t.end();
819+
});
820+
821+
t.test('#setMinPitch', (t) => {
822+
const map = createMap(t, {pitch: 20});
823+
map.setMinPitch(10);
824+
map.setPitch(0);
825+
t.equal(map.getPitch(), 10);
826+
t.end();
827+
});
828+
829+
t.test('unset minPitch', (t) => {
830+
const map = createMap(t, {minPitch: 20});
831+
map.setMinPitch(null);
832+
map.setPitch(0);
833+
t.equal(map.getPitch(), 0);
834+
t.end();
835+
});
836+
837+
t.test('#getMinPitch', (t) => {
838+
const map = createMap(t, {pitch: 0});
839+
t.equal(map.getMinPitch(), 0, 'returns default value');
840+
map.setMinPitch(10);
841+
t.equal(map.getMinPitch(), 10, 'returns custom value');
842+
t.end();
843+
});
844+
845+
t.test('ignore minPitchs over maxPitch', (t) => {
846+
const map = createMap(t, {pitch: 0, maxPitch: 10});
847+
t.throws(() => {
848+
map.setMinPitch(20);
849+
});
850+
map.setPitch(0);
851+
t.equal(map.getPitch(), 0);
852+
t.end();
853+
});
854+
855+
t.test('#setMaxPitch', (t) => {
856+
const map = createMap(t, {pitch: 0});
857+
map.setMaxPitch(10);
858+
map.setPitch(20);
859+
t.equal(map.getPitch(), 10);
860+
t.end();
861+
});
862+
863+
t.test('unset maxPitch', (t) => {
864+
const map = createMap(t, {maxPitch:10});
865+
map.setMaxPitch(null);
866+
map.setPitch(20);
867+
t.equal(map.getPitch(), 20);
868+
t.end();
869+
});
870+
871+
t.test('#getMaxPitch', (t) => {
872+
const map = createMap(t, {pitch: 0});
873+
t.equal(map.getMaxPitch(), 60, 'returns default value');
874+
map.setMaxPitch(10);
875+
t.equal(map.getMaxPitch(), 10, 'returns custom value');
876+
t.end();
877+
});
878+
879+
t.test('ignore maxPitchs over minPitch', (t) => {
880+
const map = createMap(t, {minPitch:10});
881+
t.throws(() => {
882+
map.setMaxPitch(0);
883+
});
884+
map.setPitch(10);
885+
t.equal(map.getPitch(), 10);
886+
t.end();
887+
});
888+
889+
t.test('throw on maxPitch smaller than minPitch at init', (t) => {
890+
t.throws(() => {
891+
createMap(t, {minPitch: 10, maxPitch: 5});
892+
}, new Error(`maxPitch must be greater than or equal to minPitch`));
893+
t.end();
894+
});
895+
896+
t.test('throw on maxPitch smaller than minPitch at init with falsey maxPitch', (t) => {
897+
t.throws(() => {
898+
createMap(t, {minPitch: 1, maxPitch: 0});
899+
}, new Error(`maxPitch must be greater than or equal to minPitch`));
900+
t.end();
901+
});
902+
903+
t.test('throw on maxPitch greater than valid maxPitch at init', (t) => {
904+
t.throws(() => {
905+
createMap(t, {maxPitch: 90});
906+
}, new Error(`maxPitch must be less than or equal to 60`));
907+
t.end();
908+
});
909+
910+
t.test('throw on minPitch less than valid minPitch at init', (t) => {
911+
t.throws(() => {
912+
createMap(t, {minPitch: -10});
913+
}, new Error(`minPitch must be greater than or equal to 0`));
818914
t.end();
819915
});
820916

0 commit comments

Comments
 (0)