Skip to content

[Proposal] WMS Tile Grid Settings

stefano bovio edited this page May 16, 2023 · 12 revisions

Overview

The goal of this improvement is to simplify the current MapStore project creation and update system.

Proposed by

  • Lorenzo Natali (author and writer of the proposal)

Assigned to Release

The proposal is for 2023.02.00 or next

State

  • Under Discussion
  • In Progress
  • Completed
  • Rejected
  • Deferred

Motivation

MapStore, and in particular the Openlayers implementation of Tiled WMS, generates the a tile grid based to the map resolutions. This limits the possibility to use the tile caches from GeoWebCache when the map have custom resolutions. Also creating custom gridsets causes problems because the current implementation allows to define a fixed origin (top-left corner) for the layer, while GeoWebCache generates the tilegrid by starting from the bottom-left corner. In case the resolutions are not exponential (that generate tiles of power of 2, that are integer), this means that the top-left corner vary on every zoom level.

Moreover, in order to pass to a more modern web mapping (e.g. with fractional scales or ready for the 3D modes), we have to bind the tile grid system to detouch the tile grid generation strategy from the map resolutions and allow to configure them accordingly with the source.

Original issue proposed here:

https://github.com/geosolutions-it/MapStore2/issues/9025

Proposal

We should implement some improvement to MapStore in order to:

  • Use by default the exponential generation of tilegrid, that guarantees a better alignment to default gridsets generated by GeoWebCache
  • Allow to define from UI and store in the Map JSON:
    • default tilegrid settings for WMS source (advanced layer settings)
    • layer tilegrid settings for WMS Layer (layer settings)

Here a sample about Tile grid settings button in WMS Source

image

The same will be present in layer settings.

About 3D

Actually the 3D map relies on EPSG:4326. We can configure a proper custom TilingScheme but using the default for now is enough and it is not part of the estimation.

Things to check

  • Printing

Tasks Daft

  • Properly define new defaults (while deveolping)
  • Change WMSLayer in OpenLayers to use a tileGrid object
  • Extract/export proper functionalities from MapUtils (e.g. get resolutions for map)
  • UI to insert configurations in WMS source and WMS Layer tile grid configuration (near tile-sizes, a new button, should allow to completely customize tile grid in a dialog):
    • allow to define origin(s), tileSize(s),resolutions (or scales) manually
    • Reuse the dialog in both layer properties and WMS source
    • Optional: retrieve them from WMTS (GeoServer only, helps to auto-load)
  • Optional: In map settings, allow to configure the scales/resolutions of the map manually
  • Testing with various CRS/Tile grids: (also developer need to test)

Temptative Implementation

This (missing export getResolutionsForProjection from MapUtils) seems to work, but generates some MISS for first testing, to verify

diff --git a/web/client/components/map/openlayers/plugins/WMSLayer.js b/web/client/components/map/openlayers/plugins/WMSLayer.js
index 87ad6931c..a7b1dcd8b 100644
--- a/web/client/components/map/openlayers/plugins/WMSLayer.js
+++ b/web/client/components/map/openlayers/plugins/WMSLayer.js
@@ -20,7 +20,7 @@ import { getConfigProp } from '../../../../utils/ConfigUtils';
 
 import {optionsToVendorParams} from '../../../../utils/VendorParamsUtils';
 import {addAuthenticationToSLD, addAuthenticationParameter, getAuthenticationHeaders} from '../../../../utils/SecurityUtils';
-import { creditsToAttribution, getWMSVendorParams } from '../../../../utils/LayersUtils';
+import { creditsToAttribution } from '../../../../utils/LayersUtils';
 
 import MapUtils from '../../../../utils/MapUtils';
 import  {loadTile, getElevation as getElevationFunc} from '../../../../utils/ElevationUtils';
