diff --git a/web/client/actions/__tests__/map-test.js b/web/client/actions/__tests__/map-test.js index 48d6ff1f08..935b1facc2 100644 --- a/web/client/actions/__tests__/map-test.js +++ b/web/client/actions/__tests__/map-test.js @@ -33,6 +33,7 @@ import { clickOnMap, changeMousePointer, changeZoomLevel, + changeCRS, changeMapCrs, changeMapScales, changeMapStyle, @@ -54,7 +55,7 @@ import { updateMapOptions, UPDATE_MAP_OPTIONS } from '../map'; - +import {updateNode} from '../layers'; describe('Test correctness of the map actions', () => { @@ -127,12 +128,50 @@ describe('Test correctness of the map actions', () => { it('changes map crs', () => { const testVal = 'EPSG:4326'; - const retval = changeMapCrs(testVal); + const retval = changeCRS(testVal); expect(retval).toExist(); expect(retval.type).toBe(CHANGE_MAP_CRS); expect(retval.crs).toBe(testVal); }); + it('changes map crs and update resoolutions', () => { + const crs = 'EPSG:4326'; + const thunk = changeMapCrs(crs); + expect(thunk).toExist(); + const dispatchedActions = []; + const dispatch = (action) => { + dispatchedActions.push(action); + }; + thunk(dispatch, + () => ({ + layers: [ { + id: 'layer1', + minResolution: 2000, + maxResolution: 4000 + }, + { + id: 'layer2', + minResolution: 5000 + }], + map: {present: {projection: "EPSG:3857"}} + })); + const expectedActions = [ + updateNode('layer1', 'layer', { minResolution: 0.02197265625, maxResolution: 0.0439453125 }), + updateNode('layer2', 'layer', { minResolution: 0.0439453125 }), + changeCRS(crs) + ]; + for (let i = 0; i < expectedActions.length; i++) { + const expected = expectedActions[i]; + const actual = dispatchedActions[i]; + if (JSON.stringify(expected) !== JSON.stringify(actual)) { + throw new Error( + `Dispatched action at index ${i} does not match expected. \nExpected: ${JSON.stringify( + expected + )}\nActual: ${JSON.stringify(actual)}` + ); + } + } + }); it('changeMapScales', () => { const testScales = [100000, 50000, 25000, 10000, 5000]; diff --git a/web/client/actions/map.js b/web/client/actions/map.js index 33d3a6ad31..3f5e4d37ef 100644 --- a/web/client/actions/map.js +++ b/web/client/actions/map.js @@ -6,6 +6,11 @@ * LICENSE file in the root directory of this source tree. */ +import { getResolutions, convertResolution } from '../utils/MapUtils'; +import { layersSelector } from '../selectors/layers'; +import { projectionSelector } from '../selectors/map'; +import { updateNode } from '../actions/layers'; +import minBy from 'lodash/minBy'; export const CHANGE_MAP_VIEW = 'CHANGE_MAP_VIEW'; export const CLICK_ON_MAP = 'CLICK_ON_MAP'; @@ -81,10 +86,47 @@ export function changeMapView(center, zoom, bbox, size, mapStateSource, projecti }; } +export const changeCRS = (crs) => ({ + type: CHANGE_MAP_CRS, + crs: crs +}); export function changeMapCrs(crs) { - return { - type: CHANGE_MAP_CRS, - crs: crs + return (dispatch, getState = () => {}) => { + const state = getState(); + const sourceCRS = projectionSelector(state); + const layersWithLimits = layersSelector(state).filter(l => l.minResolution || l.maxResolution); + layersWithLimits.forEach(layer => { + const options = {}; + const newResolutions = getResolutions(crs); + if (layer.minResolution) { + options.minResolution = convertResolution(sourceCRS, crs, layer.minResolution).transformedResolution; + const diffs = newResolutions.map((resolution, zoom) => ({ diff: Math.abs(resolution - options.minResolution), zoom })); + const { zoom } = minBy(diffs, 'diff'); + options.minResolution = newResolutions[zoom]; + } + if (layer.maxResolution) { + options.maxResolution = convertResolution(sourceCRS, crs, layer.maxResolution).transformedResolution; + const diffs = newResolutions.map((resolution, zoom) => ({ diff: Math.abs(resolution - options.maxResolution), zoom })); + const { zoom } = minBy(diffs, 'diff'); + // check if min and max resolutions are not the same + options.maxResolution = newResolutions[zoom]; + if (options.minResolution === options.maxResolution) { + if ((zoom - 1) >= 0) { + // increase max res if possible + options.maxResolution = newResolutions[zoom - 1]; + } else if (zoom + 1 < newResolutions.length) { + // decrease max res if possible + options.minResolution = newResolutions[zoom + 1]; + } else { + // keep only min res if none of the previous is happening + options.maxResolution = undefined; + } + } + } + // the minimum difference represents the nearest zoom to the target resolution + dispatch(updateNode(layer.id, "layer", options)); + }); + dispatch(changeCRS(crs)); }; } diff --git a/web/client/components/map/openlayers/Map.jsx b/web/client/components/map/openlayers/Map.jsx index b8208e028b..6be0a92791 100644 --- a/web/client/components/map/openlayers/Map.jsx +++ b/web/client/components/map/openlayers/Map.jsx @@ -529,7 +529,8 @@ class OpenlayersMap extends React.Component { multiWorld: true, // does not allow intermediary zoom levels // we need this at true to set correctly the scale box - constrainResolution: true + constrainResolution: true, + resolutions: this.getResolutions(normalizeSRS(projection)) }, newOptions || {}); return new View(viewOptions); }; diff --git a/web/client/epics/__tests__/map-test.js b/web/client/epics/__tests__/map-test.js index db95e11a39..2606f0c86e 100644 --- a/web/client/epics/__tests__/map-test.js +++ b/web/client/epics/__tests__/map-test.js @@ -9,7 +9,7 @@ import expect from 'expect'; import { resetLimitsOnInit, zoomToExtentEpic, checkMapPermissions } from '../map'; -import { CHANGE_MAP_VIEW, zoomToExtent, CHANGE_MAP_LIMITS, changeMapCrs } from '../../actions/map'; +import { CHANGE_MAP_VIEW, zoomToExtent, CHANGE_MAP_LIMITS, changeCRS } from '../../actions/map'; import { LOAD_MAP_INFO, configureMap } from '../../actions/config'; import { testEpic, addTimeoutEpic, TEST_TIMEOUT } from './epicTestUtils'; import MapUtils from '../../utils/MapUtils'; @@ -226,7 +226,7 @@ describe('map epics', () => { } } }; - testEpic(resetLimitsOnInit, 1, changeMapCrs("EPSG:1234"), ([action]) => { + testEpic(resetLimitsOnInit, 1, changeCRS("EPSG:1234"), ([action]) => { const { restrictedExtent, type, minZoom } = action; expect(restrictedExtent.length).toBe(4); expect(restrictedExtent).toEqual([1, 1, 1, 1]); diff --git a/web/client/utils/MapUtils.js b/web/client/utils/MapUtils.js index bf7a7b5e79..6b2bd82475 100644 --- a/web/client/utils/MapUtils.js +++ b/web/client/utils/MapUtils.js @@ -25,11 +25,15 @@ import { minBy, omit } from 'lodash'; +import { get as getProjectionOL, getPointResolution, transform } from 'ol/proj'; +import { get as getExtent } from 'ol/proj/projections'; import uuidv1 from 'uuid/v1'; import { getUnits, normalizeSRS, reproject } from './CoordinatesUtils'; + import { getProjection } from './ProjectionUtils'; + import { set } from './ImmutableUtils'; import { saveLayer, @@ -339,6 +343,64 @@ export function getScales(projection, dpi) { const dpu = dpi2dpu(dpi, projection); return getResolutions(projection).map((resolution) => resolution * dpu); } + +export function getScale(projection, dpi, resolution) { + const dpu = dpi2dpu(dpi, projection); + return resolution * dpu; +} +/** + * get random coordinates within CRS extent + * @param {string} crs the code of the projection for example EPSG:4346 + * @returns {number[]} the point in [x,y] [lon,lat] + */ +export function getRandomPointInCRS(crs) { + const extent = getExtent(crs); // Get the projection's extent + if (!extent) { + throw new Error(`Extent not available for CRS: ${crs}`); + } + const [minX, minY, maxX, maxY] = extent.extent_; + + // Check if the equator (latitude = 0) is within the CRS extent + const isEquatorWithinExtent = minY <= 0 && maxY >= 0; + + // Generate a random X coordinate within the valid longitude range + const randomX = Math.random() * (maxX - minX) + minX; + + // Set Y to 0 if the equator is within the extent, otherwise generate a random Y + const randomY = isEquatorWithinExtent ? 0 : Math.random() * (maxY - minY) + minY; + + return [randomX, randomY]; +} + +/** + * convert resolution between CRSs + * @param {string} sourceCRS the code of a projection + * @param {string} targetCRS the code of a projection + * @param {number} sourceResolution the resolution to convert + * @returns the converted resolution + */ +export function convertResolution(sourceCRS, targetCRS, sourceResolution) { + const sourceProjection = getProjectionOL(sourceCRS); + const targetProjection = getProjectionOL(targetCRS); + + if (!sourceProjection || !targetProjection) { + throw new Error(`Invalid CRS: ${sourceCRS} or ${targetCRS}`); + } + + // Get a random point in the extent of the source CRS + const randomPoint = getRandomPointInCRS(sourceCRS); + + // Transform the resolution + const transformedResolution = getPointResolution( + sourceProjection, + sourceResolution, + transform(randomPoint, sourceCRS, targetCRS), + targetProjection.getUnits() + ); + + return { randomPoint, transformedResolution }; +} + /** * Convert a resolution to the nearest zoom * @param {number} targetResolution resolution to be converted in zoom diff --git a/web/client/utils/__tests__/MapUtils-test.js b/web/client/utils/__tests__/MapUtils-test.js index cae1e240d6..c647bfc759 100644 --- a/web/client/utils/__tests__/MapUtils-test.js +++ b/web/client/utils/__tests__/MapUtils-test.js @@ -41,7 +41,9 @@ import { mapUpdated, getZoomFromResolution, getResolutionObject, - reprojectZoom + reprojectZoom, + getRandomPointInCRS, + convertResolution } from '../MapUtils'; import { VisualizationModes } from '../MapTypeUtils'; @@ -2416,6 +2418,7 @@ describe('Test the MapUtils', () => { const resolution = 1000; // ~zoom 7 in Web Mercator expect(getZoomFromResolution(resolution)).toBe(7); }); + it('reprojectZoom', () => { expect(reprojectZoom(5, 'EPSG:3857', 'EPSG:4326')).toBe(4); expect(reprojectZoom(5.2, 'EPSG:3857', 'EPSG:4326')).toBe(4); @@ -2431,4 +2434,11 @@ describe('Test the MapUtils', () => { .toEqual({ resolution: 9028, scale: 34121574.80314961, zoom: 4 }); }); }); + it('getRandomPointInCRS', () => { + expect(getRandomPointInCRS('EPSG:3857').length).toBe(2); + expect(getRandomPointInCRS('EPSG:4326').length).toBe(2); + }); + it('convertResolution', () => { + expect(convertResolution('EPSG:3857', 'EPSG:4326', 2000).transformedResolution).toBe(0.017986440587896155); + }); });