From 68c81fc7a32a29ce337855f1f885ad82c7c20c0f Mon Sep 17 00:00:00 2001 From: Harel M Date: Mon, 5 Oct 2026 15:49:55 +0300 Subject: [PATCH] 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: image - [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. --- CHANGELOG.md | 1 + e2e/modals.spec.ts | 24 ++++++++++++++++ src/components/App.tsx | 1 + src/components/MapMaplibreGl.tsx | 2 +- src/components/modals/ModalDebug.tsx | 43 +++++++++++++++++++++------- 5 files changed, 59 insertions(+), 12 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index a5f3d01c..f36b2eff 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -7,6 +7,7 @@ - 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 - 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..._ ### 🐞 Bug fixes diff --git a/e2e/modals.spec.ts b/e2e/modals.spec.ts index ca60542a..9d4bf88d 100644 --- a/e2e/modals.spec.ts +++ b/e2e/modals.spec.ts @@ -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", () => { beforeEach(async () => { await when.click("nav:export"); diff --git a/src/components/App.tsx b/src/components/App.tsx index 2cbcdc50..aad9d6cd 100644 --- a/src/components/App.tsx +++ b/src/components/App.tsx @@ -181,6 +181,7 @@ export class App extends React.Component { showTileBoundaries: false, showCollisionBoxes: false, showOverdrawInspector: false, + maxZoom: undefined, }, openlayersDebugOptions: { debugToolbox: false, diff --git a/src/components/MapMaplibreGl.tsx b/src/components/MapMaplibreGl.tsx index 4441fbfe..d6135b35 100644 --- a/src/components/MapMaplibreGl.tsx +++ b/src/components/MapMaplibreGl.tsx @@ -126,6 +126,7 @@ class MapMaplibreGlInternal extends React.Component openlayersDebugOptions?: object mapView: { zoom: number @@ -23,6 +26,15 @@ type ModalDebugInternalProps = { class ModalDebugInternal extends React.Component { + // 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() { const {t, mapView} = this.props; @@ -39,15 +51,24 @@ class ModalDebugInternal extends React.Component {

{t("Options")}

{this.props.renderer === "mlgljs" && -
    - {Object.entries(this.props.maplibreGlDebugOptions!).map(([key, val]) => { - return
  • - -
  • ; - })} -
+ <> +
    + {Object.entries(this.props.maplibreGlDebugOptions!).filter((entry): entry is [string, boolean] => typeof entry[1] === "boolean").map(([key, val]) => { + return
  • + +
  • ; + })} +
+ + } {this.props.renderer === "ol" &&