Skip to content

Commit 4e68715

Browse files
#11779: FIX Setting the map projection freezes MapStore causing OOM if resolutions configured in new.json file with a different projection (#11788)
* #11779: FIX Setting the map projection crashes MapStore if resolutions configured in new.json file with a different projection Description: - prevent OOM by enforcing projection-aware resolutions in new.json file in mapOptions.view - Add strict check: only use configured resolutions if `view.projection` matches target SRS - Eliminates infinite tile loading and out-of-memory crashes when switching to different CRS - fix print file by passing the projection prop with resolutions to gurantee the above check - add unit test * #11779: FIX Setting the map projection crashes MapStore if resolutions configured in new.json file with a different projection Description: - edit unit test uses resolutions without projection * - resolve review comments by editing the order of viewOptions in openlayers/Map.js
1 parent f5bc240 commit 4e68715

3 files changed

Lines changed: 91 additions & 12 deletions

File tree

web/client/components/map/openlayers/Map.jsx

Lines changed: 50 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -353,14 +353,34 @@ class OpenlayersMap extends React.Component {
353353
* - custom grid set with custom extent. You need to customize the projection definition extent to make it work.
354354
* - custom grid set is partially supported by mapOptions.view.resolutions but this is not managed by projection change yet
355355
* - custom tile sizes
356-
*
356+
* ** NOTES**: If mapOptions.view.resolutions + mapOptions.view.projection are provided and match → will use them.
357+
* - Else → will compute resolutions for provided/mapView projection.
357358
*/
358359
getResolutions = (srs) => {
359-
if (this.props.mapOptions && this.props.mapOptions.view && this.props.mapOptions.view.resolutions) {
360-
return this.props.mapOptions.view.resolutions;
360+
// Resolve requested projection
361+
const requestedProj = srs
362+
? getProjection(srs)
363+
: (this.map?.getView()?.getProjection());
364+
const requestedSRS = normalizeSRS(srs || requestedProj.getCode());
365+
366+
// Check for explicitly configured resolutions + matching projection
367+
const viewOptions = this.props?.mapOptions?.view || {};
368+
const configuredResolutions = viewOptions.resolutions;
369+
const configuredResProjection = viewOptions.projection && normalizeSRS(viewOptions.projection);
370+
371+
// If resolutions are explicitly configured *and* tied to a projection that matches our target,
372+
// return them directly — avoids recomputation and ensures consistency with custom tile sources.
373+
if (
374+
configuredResolutions &&
375+
Array.isArray(configuredResolutions) &&
376+
configuredResProjection &&
377+
requestedSRS === configuredResProjection
378+
) {
379+
return configuredResolutions;
361380
}
362-
const projection = srs ? getProjection(srs) : this.map.getView().getProjection();
363-
const extent = projection.getExtent();
381+
382+
// Otherwise compute dynamically
383+
const extent = requestedProj.getExtent();
364384
return getResolutionsForProjection(
365385
srs ?? this.map.getView().getProjection().getCode(),
366386
{
@@ -513,15 +533,34 @@ class OpenlayersMap extends React.Component {
513533
};
514534

515535
createView = (center, zoom, projection, options, limits = {}) => {
536+
const srs = normalizeSRS(projection);
516537
// limit has a crs defined
517538
const extent = limits.restrictedExtent && limits.crs && reprojectBbox(limits.restrictedExtent, limits.crs, normalizeSRS(projection));
518-
const newOptions = !options || (options && !options.view) ? Object.assign({}, options, { extent }) : Object.assign({}, options);
539+
540+
// Determine whether to use configured resolutions
541+
const configuredResolutions = options?.resolutions;
542+
const configuredProj = normalizeSRS(options?.projection);
543+
544+
let resolutionsToUse;
545+
if (configuredResolutions && configuredProj === normalizeSRS(projection)) {
546+
// use provided resolutions (keep backward compatibility)
547+
resolutionsToUse = configuredResolutions;
548+
} else {
549+
// compute resolutions dynamically (e.g., EPSG:4326)
550+
resolutionsToUse = this.getResolutions(normalizeSRS(projection));
551+
}
552+
const newOptions = {
553+
...options,
554+
projection: srs,
555+
resolutions: resolutionsToUse,
556+
extent: options?.extent !== undefined ? options.extent : extent
557+
};
519558
/*
520559
* setting the zoom level in the localConfig file is co-related to the projection extent(size)
521560
* it is recommended to use projections with the same coverage area (extent). If you want to have the same restricted zoom level (minZoom)
522561
*/
523562
const viewOptions = Object.assign({}, {
524-
projection: normalizeSRS(projection),
563+
projection: srs,
525564
center: [center.x, center.y],
526565
zoom: zoom,
527566
minZoom: limits.minZoom,
@@ -530,8 +569,10 @@ class OpenlayersMap extends React.Component {
530569
// does not allow intermediary zoom levels
531570
// we need this at true to set correctly the scale box
532571
constrainResolution: true,
533-
resolutions: this.getResolutions(normalizeSRS(projection))
534-
}, newOptions || {});
572+
resolutions: this.getResolutions(srs)
573+
},
574+
newOptions || {}
575+
);
535576
return new View(viewOptions);
536577
};
537578

web/client/components/map/openlayers/__tests__/Map-test.jsx

Lines changed: 39 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1404,7 +1404,7 @@ describe('OpenlayersMap', () => {
14041404
<OpenlayersMap
14051405
center={{y: 43.9, x: 10.3}}
14061406
zoom={11}
1407-
mapOptions={{view: { resolutions }}}
1407+
mapOptions={{view: { resolutions, projection: "EPSG:3857" }}}
14081408
>
14091409
<OpenlayersLayer type="wms" srs="EPSG:3857" options={options} />
14101410
</OpenlayersMap>, document.getElementById("map")
@@ -1460,7 +1460,7 @@ describe('OpenlayersMap', () => {
14601460
<OpenlayersMap
14611461
center={{y: 43.9, x: 10.3}}
14621462
zoom={11}
1463-
mapOptions={{view: { resolutions }}}
1463+
mapOptions={{view: { resolutions, projection: "EPSG:3857" }}}
14641464
>
14651465
<OpenlayersLayer type="wms" srs="EPSG:3857" options={options} />
14661466
</OpenlayersMap>, document.getElementById("map")
@@ -1523,4 +1523,41 @@ describe('OpenlayersMap', () => {
15231523
// center is modified
15241524
expect(map.map.getView().getCenter()).toEqual([10.3346773790, 43.9323234388]);
15251525
});
1526+
it('should correctly apply view projection without propagating to zoom changes', () => {
1527+
const resolutions = [0.0005, 0.0004, 0.0003, 0.0002];
1528+
const map = ReactDOM.render(
1529+
<OpenlayersMap
1530+
center={{y: 45, x: 10}}
1531+
zoom={2}
1532+
projection="EPSG:4326"
1533+
mapOptions={{ view: { projection: 'EPSG:4326', resolutions } }}
1534+
/>,
1535+
document.getElementById("map")
1536+
);
1537+
1538+
const view = map.map.getView();
1539+
expect(view.getProjection().getCode()).toBe('EPSG:4326');
1540+
expect(view.getResolutions()).toEqual(resolutions); // Custom resolutions applied
1541+
1542+
// Simulate a zoom change
1543+
view.setZoom(3);
1544+
expect(view.getProjection().getCode()).toBe('EPSG:4326');
1545+
1546+
// Simulate receiving new props with a different projection
1547+
ReactDOM.render(
1548+
<OpenlayersMap
1549+
center={{y: 45, x: 10}}
1550+
zoom={3}
1551+
projection="EPSG:3857"
1552+
mapOptions={{ view: { projection: 'EPSG:4326', resolutions } }}
1553+
/>,
1554+
document.getElementById("map")
1555+
);
1556+
1557+
const updatedView = map.map.getView();
1558+
updatedView.setZoom(5);
1559+
expect(updatedView.getProjection().getCode()).toBe('EPSG:3857');
1560+
expect(updatedView.getResolutions()).toNotEqual(resolutions);
1561+
});
1562+
15261563
});

web/client/components/print/MapPreview.jsx

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -149,7 +149,8 @@ class MapPreview extends React.Component {
149149
let mapOptions = !isEmpty(resolutions) || !isNil(this.props.rotation) ? {
150150
view: {
151151
...(!isEmpty(resolutions) && {resolutions}),
152-
rotation: !isNil(this.props.rotation) ? Number(this.props.rotation) : 0
152+
rotation: !isNil(this.props.rotation) ? Number(this.props.rotation) : 0,
153+
projection
153154
}
154155
} : {};
155156

0 commit comments

Comments
 (0)