diff --git a/web/client/components/map/openlayers/Map.jsx b/web/client/components/map/openlayers/Map.jsx index 3b90b96054..a90d3cdbc9 100644 --- a/web/client/components/map/openlayers/Map.jsx +++ b/web/client/components/map/openlayers/Map.jsx @@ -20,7 +20,8 @@ import PropTypes from 'prop-types'; import React from 'react'; import assign from 'object-assign'; -import {reproject, reprojectBbox, normalizeLng, normalizeSRS, getExtentForProjection} from '../../../utils/CoordinatesUtils'; +import {reproject, reprojectBbox, normalizeLng, normalizeSRS } from '../../../utils/CoordinatesUtils'; +import { getProjection as msGetProjection } from '../../../utils/ProjectionUtils'; import ConfigUtils from '../../../utils/ConfigUtils'; import mapUtils, { getResolutionsForProjection } from '../../../utils/MapUtils'; import projUtils from '../../../utils/openlayers/projUtils'; @@ -180,9 +181,8 @@ class OpenlayersMap extends React.Component { map.on('moveend', this.updateMapInfoState); map.on('singleclick', (event) => { if (this.props.onClick && !this.map.disabledListeners.singleclick) { - let view = this.map.getView(); let pos = event.coordinate.slice(); - let projectionExtent = view.getProjection().getExtent() || getExtentForProjection(this.props.projection); + let projectionExtent = this.getExtent(this.map, this.props); if (this.props.projection === 'EPSG:4326') { pos[0] = normalizeLng(pos[0]); } @@ -374,12 +374,20 @@ class OpenlayersMap extends React.Component { const extent = projection.getExtent(); return getResolutionsForProjection( srs ?? this.map.getView().getProjection().getCode(), - this.props.mapOptions.minResolution, - this.props.mapOptions.maxResolution, - this.props.mapOptions.minZoom, - this.props.mapOptions.maxZoom, - this.props.mapOptions.zoomFactor, - extent); + { + minResolution: this.props.mapOptions.minResolution, + maxResolution: this.props.mapOptions.maxResolution, + minZoom: this.props.mapOptions.minZoom, + maxZoom: this.props.mapOptions.maxZoom, + zoomFactor: this.props.mapOptions.zoomFactor, + extent + } + ); + }; + + getExtent = (map, props) => { + const view = map.getView(); + return view.getProjection().getExtent() || msGetProjection(props.projection).extent; }; render() { @@ -439,7 +447,7 @@ class OpenlayersMap extends React.Component { updateMapInfoState = () => { let view = this.map.getView(); let tempCenter = view.getCenter(); - let projectionExtent = view.getProjection().getExtent() || getExtentForProjection(this.props.projection); + let projectionExtent = this.getExtent(this.map, this.props); const crs = view.getProjection().getCode(); // some projections are repeated on the x axis // and they need to be updated also if the center is outside of the projection extent diff --git a/web/client/components/map/openlayers/__tests__/Map-test.jsx b/web/client/components/map/openlayers/__tests__/Map-test.jsx index e70c9fcb27..079285a8e7 100644 --- a/web/client/components/map/openlayers/__tests__/Map-test.jsx +++ b/web/client/components/map/openlayers/__tests__/Map-test.jsx @@ -1054,7 +1054,7 @@ describe('OpenlayersMap', () => { it('test getResolutions default', () => { const maxResolution = 2 * 20037508.34; const tileSize = 256; - const expectedResolutions = Array.from(Array(29).keys()).map( k=> maxResolution / tileSize / Math.pow(2, k)); + const expectedResolutions = Array.from(Array(31).keys()).map( k=> maxResolution / tileSize / Math.pow(2, k)); let map = ReactDOM.render(, document.getElementById("map")); expect(map.getResolutions().length).toBe(expectedResolutions.length); // NOTE: round @@ -1083,7 +1083,7 @@ describe('OpenlayersMap', () => { proj.defs(projectionDefs[0].code, projectionDefs[0].def); const maxResolution = 1847542.2626266503 - 1241482.0019432348; const tileSize = 256; - const expectedResolutions = Array.from(Array(29).keys()).map(k => maxResolution / tileSize / Math.pow(2, k)); + const expectedResolutions = Array.from(Array(31).keys()).map(k => maxResolution / tileSize / Math.pow(2, k)); let map = ReactDOM.render( { expect(mouseWheelPresent.getActive()).toBe(true); }); }); + it('should create the layer resolutions based on projection and not the map resolutions', () => { + const options = { + url: '/geoserver/wms', + name: 'workspace:layer', + visibility: true + }; + const resolutions = [ + 529.1666666666666, + 317.5, + 158.75, + 79.375, + 26.458333333333332, + 19.84375, + 10.583333333333332, + 5.291666666666666, + 2.645833333333333, + 1.3229166666666665, + 0.6614583333333333, + 0.396875, + 0.13229166666666667, + 0.079375, + 0.0396875, + 0.021166666666666667 + ]; + const map = ReactDOM.render( + + + , document.getElementById("map") + ); + expect(map).toBeTruthy(); + expect(map.map.getView().getResolutions().length).toBe(resolutions.length); + expect(map.map.getLayers().getLength()).toBe(1); + expect(map.map.getLayers().getArray()[0].getSource().getTileGrid().getResolutions().length).toBe(31); + }); describe("hookRegister", () => { it("default", () => { const map = ReactDOM.render(, document.getElementById("map")); diff --git a/web/client/components/map/openlayers/plugins/WMSLayer.js b/web/client/components/map/openlayers/plugins/WMSLayer.js index 87ad6931ce..7a682508e6 100644 --- a/web/client/components/map/openlayers/plugins/WMSLayer.js +++ b/web/client/components/map/openlayers/plugins/WMSLayer.js @@ -15,6 +15,7 @@ import isArray from 'lodash/isArray'; import assign from 'object-assign'; import axios from '../../../../libs/ajax'; import CoordinatesUtils from '../../../../utils/CoordinatesUtils'; +import { getProjection } from '../../../../utils/ProjectionUtils'; import {needProxy, getProxyUrl} from '../../../../utils/ProxyUtils'; import { getConfigProp } from '../../../../utils/ConfigUtils'; @@ -22,7 +23,7 @@ import {optionsToVendorParams} from '../../../../utils/VendorParamsUtils'; import {addAuthenticationToSLD, addAuthenticationParameter, getAuthenticationHeaders} from '../../../../utils/SecurityUtils'; import { creditsToAttribution, getWMSVendorParams } from '../../../../utils/LayersUtils'; -import MapUtils from '../../../../utils/MapUtils'; +import { getResolutionsForProjection } from '../../../../utils/MapUtils'; import {loadTile, getElevation as getElevationFunc} from '../../../../utils/ElevationUtils'; import ImageLayer from 'ol/layer/Image'; @@ -189,6 +190,24 @@ function getElevation(pos) { } const toOLAttributions = credits => credits && creditsToAttribution(credits) || undefined; +const generateTileGrid = (options, map) => { + const mapSrs = map?.getView()?.getProjection()?.getCode() || 'EPSG:3857'; + const normalizedSrs = CoordinatesUtils.normalizeSRS(options.srs || mapSrs, options.allowedSRS); + const extent = get(normalizedSrs).getExtent() || getProjection(normalizedSrs).extent; + const tileSize = options.tileSize ? options.tileSize : 256; + const resolutions = options.resolutions || getResolutionsForProjection(normalizedSrs, { + tileWidth: tileSize, + tileHeight: tileSize, + extent + }); + const origin = options.origin ? options.origin : [extent[0], extent[1]]; + return new TileGrid({ + extent, + resolutions, + tileSize, + origin + }); +}; const createLayer = (options, map) => { const urls = getWMSURLs(isArray(options.url) ? options.url : [options.url]); @@ -215,20 +234,12 @@ const createLayer = (options, map) => { }) }); } - const mapSrs = map && map.getView() && map.getView().getProjection() && map.getView().getProjection().getCode() || 'EPSG:3857'; - const normalizedSrs = CoordinatesUtils.normalizeSRS(options.srs || mapSrs, options.allowedSRS); - const extent = get(normalizedSrs).getExtent() || CoordinatesUtils.getExtentForProjection(normalizedSrs).extent; const sourceOptions = addTileLoadFunction({ attributions: toOLAttributions(options.credits), urls: urls, crossOrigin: options.crossOrigin, params: queryParameters, - tileGrid: new TileGrid({ - extent: extent, - resolutions: options.resolutions || MapUtils.getResolutions(), - tileSize: options.tileSize ? options.tileSize : 256, - origin: options.origin ? options.origin : [extent[0], extent[1]] - }), + tileGrid: generateTileGrid(options, map), tileLoadFunction: loadFunction(options, headers) }, options); const wmsSource = new TileWMS({ ...sourceOptions }); @@ -308,16 +319,11 @@ Layers.registerType('wms', { if (oldOptions.srs !== newOptions.srs) { const normalizedSrs = CoordinatesUtils.normalizeSRS(newOptions.srs, newOptions.allowedSRS); - const extent = get(normalizedSrs).getExtent() || CoordinatesUtils.getExtentForProjection(normalizedSrs).extent; + const extent = get(normalizedSrs).getExtent() || getProjection(normalizedSrs).extent; if (newOptions.singleTile && !newIsVector) { layer.setExtent(extent); } else { - const tileGrid = new TileGrid({ - extent: extent, - resolutions: newOptions.resolutions || MapUtils.getResolutions(), - tileSize: newOptions.tileSize ? newOptions.tileSize : 256, - origin: newOptions.origin ? newOptions.origin : [extent[0], extent[1]] - }); + const tileGrid = generateTileGrid(newOptions, map); wmsSource.tileGrid = tileGrid; if (vectorSource) { vectorSource.tileGrid = tileGrid; diff --git a/web/client/utils/CoordinatesUtils.js b/web/client/utils/CoordinatesUtils.js index 14bc63f87b..7473097d38 100644 --- a/web/client/utils/CoordinatesUtils.js +++ b/web/client/utils/CoordinatesUtils.js @@ -35,7 +35,7 @@ import bboxPolygon from '@turf/bbox-polygon'; import overlap from '@turf/boolean-overlap'; import contains from '@turf/boolean-contains'; import turfBbox from '@turf/bbox'; -import { getConfigProp } from './ConfigUtils'; +import { getProjection } from './ProjectionUtils'; let CoordinatesUtils; @@ -1037,26 +1037,6 @@ export const getPolygonFromCircle = (center, radius, units = "degrees", steps = return turfCircle(center, radius, {steps, units}); }; -/** - * Returns an array of projections - * @return {array} of projection Definitions [{code, extent}] - */ -export const getProjections = () => { - const projections = (getConfigProp('projectionDefs') || []).concat([{code: "EPSG:3857", extent: [-20026376.39, -20048966.10, 20026376.39, 20048966.10]}, - {code: "EPSG:4326", extent: [-180, -90, 180, 90]} - ]); - return projections; -}; - -/** - * Return a projection from a list of projections - * @param code {string} code for the projection EPSG:3857 - * @return {object} {extent, code} fallsback to default {extent: [-20026376.39, -20048966.10, 20026376.39, 20048966.10]} - */ -export const getExtentForProjection = (code = "EPSG:3857") => { - return getProjections().find(project => project.code === code) || {extent: [-20026376.39, -20048966.10, 20026376.39, 20048966.10]}; -}; - /** * Return a boolean to show if a layer fits within a boundary/extent * @param layer {object} to check if fits with in a projection boundary @@ -1064,7 +1044,7 @@ export const getExtentForProjection = (code = "EPSG:3857") => { */ export const checkIfLayerFitsExtentForProjection = (layer = {}) => { const crs = layer.bbox?.crs || "EPSG:3857"; - const [crsMinX, crsMinY, crsMaxX, crsMaxY] = getExtentForProjection(crs).extent; + const [crsMinX, crsMinY, crsMaxX, crsMaxY] = getProjection(crs).extent; const [minx, minY, maxX, maxY] = turfBbox({type: 'FeatureCollection', features: layer.features || []}); return ((minx >= crsMinX) && (minY >= crsMinY) && (maxX <= crsMaxX) && (maxY <= crsMaxY)); }; @@ -1165,7 +1145,6 @@ CoordinatesUtils = { getPolygonFromCircle, checkIfLayerFitsExtentForProjection, getLonLatFromPoint, - getExtentForProjection, convertRadianToDegrees, convertDegreesToRadian }; diff --git a/web/client/utils/MapUtils.js b/web/client/utils/MapUtils.js index 611fd13431..68fdc37d5c 100644 --- a/web/client/utils/MapUtils.js +++ b/web/client/utils/MapUtils.js @@ -28,7 +28,8 @@ import { import uuidv1 from 'uuid/v1'; -import { getExtentForProjection, getUnits, normalizeSRS, reproject } from './CoordinatesUtils'; +import { getUnits, normalizeSRS, reproject } from './CoordinatesUtils'; +import { getProjection } from './ProjectionUtils'; import { set } from './ImmutableUtils'; import { saveLayer, @@ -213,13 +214,29 @@ export function getGoogleMercatorResolutions(minZoom, maxZoom, dpi) { * - custom grid set with custom extent. You need to customize the projection definition extent to make it work. * - custom grid set is partially supported by mapOptions.view.resolutions but this is not managed by projection change yet * - custom tile sizes - * + * @param {string} srs projection code + * @param {object} options optional configuration + * @param {number} options.minResolution minimum resolution of the tile grid pyramid, default computed based on minimum zoom + * @param {number} options.maxResolution maximum resolution of the tile grid pyramid, default computed based on maximum zoom + * @param {number} options.minZoom minimum zoom of the tile grid pyramid, default 0 + * @param {number} options.maxZoom maximum zoom of the tile grid pyramid, default 30 + * @param {number} options.zoomFactor zoom factor, default 2 + * @param {array} options.extent extent of the tile grid pyramid in the projection coordinates, [minx, miny, maxx, maxy], default maximum extent of the projection + * @param {number} options.tileWidth tile width, default 256 + * @param {number} options.tileHeight tile height, default 256 + * @return {array} a list of resolution based on the selected projection */ -export function getResolutionsForProjection(srs, minRes, maxRes, minZ, maxZ, zoomF, ext) { - const tileWidth = 256; // TODO: pass as parameters - const tileHeight = 256; // TODO: pass as parameters - allow different from tileWidth - - const defaultMaxZoom = 28; +export function getResolutionsForProjection(srs, { + minResolution: minRes, + maxResolution: maxRes, + minZoom: minZ, + maxZoom: maxZ, + zoomFactor: zoomF, + extent: ext, + tileWidth = 256, + tileHeight = 256 +} = {}) { + const defaultMaxZoom = 30; const defaultZoomFactor = 2; let minZoom = minZ ?? 0; @@ -230,7 +247,7 @@ export function getResolutionsForProjection(srs, minRes, maxRes, minZ, maxZ, zoo const projection = proj4.defs(srs); - const extent = ext ?? getExtentForProjection(srs)?.extent; + const extent = ext ?? getProjection(srs)?.extent; const extentWidth = !extent ? 360 * METERS_PER_UNIT.degrees / METERS_PER_UNIT[projection.getUnits()] : diff --git a/web/client/utils/ProjectionUtils.js b/web/client/utils/ProjectionUtils.js new file mode 100644 index 0000000000..bd4f5f0725 --- /dev/null +++ b/web/client/utils/ProjectionUtils.js @@ -0,0 +1,51 @@ +/* + * Copyright 2023, GeoSolutions Sas. + * All rights reserved. + * + * This source code is licensed under the BSD-style license found in the + * LICENSE file in the root directory of this source tree. +*/ + +import Proj4js from 'proj4'; +import { getConfigProp } from './ConfigUtils'; + +const proj4 = Proj4js; + +const DEFAULT_PROJECTIONS = { + 'EPSG:3857': { + def: '+proj=merc +a=6378137 +b=6378137 +lat_ts=0 +lon_0=0 +x_0=0 +y_0=0 +k=1 +units=m +nadgrids=@null +wktext +no_defs +type=crs', + extent: [-20037508.34, -20037508.34, 20037508.34, 20037508.34], + worldExtent: [-180.0, -85.06, 180.0, 85.06] + }, + 'EPSG:4326': { + def: '+proj=longlat +datum=WGS84 +no_defs +type=crs', + extent: [-180.0, -90.0, 180.0, 90.0], + worldExtent: [-180.0, -90.0, 180.0, 90.0] + } +}; + +/** + * Returns an object of projections where the key represents the code + * @return {object} projection definitions + */ +export const getProjections = () => { + return (getConfigProp('projectionDefs') || []) + .reduce((acc, { code, ...options }) => ({ + ...acc, + [code]: { + ...options, + proj4Def: { ...proj4.defs(code) } + } + }), + { ...DEFAULT_PROJECTIONS }); +}; + +/** + * Return a projection given a code + * @param {string} code for the projection, default 'EPSG:3857' + * @return {object} projection definition, fallback to default 'EPSG:3857' definition + */ +export const getProjection = (code = 'EPSG:3857') => { + const projections = getProjections(); + return projections[code] || projections['EPSG:3857']; +}; diff --git a/web/client/utils/__tests__/CoordinatesUtils-test.js b/web/client/utils/__tests__/CoordinatesUtils-test.js index 673681e914..820eb15970 100644 --- a/web/client/utils/__tests__/CoordinatesUtils-test.js +++ b/web/client/utils/__tests__/CoordinatesUtils-test.js @@ -1,4 +1,4 @@ -/** +/* * Copyright 2015, GeoSolutions Sas. * All rights reserved. * @@ -36,12 +36,10 @@ import { extractCrsFromURN, makeNumericEPSG, getPolygonFromCircle, - getProjections, - getExtentForProjection, checkIfLayerFitsExtentForProjection, getLonLatFromPoint, convertRadianToDegrees, convertDegreesToRadian } from '../CoordinatesUtils'; -import { setConfigProp, removeConfigProp } from '../ConfigUtils'; + import Proj4js from 'proj4'; describe('CoordinatesUtils', () => { @@ -831,28 +829,6 @@ describe('CoordinatesUtils', () => { expect(isNearlyEqual(polygon.geometry.coordinates[0][20][1], 14.956343723081114)).toBe(true); }); - it('getProjections returns an array projections', () => { - let projections = getProjections(); - expect(Array.isArray(projections)).toBe(true); - // 2 items because there are no projectionDefs in config - expect(projections.length).toBe(2); - - setConfigProp('projectionDefs', [{code: "EPSG:900913", extent: [1, 2, 3, 5]}]); - projections = getProjections(); - expect(projections.length).toBe(3); - removeConfigProp('projectionDefs'); - }); - - it('getExtentForProjection find an Extent by projection code', () => { - const {extent} = getExtentForProjection("EPSG:3857"); - expect(extent.length).toEqual(4); - - // returns default incase projection doesnot exist - const res = getExtentForProjection("EPSG:900913"); - expect(res.extent.length).toBe(4); - expect(res.extent).toEqual([-20026376.39, -20048966.10, 20026376.39, 20048966.10]); - }); - it('checkIfLayerFitsExtentForProjection out of bounds layer with crs EPSG:4326', () => { const geoJson = { bbox: {crs: "EPSG:4326"}, diff --git a/web/client/utils/__tests__/ProjectionUtils-test.js b/web/client/utils/__tests__/ProjectionUtils-test.js new file mode 100644 index 0000000000..6b7579eabf --- /dev/null +++ b/web/client/utils/__tests__/ProjectionUtils-test.js @@ -0,0 +1,60 @@ +/* + * Copyright 2023, GeoSolutions Sas. + * All rights reserved. + * + * This source code is licensed under the BSD-style license found in the + * LICENSE file in the root directory of this source tree. + */ + +import expect from 'expect'; + +import { + getProjections, + getProjection +} from '../ProjectionUtils'; + +import { setConfigProp, removeConfigProp } from '../ConfigUtils'; + +describe('CoordinatesUtils', () => { + it('should return default projections with getProjections', () => { + const projections = getProjections(); + expect(projections).toBeTruthy(); + expect(Object.keys(projections)).toEqual(['EPSG:3857', 'EPSG:4326']); + }); + it('should return default and configured projections with getProjections', () => { + const projectionDefs = [ + { + code: 'EPSG:3003', + def: '+proj=tmerc +lat_0=0 +lon_0=9 +k=0.9996 +x_0=1500000 +y_0=0 +ellps=intl +towgs84=-104.1,-49.1,-9.9,0.971,-2.917,0.714,-11.68 +units=m +no_defs', + extent: [1241482.0019, 973563.1609, 1830078.9331, 5215189.0853], + worldExtent: [6.6500, 8.8000, 12.0000, 47.0500] + } + ]; + setConfigProp('projectionDefs', projectionDefs); + const projections = getProjections(); + expect(Object.keys(projections)).toEqual(['EPSG:3857', 'EPSG:4326', 'EPSG:3003']); + removeConfigProp('projectionDefs'); + }); + it('should return extent with getProjection', () => { + const { extent } = getProjection('EPSG:3857'); + expect(extent).toEqual([ -20037508.34, -20037508.34, 20037508.34, 20037508.34 ]); + }); + it('should return default extent with getProjection if code is not configured', () => { + const { extent } = getProjection('EPSG:3003'); + expect(extent).toEqual([ -20037508.34, -20037508.34, 20037508.34, 20037508.34 ]); + }); + it('should return extent with getProjection if the code has been configured', () => { + const projectionDefs = [ + { + code: 'EPSG:3003', + def: '+proj=tmerc +lat_0=0 +lon_0=9 +k=0.9996 +x_0=1500000 +y_0=0 +ellps=intl +towgs84=-104.1,-49.1,-9.9,0.971,-2.917,0.714,-11.68 +units=m +no_defs', + extent: [ 1241482.0019, 973563.1609, 1830078.9331, 5215189.0853 ], + worldExtent: [ 6.6500, 8.8000, 12.0000, 47.0500 ] + } + ]; + setConfigProp('projectionDefs', projectionDefs); + const { extent } = getProjection('EPSG:3003'); + expect(extent).toEqual([ 1241482.0019, 973563.1609, 1830078.9331, 5215189.0853 ]); + removeConfigProp('projectionDefs'); + }); +}); diff --git a/web/client/utils/grids/MapGridsUtils.js b/web/client/utils/grids/MapGridsUtils.js index 62164182ea..a4e0aef5cb 100644 --- a/web/client/utils/grids/MapGridsUtils.js +++ b/web/client/utils/grids/MapGridsUtils.js @@ -1,6 +1,7 @@ import proj4 from 'proj4'; -import { reprojectBbox, bboxToFeatureGeometry, getExtentForProjection } from "../CoordinatesUtils"; +import { reprojectBbox, bboxToFeatureGeometry } from "../CoordinatesUtils"; +import { getProjection } from "../ProjectionUtils"; import booleanIntersects from "@turf/boolean-intersects"; import { getXLabelFormatter, getYLabelFormatter } from './GridLabelsUtils'; import chunk from "lodash/chunk"; @@ -410,7 +411,7 @@ export function getGridGeoJson({ const resolution = (resolutions ?? getResolutions(mapProjection))[zoom]; const mapToGrid = proj4(mapProjection, gridProjection).forward; const gridToMap = proj4(gridProjection, mapProjection).forward; - const projectionCenter = mapToGrid(getCenter(getExtentForProjection(gridProjection).extent)); + const projectionCenter = mapToGrid(getCenter(getProjection(gridProjection).extent)); const interval = getInterval( intervals ?? getIntervals(gridProjection), projectionCenter, @@ -425,7 +426,7 @@ export function getGridGeoJson({ mapProjection, gridProjection, extent, - getExtentForProjection(mapProjection).extent, + getProjection(mapProjection).extent, center ?? getCenter(extent), squaredTolerance, maxLines,