Skip to content

Commit

Permalink
geosolutions-it#9025 WMS caching with custom scales (projection resol…
Browse files Browse the repository at this point in the history
…utions strategy) (geosolutions-it#9168)
  • Loading branch information
allyoucanmap committed Jun 14, 2023
1 parent 084361f commit 3fcb571
Show file tree
Hide file tree
Showing 9 changed files with 225 additions and 89 deletions.
28 changes: 18 additions & 10 deletions web/client/components/map/openlayers/Map.jsx
Original file line number Diff line number Diff line change
Expand Up @@ -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';
Expand Down Expand Up @@ -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]);
}
Expand Down Expand Up @@ -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() {
Expand Down Expand Up @@ -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
Expand Down
42 changes: 40 additions & 2 deletions web/client/components/map/openlayers/__tests__/Map-test.jsx
Original file line number Diff line number Diff line change
Expand Up @@ -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(<OpenlayersMap id="ol-map" center={{ y: 43.9, x: 10.3 }} zoom={11} mapOptions={{ attribution: { container: 'body' } }} />, document.getElementById("map"));
expect(map.getResolutions().length).toBe(expectedResolutions.length);
// NOTE: round
Expand Down Expand Up @@ -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(<OpenlayersMap
id="ol-map"
center={{
Expand Down Expand Up @@ -1341,6 +1341,44 @@ describe('OpenlayersMap', () => {
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(
<OpenlayersMap
center={{y: 43.9, x: 10.3}}
zoom={11}
mapOptions={{view: { resolutions }}}
>
<OpenlayersLayer type="wms" srs="EPSG:3857" options={options} />
</OpenlayersMap>, 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(<OpenlayersMap id="mymap" center={{y: 43.9, x: 10.3}} zoom={11}/>, document.getElementById("map"));
Expand Down
40 changes: 23 additions & 17 deletions web/client/components/map/openlayers/plugins/WMSLayer.js
Original file line number Diff line number Diff line change
Expand Up @@ -15,14 +15,15 @@ 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';

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';
Expand Down Expand Up @@ -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]);
Expand All @@ -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 });
Expand Down Expand Up @@ -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;
Expand Down
25 changes: 2 additions & 23 deletions web/client/utils/CoordinatesUtils.js
Original file line number Diff line number Diff line change
Expand Up @@ -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;

Expand Down Expand Up @@ -1037,34 +1037,14 @@ 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
* @return {boolean} true or false
*/
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));
};
Expand Down Expand Up @@ -1165,7 +1145,6 @@ CoordinatesUtils = {
getPolygonFromCircle,
checkIfLayerFitsExtentForProjection,
getLonLatFromPoint,
getExtentForProjection,
convertRadianToDegrees,
convertDegreesToRadian
};
Expand Down
33 changes: 25 additions & 8 deletions web/client/utils/MapUtils.js
Original file line number Diff line number Diff line change
Expand Up @@ -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,
Expand Down Expand Up @@ -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;
Expand All @@ -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()] :
Expand Down
51 changes: 51 additions & 0 deletions web/client/utils/ProjectionUtils.js
Original file line number Diff line number Diff line change
@@ -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'];
};
Loading

0 comments on commit 3fcb571

Please sign in to comment.