@@ -123,7 +123,7 @@ function wmsToOpenlayersOptions(options) {
         TRANSPARENT: options.transparent !== undefined ? options.transparent : true,
         SRS: CoordinatesUtils.normalizeSRS(options.srs || 'EPSG:3857', options.allowedSRS),
         CRS: CoordinatesUtils.normalizeSRS(options.srs || 'EPSG:3857', options.allowedSRS),
-        ...getWMSVendorParams(options),
+        TILED: options.singleTile ? false : (!isNil(options.tiled) ? options.tiled : true),
         VERSION: options.version || "1.3.0"
     }, assign(
         {},
@@ -189,6 +189,64 @@ function getElevation(pos) {
 }
 const toOLAttributions = credits => credits && creditsToAttribution(credits) || undefined;
 
+/**
+ * Generates the tile grid for the layer based on the options.
+ * If the options contains a tileGridStrategy, it will be used to generate the tile grid.
+ * Otherwise, the default tile grid will be used.
+ * @param {object} options layer options. If it contains a `tileGridStrategy`, it will be used to generate the tile grid.
+ * @param {string} options.tileGridStrategy the tile grid strategy to use. Valid values are:
+ * - `default`: the tile grid will be generated by calculating the resolutions based on the current projection extent, exponentially increasing the resolution.
+ * - `map`: the tile grid will be generated using the current map resolutions.
+ * - `custom`: the tile grid will be generated using the resolutions provided in the `resolutions` option. Also `origin` or `origins` can be provided.
+ * @param {map} map the map object
+ * @returns {TileGrid} the tile grid for the layer
+ */
+const generateTileGrid = (options, map) => {
+
+    const strategy = options.tileGridStrategy // use a tileGrid object with strategy object, containing information about the tile grid (by crs)
+        || (options.resolutions && (options.origin || options.origins)
+            ? 'custom'
+            : 'default');
+    const mapSrs = map && map.getView() && map.getView().getProjection() && map.getView().getProjection().getCode() || 'EPSG:3857';
+    const normalizedSrs = CoordinatesUtils.normalizeSRS(options.srs || mapSrs, options.allowedSRS);
+    const tileSize = options.tileSize ? options.tileSize : 256; // TODO check tileSizes
+    // if tileSizes is defined, it overrides tileSize.
+    // tileSizes is an array of tile sizes for each resolution (array of same length of resolutions)
+    // other openlayers TileGrid arguments are
+    const extent = options.extentsv
+        || get(normalizedSrs).getExtent()
+        || CoordinatesUtils.getExtentForProjection(normalizedSrs).extent;
+    switch (strategy) {
+    case 'map':
+        return new TileGrid({
+            extent: extent,
+            tileSize,
+            resolutions: options.resolutions || MapUtils.getResolutions(),
+            origin: options.origin ? options.origin : [extent[0], extent[1]]
+        });
+    case 'custom':
+        return new TileGrid({
+            extent: extent,
+            // minZoom: options.minZoom, // TODO: check
+            origin: options.origin ? options.origin : [extent[0], extent[1]],
+            origins: options.origins,
+            resolutions: options.resolutions,
+            // sizes: options.sizes, // Number of tile rows and columns for each zoom level. // TODO: check
+            tileSize,
+            tileSizes: options.tileSizes
+
+        });
+    case 'default':
+    default:
+        return new TileGrid({
+            extent: extent,
+            tileSize,
+            resolutions: MapUtils.getResolutionsForProjection(normalizedSrs),
+            origin: [extent[0], extent[1]]
+            // calculate resolutions?
+        });
+    }
+};
 
 const createLayer = (options, map) => {
     const urls = getWMSURLs(isArray(options.url) ? options.url : [options.url]);
@@ -215,20 +273,14 @@ 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 tileGrid = generateTileGrid(options, map);
     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,
         tileLoadFunction: loadFunction(options, headers)
     }, options);
     const wmsSource = new TileWMS({ ...sourceOptions });

Proposal for custom tileGridStrategy options using WMTS endpoint

The work done in the PR #9168 introduces the tile grid based on projection as default removing the possibility to have a map strategy. We tested some options on how to use WMTS grid information while working on the PR #9168 with the following findings:

  • an on the fly request for WMTS endpoints to retrieve the tile grid info is feasible but it could cause problem on loading if a layer is not properly configured server side. For this reason this implementation is excluded
  • if we want to store the tile grid information is not enough to have origins, origin, resolutions or tileSizes as root properties of the layer because this property could vary based on the selected projection
  • the WMTS custom strategy could work only if we can reconstruct the url besed on GeoServer name

Here a WIP branch with part of the proposal code explained below allyoucanmap/issue_9025_2, see WIP commit

The idea is a new property for the layer called tileGrids for WMS layers persisted in the map configuration. The tileGrids property is an array of available tile grids with the following properties:

