Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

#9025 WMS caching with custom scales (projection resolutions strategy) #9168

Merged
merged 6 commits into from
May 17, 2023
Merged
Show file tree
Hide file tree
Changes from 5 commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
22 changes: 13 additions & 9 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 @@ -182,7 +183,7 @@ class OpenlayersMap extends React.Component {
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 = view.getProjection().getExtent() || msGetProjection(this.props.projection).extent;
if (this.props.projection === 'EPSG:4326') {
pos[0] = normalizeLng(pos[0]);
}
Expand Down Expand Up @@ -374,12 +375,15 @@ 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
}
);
};

render() {
Expand Down Expand Up @@ -439,7 +443,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 = view.getProjection().getExtent() || msGetProjection(this.props.projection).extent;
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This is used twice. IDK exactly why OL extent may be null (and if it should have priority or not).
You can create a function getExtent(this.map, this.props) to centralize and help future refactors and/or changes.

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