feat: Add ability to control the max zoom of the map (#2201)

- Resolves #2202

## Launch Checklist

The map max zoom was increased to 24 without the ability to change it.
- https://github.com/maplibre/maputnik/issues/593

The default is 22 for all maplibre instances if you don't override it in
your code.
The zoom levels to over scale is dependent on max zoom, which can affect
how the map looks.
This PR adds the ability to change this.
This was added to the debug dialog:
<img width="446" height="446" alt="image"
src="https://github.com/user-attachments/assets/ea68c392-309f-4476-8922-790670762d56"
/>


 - [x] Briefly describe the changes in this PR.
 - [x] Link to related issues.
- [x] Include before/after visuals or gifs if this PR includes visual
changes.
 - [x] Write tests for all new functionality.
 - [x] Add an entry to `CHANGELOG.md` under the `## main` section.
This commit is contained in:
Harel M
2026-10-05 15:49:55 +03:00
committed by GitHub
parent e172e6c6fc
commit 68c81fc7a3
5 changed files with 59 additions and 12 deletions
+1
View File
@@ -7,6 +7,7 @@
- Exported HTML now loads MapLibre GL JS as an ES module, since v6 no longer ships a UMD bundle - Exported HTML now loads MapLibre GL JS as an ES module, since v6 no longer ships a UMD bundle
- Add `font-faces`, `sky`, `roll` and `centerAltitude` to the style settings modal - Add `font-faces`, `sky`, `roll` and `centerAltitude` to the style settings modal
- The sidebar can now be resized, both as a whole and in the split between the layer list and the layer editor - The sidebar can now be resized, both as a whole and in the split between the layer list and the layer editor
- Allow viewing and changing the map's max zoom in the debug modal
- _...Add new stuff here..._ - _...Add new stuff here..._
### 🐞 Bug fixes ### 🐞 Bug fixes
+24
View File
@@ -64,6 +64,30 @@ describe("modals", () => {
}); });
}); });
describe("debug", () => {
beforeEach(async () => {
await when.setStyle("zoom_7_center_0_51");
await then(get.locationHash()).shouldInclude("#7/51/0");
await when.typeKeys("!");
});
test("max zoom is empty until the user sets it", async () => {
await then(get.elementByTestId("modal:debug.max-zoom")).shouldHaveValue("");
});
test("lowering the max zoom zooms the map out to it", async () => {
await when.setValue("modal:debug.max-zoom", "3");
await then(get.locationHash()).shouldInclude("#3/51/0");
});
test("typing a multi-digit max zoom keeps the map in place", async () => {
await when.type("modal:debug.max-zoom", "{selectall}{backspace}22");
// Let the debounced update reach the map
await when.wait(1000);
await then(get.locationHash()).shouldInclude("#7/51/0");
});
});
describe("export", () => { describe("export", () => {
beforeEach(async () => { beforeEach(async () => {
await when.click("nav:export"); await when.click("nav:export");
+1
View File
@@ -181,6 +181,7 @@ export class App extends React.Component<any, AppState> {
showTileBoundaries: false, showTileBoundaries: false,
showCollisionBoxes: false, showCollisionBoxes: false,
showOverdrawInspector: false, showOverdrawInspector: false,
maxZoom: undefined,
}, },
openlayersDebugOptions: { openlayersDebugOptions: {
debugToolbox: false, debugToolbox: false,
+1 -1
View File
@@ -126,6 +126,7 @@ class MapMaplibreGlInternal extends React.Component<MapMaplibreGlInternalProps,
map.showTileBoundaries = this.props.options?.showTileBoundaries!; map.showTileBoundaries = this.props.options?.showTileBoundaries!;
map.showCollisionBoxes = this.props.options?.showCollisionBoxes!; map.showCollisionBoxes = this.props.options?.showCollisionBoxes!;
map.showOverdrawInspector = this.props.options?.showOverdrawInspector!; map.showOverdrawInspector = this.props.options?.showOverdrawInspector!;
map.setMaxZoom(this.props.options?.maxZoom);
// set the map view when the prop was updated from outside // set the map view when the prop was updated from outside
if (this.props.mapView._from === "app") { if (this.props.mapView._from === "app") {
@@ -152,7 +153,6 @@ class MapMaplibreGlInternal extends React.Component<MapMaplibreGlInternalProps,
container: this.container!, container: this.container!,
style: this.props.mapStyle, style: this.props.mapStyle,
hash: true, hash: true,
maxZoom: 24,
// make root relative urls in stylefiles work as maplibre gl js does // make root relative urls in stylefiles work as maplibre gl js does
// not support this for everything: // not support this for everything:
// https://github.com/maplibre/maplibre-gl-js/issues/6818 // https://github.com/maplibre/maplibre-gl-js/issues/6818
+32 -11
View File
@@ -1,16 +1,19 @@
import React from "react"; import React from "react";
import { debounce } from "lodash";
import { Trans, type WithTranslation, withTranslation } from "react-i18next"; import { Trans, type WithTranslation, withTranslation } from "react-i18next";
import type { MapOptions } from "maplibre-gl";
import { Modal } from "./Modal"; import { Modal } from "./Modal";
import { FieldNumber } from "../FieldNumber";
type ModalDebugInternalProps = { type ModalDebugInternalProps = {
isOpen: boolean isOpen: boolean
renderer: string renderer: string
onChangeMaplibreGlDebug(key: string, checked: boolean): unknown onChangeMaplibreGlDebug(key: string, value: boolean | number | undefined): unknown
onChangeOpenlayersDebug(key: string, checked: boolean): unknown onChangeOpenlayersDebug(key: string, checked: boolean): unknown
onOpenToggle(): void onOpenToggle(): void
maplibreGlDebugOptions?: object maplibreGlDebugOptions?: Partial<MapOptions>
openlayersDebugOptions?: object openlayersDebugOptions?: object
mapView: { mapView: {
zoom: number zoom: number
@@ -23,6 +26,15 @@ type ModalDebugInternalProps = {
class ModalDebugInternal extends React.Component<ModalDebugInternalProps> { class ModalDebugInternal extends React.Component<ModalDebugInternalProps> {
// Typing "22" would otherwise apply 2 first and zoom the map out of the current view
onChangeMaxZoom = debounce((value: number | undefined) => {
this.props.onChangeMaplibreGlDebug("maxZoom", value);
}, 500);
componentWillUnmount() {
this.onChangeMaxZoom.cancel();
}
render() { render() {
const {t, mapView} = this.props; const {t, mapView} = this.props;
@@ -39,15 +51,24 @@ class ModalDebugInternal extends React.Component<ModalDebugInternalProps> {
<section className="maputnik-modal-section maputnik-modal-shortcuts"> <section className="maputnik-modal-section maputnik-modal-shortcuts">
<h1>{t("Options")}</h1> <h1>{t("Options")}</h1>
{this.props.renderer === "mlgljs" && {this.props.renderer === "mlgljs" &&
<ul> <>
{Object.entries(this.props.maplibreGlDebugOptions!).map(([key, val]) => { <ul>
return <li key={key}> {Object.entries(this.props.maplibreGlDebugOptions!).filter((entry): entry is [string, boolean] => typeof entry[1] === "boolean").map(([key, val]) => {
<label> return <li key={key}>
<input type="checkbox" checked={val} onChange={(e) => this.props.onChangeMaplibreGlDebug(key, e.target.checked)} /> {key} <label>
</label> <input type="checkbox" checked={val} onChange={(e) => this.props.onChangeMaplibreGlDebug(key, e.target.checked)} /> {key}
</li>; </label>
})} </li>;
</ul> })}
</ul>
<FieldNumber
label={t("Max Zoom")}
data-wd-key="modal:debug.max-zoom"
value={this.props.maplibreGlDebugOptions!.maxZoom ?? undefined}
min={0}
onChange={this.onChangeMaxZoom}
/>
</>
} }
{this.props.renderer === "ol" && {this.props.renderer === "ol" &&
<ul> <ul>