property type description
id string identifier of the tile grid
crs string projection of the tile grid
scales array list of scales available for the tile grid, alternative to resolutions
resolutions array list of resolutions available for the tile grid, alternative to scales
origin array [x, y] value of the origin of the tile grid, alternative to origins
origins array array of origin for each level of resolutions/scales, alternative to origin
tileSize array [width, height] value of the tile size of the tile grid, alternative to tileSizes
tileSizes array array of tileSize for each level of resolutions/scales, alternative to tileSize

example of layer configuration:

{
    "id": "topp:states__83b99ac0-e8e4-11ed-a3e5-577a9317356b",
    "format": "image/png",
    "search": {
        "url": "http://localhost:8080/geoserver/wfs",
        "type": "wfs"
    },
    "name": "topp:states",
    "description": "This is some census data on the states.",
    "title": "USA Population",
    "type": "wms",
    "url": "http://localhost:8080/geoserver/wms",
    "bbox": {
        "crs": "EPSG:4326",
        "bounds": {
            "minx": "-124.731422",
            "miny": "24.955967",
            "maxx": "-66.969849",
            "maxy": "49.371735"
        }
    },
    "visibility": true,
    "singleTile": false,
    "version": "1.3.0",
    "opacity": 1,
    "tileGrids": [
        {
            "id": "EPSG:900913",
            "crs": "EPSG:900913",
            "scales": [
                559082263.9508929,
                279541131.97544646,
                139770565.98772323,
                69885282.99386162,
                34942641.49693081,
                17471320.748465404,
                8735660.374232702,
                4367830.187116351,
                2183915.0935581755,
                1091957.5467790877,
                545978.7733895439,
                272989.38669477194,
                136494.69334738597,
                68247.34667369298,
                34123.67333684649,
                17061.836668423246,
                8530.918334211623,
                4265.4591671058115,
                2132.7295835529058,
                1066.3647917764529,
                533.1823958882264,
                266.5911979441132,
                133.2955989720566,
                66.6477994860283,
                33.32389974301415,
                16.661949871507076,
                8.330974935753538,
                4.165487467876769,
                2.0827437339383845,
                1.0413718669691923,
                0.5206859334845961
            ],
            "origin": [
                -20037508.34,
                20037508
            ],
            "tileSize": [
                256,
                256
            ]
        },
        {
            "id": "EPSG:900913x2",
            "crs": "EPSG:900913",
            "scales": [
                279541131.97544646,
                139770565.98772323,
                69885282.99386162,
                34942641.49693081,
                17471320.748465404,
                8735660.374232702,
                4367830.187116351,
                2183915.0935581755,
                1091957.5467790877,
                545978.7733895439,
                272989.38669477194,
                136494.69334738597,
                68247.34667369298,
                34123.67333684649,
                17061.836668423246,
                8530.918334211623,
                4265.4591671058115,
                2132.7295835529058,
                1066.3647917764529,
                533.1823958882264,
                266.5911979441132,
                133.2955989720566,
                66.6477994860283,
                33.32389974301415,
                16.661949871507076,
                8.330974935753538,
                4.165487467876769,
                2.0827437339383845,
                1.0413718669691923,
                0.5206859334845961,
                0.26034296674229807
            ],
            "origin": [
                -20037508.34,
                20037508
            ],
            "tileSize": [
                512,
                512
            ]
        }
    ],
    "tileGridStrategy": "custom"
}

This is the generateTileGrid function for WMS layer in the OpenLayers implementation of MapStore with an additional check to extract the custom tile grid. The tile grid will be selected from the tileGrids array if:

  • if tileGrids property exists
  • the TILED param is applied
  • the tileGridStrategy is custom
  • there is at least one tile size that has the same projection of the map and at list a tileSize that matches the selected one
