From c629e10af748c721cb12ebe5b641ed15fef37f46 Mon Sep 17 00:00:00 2001 From: Lukas Weber <32765578+lukasalexanderweber@users.noreply.github.com> Date: Wed, 21 Jan 2026 22:04:21 +0100 Subject: [PATCH] set correct map view if opened stylefile provides a map view and the current map is empty (#1552) ## Launch Checklist closes https://github.com/maplibre/maputnik/issues/1546 - [x] Link to related issues. https://github.com/maplibre/maputnik/issues/1546 - [x] Write tests for all new functionality. - [x] Add an entry to `CHANGELOG.md` under the `## main` section. --------- Co-authored-by: pre-commit-ci[bot] <66853113+pre-commit-ci[bot]@users.noreply.github.com> Co-authored-by: Harel M --- CHANGELOG.md | 1 + cypress/e2e/map.cy.ts | 16 ++++++++++++ cypress/e2e/maputnik-cypress-helper.ts | 1 + cypress/e2e/maputnik-driver.ts | 26 ++++++++++++++++++- ...le-style-with-zoom-5-and-center-50-50.json | 19 ++++++++++++++ ...ple-style-with-zoom-7-and-center-0-51.json | 17 ++++++++++++ src/components/App.tsx | 23 +++++++++++++++- src/components/MapMaplibreGl.tsx | 17 ++++++++++-- 8 files changed, 116 insertions(+), 4 deletions(-) create mode 100644 cypress/fixtures/example-style-with-zoom-5-and-center-50-50.json create mode 100644 cypress/fixtures/example-style-with-zoom-7-and-center-0-51.json diff --git a/CHANGELOG.md b/CHANGELOG.md index 2a6c9a9c..f24f77ed 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -12,6 +12,7 @@ - Upgraded codemirror from version 5 to version 6 - Add code editor to allow editing the entire style - Add support for sprite object in setting modal +- Set the correct map view when opening a new style on an empty map - Allow root-relative urls in the stylefile - _...Add new stuff here..._ diff --git a/cypress/e2e/map.cy.ts b/cypress/e2e/map.cy.ts index 1845dd8d..c131422c 100644 --- a/cypress/e2e/map.cy.ts +++ b/cypress/e2e/map.cy.ts @@ -22,6 +22,21 @@ describe("map", () => { "Zoom: " + (zoomLevel + 1) ); }); + + it("via style file definition", () => { + when.setStyle("zoom_7_center_0_51"); + then(get.elementByTestId("maplibre:ctrl-zoom")).shouldBeVisible(); + then(get.elementByTestId("maplibre:ctrl-zoom")).shouldContainText( + "Zoom: " + (7) + ); + then(get.locationHash().should("contain", "#7/51/0")); + + // opening another stylefile does not update the map view again + // as discussed in https://github.com/maplibre/maputnik/issues/1546 + when.openASecondStyleWithDifferentZoomAndCenter(); + then(get.locationHash().should("contain", "#7/51/0")); + + }); }); describe("search", () => { @@ -33,6 +48,7 @@ describe("map", () => { describe("popup", () => { beforeEach(() => { when.setStyle("rectangles"); + then(get.locationHash().should("exist")); }); it("should open on feature click", () => { when.clickCenter("maplibre:map"); diff --git a/cypress/e2e/maputnik-cypress-helper.ts b/cypress/e2e/maputnik-cypress-helper.ts index 4ade0c56..40ff36cc 100644 --- a/cypress/e2e/maputnik-cypress-helper.ts +++ b/cypress/e2e/maputnik-cypress-helper.ts @@ -10,6 +10,7 @@ export default class MaputnikCypressHelper { }; public get = { + locationHash: (): Cypress.Chainable => cy.location("hash"), ...this.helper.get, }; diff --git a/cypress/e2e/maputnik-driver.ts b/cypress/e2e/maputnik-driver.ts index 72fdf89e..853de436 100644 --- a/cypress/e2e/maputnik-driver.ts +++ b/cypress/e2e/maputnik-driver.ts @@ -92,6 +92,20 @@ export class MaputnikDriver { fixture: "example-style-with-fonts.json", }, }); + this.helper.given.interceptAndMockResponse({ + method: "GET", + url: baseUrl + "example-style-with-zoom-7-and-center-0-51.json", + response: { + fixture: "example-style-with-zoom-7-and-center-0-51.json", + }, + }); + this.helper.given.interceptAndMockResponse({ + method: "GET", + url: baseUrl + "example-style-with-zoom-5-and-center-50-50.json", + response: { + fixture: "example-style-with-zoom-5-and-center-50-50.json", + }, + }); this.helper.given.interceptAndMockResponse({ method: "GET", url: "*example.local/*", @@ -120,13 +134,20 @@ export class MaputnikDriver { waitForExampleFileResponse: () => { this.helper.when.waitForResponse("example-style.json"); }, + openASecondStyleWithDifferentZoomAndCenter: () => { + cy.contains("button", "Open").click(); + cy.get('[data-wd-key="modal:open.url.input"]') + .should("be.enabled") + .clear() + .type("http://localhost:8888/example-style-with-zoom-5-and-center-50-50.json{enter}"); + }, chooseExampleFile: () => { this.helper.given.fixture("example-style.json", "example-style.json"); this.helper.when.openFileByFixture("example-style.json", "modal:open.file.button", "modal:open.file.input"); this.helper.when.wait(200); }, setStyle: ( - styleProperties: "geojson" | "raster" | "both" | "layer" | "rectangles" | "font" | "", + styleProperties: "geojson" | "raster" | "both" | "layer" | "rectangles" | "font" | "zoom_7_center_0_51" | "", zoom?: number ) => { const url = new URL(baseUrl); @@ -149,6 +170,9 @@ export class MaputnikDriver { case "font": url.searchParams.set("style", baseUrl + "example-style-with-fonts.json"); break; + case "zoom_7_center_0_51": + url.searchParams.set("style", baseUrl + "example-style-with-zoom-7-and-center-0-51.json"); + break; } if (zoom) { diff --git a/cypress/fixtures/example-style-with-zoom-5-and-center-50-50.json b/cypress/fixtures/example-style-with-zoom-5-and-center-50-50.json new file mode 100644 index 00000000..4751e5f0 --- /dev/null +++ b/cypress/fixtures/example-style-with-zoom-5-and-center-50-50.json @@ -0,0 +1,19 @@ +{ + "id": "test-style", + "center": [50,50], + "zoom": 5, + "version": 8, + "name": "Test Style", + "sources": { + "rectangles": { + "type": "geojson", + "data": { + "type": "FeatureCollection", + "features": [] + } + } + }, + "glyphs": "https://example.local/fonts/{fontstack}/{range}.pbf", + "sprites": "https://example.local/fonts/{fontstack}/{range}.pbf", + "layers": [] +} diff --git a/cypress/fixtures/example-style-with-zoom-7-and-center-0-51.json b/cypress/fixtures/example-style-with-zoom-7-and-center-0-51.json new file mode 100644 index 00000000..ecafd740 --- /dev/null +++ b/cypress/fixtures/example-style-with-zoom-7-and-center-0-51.json @@ -0,0 +1,17 @@ +{ + "id": "test-style", + "center": [0,51], + "zoom": 7, + "version": 8, + "name": "Test Style", + "sources": { + "rectangles": { + "type": "geojson", + "data": { + "type": "FeatureCollection", + "features": [] + } + } + }, + "layers": [] +} diff --git a/src/components/App.tsx b/src/components/App.tsx index d78823a1..93382b46 100644 --- a/src/components/App.tsx +++ b/src/components/App.tsx @@ -99,6 +99,7 @@ type AppState = { lng: number, lat: number, }, + _from: "map" | "app" }, maplibreGlDebugOptions: Partial & { showTileBoundaries: boolean, @@ -148,6 +149,7 @@ export default class App extends React.Component { lng: 0, lat: 0, }, + _from: "app" }, isOpen: { settings: false, @@ -335,6 +337,13 @@ export default class App extends React.Component { ...opts, }; + + // Detect empty style + const oldStyle = this.state.mapStyle; + const isEmptySources = !oldStyle.sources || Object.keys(oldStyle.sources).length === 0; + const isEmptyLayers = !oldStyle.layers || oldStyle.layers.length === 0; + const isEmptyStyle = isEmptySources && isEmptyLayers; + // For the style object, find the urls that has "{key}" and insert the correct API keys // Without this, going from e.g. MapTiler to OpenLayers and back will lose the maptlier key. @@ -466,15 +475,25 @@ export default class App extends React.Component { this.saveStyle(newStyle); } + const zoom = newStyle?.zoom; + const center = newStyle?.center; + this.setState({ mapStyle: newStyle, dirtyMapStyle: dirtyMapStyle, + mapView: isEmptyStyle && zoom && center ? { + zoom: zoom, + center: { + lng: center[0], + lat: center[1], + }, + _from: "app" + } : this.state.mapView, errors: mappedErrors, }, () => { this.fetchSources(); this.setStateInUrl(); }); - }; onUndo = () => { @@ -665,6 +684,7 @@ export default class App extends React.Component { lng: number, lat: number, }, + _from: "map" | "app" }) => { this.setState({ mapView, @@ -676,6 +696,7 @@ export default class App extends React.Component { const mapProps = { mapStyle: (dirtyMapStyle || mapStyle), + mapView: this.state.mapView, replaceAccessTokens: (mapStyle: StyleSpecification) => { return style.replaceAccessTokens(mapStyle, { allowFallback: true diff --git a/src/components/MapMaplibreGl.tsx b/src/components/MapMaplibreGl.tsx index 4b3f0c2d..a51be7f5 100644 --- a/src/components/MapMaplibreGl.tsx +++ b/src/components/MapMaplibreGl.tsx @@ -52,6 +52,14 @@ type MapMaplibreGlInternalProps = { onDataChange?(event: {map: Map | null}): unknown onLayerSelect(index: number): void mapStyle: StyleSpecification + mapView: { + zoom: number, + center: { + lng: number, + lat: number, + }, + _from: "map" | "app" + }; inspectModeEnabled: boolean highlightedLayer?: HighlightedLayer options?: Partial & { @@ -60,7 +68,7 @@ type MapMaplibreGlInternalProps = { showOverdrawInspector?: boolean } replaceAccessTokens(mapStyle: StyleSpecification): StyleSpecification - onChange(value: {center: LngLat, zoom: number}): unknown + onChange(value: {center: LngLat, zoom: number, _from: "map" | "app"}): unknown } & WithTranslation; type MapMaplibreGlState = { @@ -117,6 +125,11 @@ class MapMaplibreGlInternal extends React.Component { const center = map.getCenter(); const zoom = map.getZoom(); - this.props.onChange({center, zoom}); + this.props.onChange({center, zoom, _from: "map"}); }; mapViewChange();