const generateTileGrid = (options, map) => {
    const mapSrs = map?.getView()?.getProjection()?.getCode() || 'EPSG:3857';
    const normalizedSrs = CoordinatesUtils.normalizeSRS(options.srs || mapSrs, options.allowedSRS);
    const tileSize = options.tileSize ? options.tileSize : 256;
    const extent = get(normalizedSrs).getExtent() || getProjection(normalizedSrs).extent;
    const { TILED } = getWMSVendorParams(options);
    const customTileGrid = TILED && options.tileGridStrategy === 'custom' && options.tileGrids
        ? options.tileGrids.find((tileGrid) =>
            CoordinatesUtils.normalizeSRS(tileGrid.crs) === normalizedSrs
            && !!(tileGrid.tileSizes
                ? tileGrid.tileSizes.some(([tileWidth, tileHeight]) => tileWidth === tileSize || tileHeight === tileSize)
                : tileGrid.tileSize[0] === tileSize || tileGrid.tileSize[1] === tileSize)
        )
        : null;
    if (customTileGrid
        && (customTileGrid.resolutions || customTileGrid.scales)
        && (customTileGrid.origins || customTileGrid.origin)
        && (customTileGrid.tileSizes || customTileGrid.tileSize)) {
        const {
            resolutions: customTileGridResolutions,
            scales,
            origin,
            origins,
            tileSize: customTileGridTileSize,
            tileSizes
        } = customTileGrid;
        const projection = get(normalizedSrs);
        const metersPerUnit = projection.getMetersPerUnit();
        const scaleToResolution = s => s * 0.28E-3 / metersPerUnit;
        const resolutions = customTileGridResolutions
            ? customTileGridResolutions
            : scales.map(scale => scaleToResolution(scale));
        return new TileGrid({
            extent,
            resolutions,
            tileSizes,
            tileSize: customTileGridTileSize,
            origin,
            origins
        });
    }
    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
    });
};

In the display panel of WMS layer settings we could add a select to choose the tile grid strategy, default or custom

image

Selecting the custom we should create a wmts url based on the wms one (geoserver only) and trigger a request to parse the available grid set

function to get the WMTS url from WMS layer options

export const generateGeoServerWMTSUrl = (options) => {
    const geoServerName = findGeoServerName(options);
    if (!geoServerName) {
        return null;
    }
    const baseUrl = getLayerUrl(options);
    const parts = baseUrl.split(geoServerName);
    const layerParts = options.name.split(':');
    const workspacePath = layerParts.length === 2 ? `${layerParts[0]}/${layerParts[1]}/` : '';
    const wmtsCapabilitiesUrl = `${parts[0]}${geoServerName}${workspacePath}gwc/service/wmts?REQUEST=GetCapabilities`;
    return wmtsCapabilitiesUrl;
};

WMTS request to extract tileGrids from capabilities

export const getLayerTileMatrixSetsInfo = (url, options) => {
    return Api.getCapabilities(url)
        .then((response) => {
            const layerParts = options.name.split(':');
            const layers = castArray(response?.Capabilities?.Contents?.Layer || []);
            const wmtsLayer = layers.find((layer) => layer['ows:Identifier'] === layerParts[1] || layer['ows:Identifier'] === options.name);
            const tileMatrixSetLinks = castArray(wmtsLayer?.TileMatrixSetLink || []).map(({ TileMatrixSet }) => TileMatrixSet);
            const tileMatrixSets = castArray(response?.Capabilities?.Contents?.TileMatrixSet || []).filter((tileMatrixSet) => tileMatrixSetLinks.includes(tileMatrixSet['ows:Identifier']));
            const tileGrids = tileMatrixSets.map((tileMatrixSet) => {
                const origins = tileMatrixSet.TileMatrix.map((tileMatrixLevel) => tileMatrixLevel.TopLeftCorner.split(' ').map(parseFloat));
                const tileSizes = tileMatrixSet.TileMatrix.map((tileMatrixLevel) => [parseFloat(tileMatrixLevel.TileWidth), parseFloat(tileMatrixLevel.TileHeight)]);
                const isSingleOrigin = origins.every(entry => origins[0][0] === entry[0] && origins[0][1] === entry[1]);
                const isSingleTileSize = tileSizes.every(entry => tileSizes[0][0] === entry[0] && tileSizes[0][1] === entry[1]);
                return {
                    id: tileMatrixSet['ows:Identifier'],
                    crs: getEPSGCode(tileMatrixSet['ows:SupportedCRS']),
                    scales: tileMatrixSet.TileMatrix.map((tileMatrixLevel) => parseFloat(tileMatrixLevel.ScaleDenominator)),
                    ...(isSingleOrigin ? { origin: origins[0] } : { origins }),
                    ...(isSingleTileSize ? { tileSize: tileSizes[0] } : { tileSizes })
                };
            });
            return {
                tileMatrixSets,
                tileMatrixSetLinks,
                tileGrids
            };
        });
};

The UI should also display message error in case tile grids are not available for the custom option.

In this proposal the Catalog tool will be using always the default strategy.