From 043a482b4d62fed729c3cb7e3574541db10e9349 Mon Sep 17 00:00:00 2001 From: Daniele Romagnoli Date: Fri, 2 Aug 2024 10:04:44 +0200 Subject: [PATCH] Supporting mapml-proxy --- .../source/extensions/mapml/installation.rst | 14 + src/extension/mapml/pom.xml | 10 + .../org/geoserver/mapml/MapMLConstants.java | 6 + .../geoserver/mapml/MapMLDocumentBuilder.java | 98 +- .../mapml/MapMLLayerConfigurationPanel.html | 17 + .../mapml/MapMLLayerConfigurationPanel.java | 36 + .../geoserver/mapml/MapMLRequestMangler.java | 607 ++++ .../resources/GeoServerApplication.properties | 2 + .../geoserver/mapml/MapMLBaseProxyTest.java | 153 + .../mapml/MapMLWMSDataAccessProxyTest.java | 108 + .../geoserver/mapml/MapMLWMSProxyTest.java | 129 + .../org/geoserver/mapml/MapMLWMSTest.java | 2 +- .../geoserver/mapml/MapMLWMTSProxyTest.java | 160 + .../src/test/resources/__files/wmscaps.xml | 114 + .../src/test/resources/__files/wmtscaps.xml | 2772 +++++++++++++++++ 15 files changed, 4174 insertions(+), 54 deletions(-) create mode 100644 src/extension/mapml/src/main/java/org/geoserver/mapml/MapMLRequestMangler.java create mode 100644 src/extension/mapml/src/test/java/org/geoserver/mapml/MapMLBaseProxyTest.java create mode 100644 src/extension/mapml/src/test/java/org/geoserver/mapml/MapMLWMSDataAccessProxyTest.java create mode 100644 src/extension/mapml/src/test/java/org/geoserver/mapml/MapMLWMSProxyTest.java create mode 100644 src/extension/mapml/src/test/java/org/geoserver/mapml/MapMLWMTSProxyTest.java create mode 100644 src/extension/mapml/src/test/resources/__files/wmscaps.xml create mode 100644 src/extension/mapml/src/test/resources/__files/wmtscaps.xml diff --git a/doc/en/user/source/extensions/mapml/installation.rst b/doc/en/user/source/extensions/mapml/installation.rst index 1922443f6c7..cf0afef466e 100644 --- a/doc/en/user/source/extensions/mapml/installation.rst +++ b/doc/en/user/source/extensions/mapml/installation.rst @@ -78,6 +78,20 @@ Using tiles to access the layer can increase the performance of your web map. Th **Use Tiles** If the "Use Tiles" checkbox is checked, by default the output MapML will define a tile-based reference to the WMS server. Otherwise, an image-based reference will be used. If one or more of the MapML-defined GridSets is referenced by the layer or layer group in its "Tile Caching" profile, GeoServer will generate tile references instead of generating WMS GetMap URLs in the MapML document body. +Client Requests +^^^^^^^^^^^^^^^ + +When configuring a cascaded WMS or WMTS remote layers, a new "Client Requests" setting is available. + +**Remote** + If the "Remote" checkbox is checked, the link templates embedded in MapML will refer to the remote WMS/WMTS. + The MapML viewer will directly contact the remote server if certain criteria are met: + +- No restricting DataAccessLimit security is associated to the layer (e.g. with GeoFence integration) that will do filtering, clipping or similar operations. In that case, the MapML will point to the local GeoServer so that the param is honored. +- No vendor parameters are used in the incoming request. If vendor parameters are used (e.g., request clipping with geometric mask) the MapML is pointing to the local GeoServer so that the vendor parameter is honored +- The remote Server is supporting the requested CoordinateReferenceSystem for that layer. +- GetTile requests will be sent to the remote server if there is a compatible gridset for that layer (same origin, same CRS, same tile sizes, same levels and same resolutions) + Vector Settings ^^^^^^^^^^^^^^^ diff --git a/src/extension/mapml/pom.xml b/src/extension/mapml/pom.xml index 2509e66d3b5..54bcfce504d 100644 --- a/src/extension/mapml/pom.xml +++ b/src/extension/mapml/pom.xml @@ -114,6 +114,16 @@ 0.3.6 test + + com.github.tomakehurst + wiremock-jre8-standalone + test + + + org.mockito + mockito-core + test + diff --git a/src/extension/mapml/src/main/java/org/geoserver/mapml/MapMLConstants.java b/src/extension/mapml/src/main/java/org/geoserver/mapml/MapMLConstants.java index 45f00f58dee..d84676ee55c 100644 --- a/src/extension/mapml/src/main/java/org/geoserver/mapml/MapMLConstants.java +++ b/src/extension/mapml/src/main/java/org/geoserver/mapml/MapMLConstants.java @@ -50,6 +50,9 @@ public final class MapMLConstants { /** MapML layer metadata use tiles */ public static final String MAPML_USE_TILES = "mapml.useTiles"; + /** MapML layer metadata remote client request */ + public static final String MAPML_USE_REMOTE = "mapml.useRemote"; + /** MapML layer resource metadata */ public static final String RESOURCE_METADATA = "resource.metadata"; @@ -86,6 +89,9 @@ public final class MapMLConstants { /** USE_TILES */ public static final String USE_TILES = "useTiles"; + /** REMOTE */ + public static final String USE_REMOTE = "useRemote"; + /** LICENSE_LINK */ public static final String LICENSE = "licenseLink"; diff --git a/src/extension/mapml/src/main/java/org/geoserver/mapml/MapMLDocumentBuilder.java b/src/extension/mapml/src/main/java/org/geoserver/mapml/MapMLDocumentBuilder.java index 4159f2edf90..2fc7786a5f5 100644 --- a/src/extension/mapml/src/main/java/org/geoserver/mapml/MapMLDocumentBuilder.java +++ b/src/extension/mapml/src/main/java/org/geoserver/mapml/MapMLDocumentBuilder.java @@ -11,6 +11,7 @@ import static org.geoserver.mapml.MapMLConstants.MAPML_SKIP_ATTRIBUTES_FO; import static org.geoserver.mapml.MapMLConstants.MAPML_SKIP_STYLES_FO; import static org.geoserver.mapml.MapMLConstants.MAPML_USE_FEATURES; +import static org.geoserver.mapml.MapMLConstants.MAPML_USE_REMOTE; import static org.geoserver.mapml.MapMLConstants.MAPML_USE_TILES; import static org.geoserver.mapml.MapMLHTMLOutput.PREVIEW_TCRS_MAP; import static org.geoserver.mapml.template.MapMLMapTemplate.MAPML_PREVIEW_HEAD_FTL; @@ -182,13 +183,6 @@ public class MapMLDocumentBuilder { private Boolean isMultiExtent = MAPML_MULTILAYER_AS_MULTIEXTENT_DEFAULT; private MapMLMapTemplate mapMLMapTemplate = new MapMLMapTemplate(); - static { - PREVIEW_TCRS_MAP.put("OSMTILE", new TiledCRS("OSMTILE")); - PREVIEW_TCRS_MAP.put("CBMTILE", new TiledCRS("CBMTILE")); - PREVIEW_TCRS_MAP.put("APSTILE", new TiledCRS("APSTILE")); - PREVIEW_TCRS_MAP.put("WGS84", new TiledCRS("WGS84")); - } - /** * Constructor * @@ -606,6 +600,7 @@ private MapMLLayerMetadata layerToMapMLLayerMetadata(RawLayer layer, String styl .getGridSubset(projType.value()) != null; boolean useTiles = Boolean.TRUE.equals(layerMeta.get(MAPML_USE_TILES, Boolean.class)); + boolean useRemote = Boolean.TRUE.equals(layerMeta.get(MAPML_USE_REMOTE, Boolean.class)); boolean useFeatures = useFeatures(layer, layerMeta); return new MapMLLayerMetadata( @@ -623,6 +618,7 @@ private MapMLLayerMetadata layerToMapMLLayerMetadata(RawLayer layer, String styl styleName, tileLayerExists, useTiles, + useRemote, useFeatures, cqlFilter, defaultMimeType); @@ -1335,15 +1331,10 @@ private void generateWMTSClientLinks(MapMLLayerMetadata mapMLLayerMetadata) { setElevationParam(mapMLLayerMetadata, params, gstl); setCustomDimensionParam(mapMLLayerMetadata, params, gstl); setCqlFilterParam(mapMLLayerMetadata, params); - String urlTemplate = ""; - try { - urlTemplate = - URLDecoder.decode( - ResponseUtils.buildURL( - baseUrlPattern, path, params, URLMangler.URLType.SERVICE), - "UTF-8"); - } catch (UnsupportedEncodingException uee) { - } + MapMLRequestMangler mangler = + new MapMLRequestMangler( + mapContent, mapMLLayerMetadata, baseUrlPattern, path, params, proj); + String urlTemplate = mangler.getUrlTemplate(); tileLink.setTref(urlTemplate); extentList.add(tileLink); } @@ -1473,15 +1464,10 @@ private void generateTiledWMSClientLinks(MapMLLayerMetadata mapMLLayerMetadata) params.put("transparent", Boolean.toString(mapMLLayerMetadata.isTransparent())); params.put("width", "256"); params.put("height", "256"); - String urlTemplate = ""; - try { - urlTemplate = - URLDecoder.decode( - ResponseUtils.buildURL( - baseUrlPattern, path, params, URLMangler.URLType.SERVICE), - "UTF-8"); - } catch (UnsupportedEncodingException uee) { - } + MapMLRequestMangler mangler = + new MapMLRequestMangler( + mapContent, mapMLLayerMetadata, baseUrlPattern, path, params, proj); + String urlTemplate = mangler.getUrlTemplate(); tileLink.setTref(urlTemplate); extentList.add(tileLink); } @@ -1611,15 +1597,10 @@ public void generateWMSClientLinks(MapMLLayerMetadata mapMLLayerMetadata) { params.put("language", this.request.getLocale().getLanguage()); params.put("width", "{w}"); params.put("height", "{h}"); - String urlTemplate = ""; - try { - urlTemplate = - URLDecoder.decode( - ResponseUtils.buildURL( - baseUrlPattern, path, params, URLMangler.URLType.SERVICE), - "UTF-8"); - } catch (UnsupportedEncodingException uee) { - } + MapMLRequestMangler mangler = + new MapMLRequestMangler( + mapContent, mapMLLayerMetadata, baseUrlPattern, path, params, proj); + String urlTemplate = mangler.getUrlTemplate(); imageLink.setTref(urlTemplate); extentList.add(imageLink); } @@ -1695,15 +1676,10 @@ private void generateWMTSQueryClientLinks(MapMLLayerMetadata mapMLLayerMetadata) params.put("infoformat", "text/mapml"); params.put("i", "{i}"); params.put("j", "{j}"); - String urlTemplate = ""; - try { - urlTemplate = - URLDecoder.decode( - ResponseUtils.buildURL( - baseUrlPattern, path, params, URLMangler.URLType.SERVICE), - "UTF-8"); - } catch (UnsupportedEncodingException uee) { - } + MapMLRequestMangler mangler = + new MapMLRequestMangler( + mapContent, mapMLLayerMetadata, baseUrlPattern, path, params, proj); + String urlTemplate = mangler.getUrlTemplate(); queryLink.setTref(urlTemplate); extentList.add(queryLink); } @@ -1744,7 +1720,7 @@ private void generateWMSQueryClientLinks(MapMLLayerMetadata mapMLLayerMetadata) params.put("layers", mapMLLayerMetadata.getLayerName()); params.put("query_layers", mapMLLayerMetadata.getLayerName()); params.put("styles", mapMLLayerMetadata.getStyleName()); - if (mapMLLayerMetadata.getCqlFilter() != null) { + if (StringUtils.isNotBlank(mapMLLayerMetadata.getCqlFilter())) { params.put("cql_filter", mapMLLayerMetadata.getCqlFilter()); } setTimeParam(mapMLLayerMetadata, params, null); @@ -1764,15 +1740,10 @@ private void generateWMSQueryClientLinks(MapMLLayerMetadata mapMLLayerMetadata) params.put("transparent", Boolean.toString(mapMLLayerMetadata.isTransparent())); params.put("x", "{i}"); params.put("y", "{j}"); - String urlTemplate = ""; - try { - urlTemplate = - URLDecoder.decode( - ResponseUtils.buildURL( - baseUrlPattern, path, params, URLMangler.URLType.SERVICE), - "UTF-8"); - } catch (UnsupportedEncodingException uee) { - } + MapMLRequestMangler mangler = + new MapMLRequestMangler( + mapContent, mapMLLayerMetadata, baseUrlPattern, path, params, proj); + String urlTemplate = mangler.getUrlTemplate(); queryLink.setTref(urlTemplate); extentList.add(queryLink); } @@ -2333,6 +2304,7 @@ static class MapMLLayerMetadata { private boolean tileLayerExists; private boolean useTiles; + private boolean useRemote; private boolean timeEnabled; private boolean elevationEnabled; @@ -2395,6 +2367,7 @@ public MapMLLayerMetadata( String styleName, boolean tileLayerExists, boolean useTiles, + boolean useRemote, boolean useFeatures, String cqFilter, String defaultMimeType) { @@ -2412,6 +2385,7 @@ public MapMLLayerMetadata( this.isTransparent = isTransparent; this.tileLayerExists = tileLayerExists; this.useTiles = useTiles; + this.useRemote = useRemote; this.useFeatures = useFeatures; this.cqlFilter = cqFilter; this.defaultMimeType = defaultMimeType; @@ -2738,6 +2712,24 @@ public void setUseTiles(boolean useTiles) { this.useTiles = useTiles; } + /** + * get if the layer uses remote + * + * @return boolean + */ + public boolean isUseRemote() { + return useRemote; + } + + /** + * set if the layer uses remote + * + * @param useRemote boolean + */ + public void setUseRemote(boolean useRemote) { + this.useRemote = useRemote; + } + /** * get the ReferencedEnvelope object * diff --git a/src/extension/mapml/src/main/java/org/geoserver/mapml/MapMLLayerConfigurationPanel.html b/src/extension/mapml/src/main/java/org/geoserver/mapml/MapMLLayerConfigurationPanel.html index 26d90499911..79381220d9e 100644 --- a/src/extension/mapml/src/main/java/org/geoserver/mapml/MapMLLayerConfigurationPanel.html +++ b/src/extension/mapml/src/main/java/org/geoserver/mapml/MapMLLayerConfigurationPanel.html @@ -43,6 +43,23 @@

+
+
  • +
    + + Client Requests + +
      +
    • + + +
    • +
    +
    +
  • +
  • diff --git a/src/extension/mapml/src/main/java/org/geoserver/mapml/MapMLLayerConfigurationPanel.java b/src/extension/mapml/src/main/java/org/geoserver/mapml/MapMLLayerConfigurationPanel.java index 6fc31821e64..c88b55437a6 100644 --- a/src/extension/mapml/src/main/java/org/geoserver/mapml/MapMLLayerConfigurationPanel.java +++ b/src/extension/mapml/src/main/java/org/geoserver/mapml/MapMLLayerConfigurationPanel.java @@ -18,6 +18,7 @@ import java.util.stream.Collectors; import org.apache.wicket.ajax.AjaxRequestTarget; import org.apache.wicket.ajax.form.OnChangeAjaxBehavior; +import org.apache.wicket.markup.html.WebMarkupContainer; import org.apache.wicket.markup.html.form.CheckBox; import org.apache.wicket.markup.html.form.DropDownChoice; import org.apache.wicket.markup.html.form.ListMultipleChoice; @@ -38,6 +39,9 @@ import org.geoserver.catalog.PublishedType; import org.geoserver.catalog.ResourceInfo; import org.geoserver.catalog.ResourcePool; +import org.geoserver.catalog.StoreInfo; +import org.geoserver.catalog.WMSStoreInfo; +import org.geoserver.catalog.WMTSStoreInfo; import org.geoserver.gwc.GWC; import org.geoserver.gwc.layer.GeoServerTileLayer; import org.geoserver.gwc.layer.GeoServerTileLayerInfo; @@ -110,6 +114,10 @@ protected void onUpdate(AjaxRequestTarget ajaxRequestTarget) { }); add(useTiles); + // Remote client requests + WebMarkupContainer remoteClientRequestContainer = setupRemoteClientRequestContainer(model); + add(remoteClientRequestContainer); + // add the checkbox to select features or not MapModel useFeaturesModel = new MapModel<>( @@ -186,6 +194,21 @@ protected void onUpdate(AjaxRequestTarget target) { add(featureCaptionTemplate); } + private WebMarkupContainer setupRemoteClientRequestContainer(IModel model) { + WebMarkupContainer remoteClientRequestContainer = + new WebMarkupContainer("RemoteClientRequestsConfiguration"); + LayerInfo layerInfo = model.getObject(); + MapModel useRemoteModel = + new MapModel<>( + new PropertyModel(model, MapMLConstants.RESOURCE_METADATA), + MapMLConstants.MAPML_USE_REMOTE); + CheckBox useRemote = new CheckBox(MapMLConstants.USE_REMOTE, useRemoteModel); + remoteClientRequestContainer.setOutputMarkupId(true); + remoteClientRequestContainer.setVisible(isWMSOrWMTSStore(layerInfo)); + remoteClientRequestContainer.add(useRemote); + return remoteClientRequestContainer; + } + /** * Get the available mime types for the layer * @@ -298,4 +321,17 @@ private List getAttributeNames(LayerInfo layer) { return Collections.emptyList(); } } + + private boolean isWMSOrWMTSStore(LayerInfo layerInfo) { + if (layerInfo != null) { + ResourceInfo resourceInfo = layerInfo.getResource(); + if (resourceInfo != null) { + StoreInfo storeInfo = resourceInfo.getStore(); + if (storeInfo instanceof WMSStoreInfo || storeInfo instanceof WMTSStoreInfo) { + return true; + } + } + } + return false; + } } diff --git a/src/extension/mapml/src/main/java/org/geoserver/mapml/MapMLRequestMangler.java b/src/extension/mapml/src/main/java/org/geoserver/mapml/MapMLRequestMangler.java new file mode 100644 index 00000000000..eb7171c3667 --- /dev/null +++ b/src/extension/mapml/src/main/java/org/geoserver/mapml/MapMLRequestMangler.java @@ -0,0 +1,607 @@ +/* (c) 2024 Open Source Geospatial Foundation - all rights reserved + * This code is licensed under the GPL 2.0 license, available at the root + * application directory. + */ +package org.geoserver.mapml; + +import java.io.IOException; +import java.io.UnsupportedEncodingException; +import java.net.MalformedURLException; +import java.net.URL; +import java.net.URLDecoder; +import java.util.Collection; +import java.util.HashMap; +import java.util.HashSet; +import java.util.List; +import java.util.Map; +import java.util.Set; +import java.util.logging.Logger; +import org.geoserver.catalog.LayerInfo; +import org.geoserver.catalog.ResourceInfo; +import org.geoserver.catalog.SLDHandler; +import org.geoserver.catalog.StoreInfo; +import org.geoserver.catalog.WMSStoreInfo; +import org.geoserver.catalog.WMTSStoreInfo; +import org.geoserver.mapml.tcrs.Point; +import org.geoserver.mapml.tcrs.TiledCRSConstants; +import org.geoserver.mapml.tcrs.TiledCRSParams; +import org.geoserver.ows.URLMangler; +import org.geoserver.ows.util.ResponseUtils; +import org.geoserver.platform.GeoServerExtensions; +import org.geoserver.security.CoverageAccessLimits; +import org.geoserver.security.DataAccessLimits; +import org.geoserver.security.ResourceAccessManager; +import org.geoserver.security.VectorAccessLimits; +import org.geoserver.security.WMSAccessLimits; +import org.geoserver.security.WMTSAccessLimits; +import org.geoserver.wms.GetMapRequest; +import org.geoserver.wms.WMSMapContent; +import org.geotools.api.filter.Filter; +import org.geotools.api.referencing.FactoryException; +import org.geotools.ows.wms.Layer; +import org.geotools.ows.wms.WMSCapabilities; +import org.geotools.ows.wmts.model.TileMatrix; +import org.geotools.ows.wmts.model.TileMatrixSet; +import org.geotools.ows.wmts.model.TileMatrixSetLink; +import org.geotools.ows.wmts.model.WMTSCapabilities; +import org.geotools.ows.wmts.model.WMTSLayer; +import org.geotools.referencing.CRS; +import org.geotools.util.logging.Logging; +import org.locationtech.jts.geom.Geometry; +import org.springframework.security.core.Authentication; +import org.springframework.security.core.context.SecurityContextHolder; +import org.springframework.util.StringUtils; + +/** + * This class takes care of setting up URL Template, including preparing cascaded URL when a + * cascaded Layer is configured to useRemote service, in case the request satisfies criteria to go + * directly through the remote service. + */ +public class MapMLRequestMangler { + + private static final String WMTS = "WMTS"; + private static final String REQUEST = "request"; + private static final String LAYER = "layer"; + private static final String GETMAP = "GETMAP"; + private static final String GETFEATUREINFO = "GETFEATUREINFO"; + private static final String WMS = "WMS"; + private static final String GETTILE = "GETTILE"; + private static final String SERVICE = "service"; + private static final String LAYERS = "layers"; + private static final String CRS_PARAM = "crs"; + private static final String SRS_PARAM = "srs"; + private static final String WMS_1_3_0 = "1.3.0"; + + private static final double ORIGIN_DELTA = 0.1; + private static final double SCALE_DELTA = 1E-5; + + static class CRSMapper { + + Set inputCRSs; + + String outputCRS; + + public CRSMapper(Set inputCRSs, String outputCRS) { + this.inputCRSs = inputCRSs; + this.outputCRS = outputCRS; + } + + boolean isSupporting(String inputCRS) { + return inputCRSs.contains(inputCRS); + } + + String getOutputCRS() { + return outputCRS; + } + } + + private static final Set crsMappers; + + static { + crsMappers = new HashSet<>(); + crsMappers.add( + new CRSMapper( + Set.of( + "EPSG:4326", + "urn:ogc:def:crs:EPSG::4326", + "urn:ogc:def:crs:MapML::WGS84"), + "EPSG:4326")); + crsMappers.add( + new CRSMapper( + Set.of("EPSG:3857", "urn:ogc:def:crs:EPSG::3857", "MapML:OSMTILE"), + "EPSG:3857")); + crsMappers.add( + new CRSMapper( + Set.of("EPSG:5936", "urn:ogc:def:crs:EPSG::5936", "MapML:APSTILE"), + "EPSG:5936")); + crsMappers.add( + new CRSMapper( + Set.of("EPSG:3978", "urn:ogc:def:crs:EPSG::3978", "MapML:CBMTILE"), + "EPSG:3978")); + } + + private static final Logger LOGGER = Logging.getLogger(MapMLRequestMangler.class); + private final MapMLDocumentBuilder.MapMLLayerMetadata mapMLLayerMetadata; + private final String path; + private final String baseUrlPattern; + private final String proj; + private final HashMap params; + private final WMSMapContent mapContent; + private ResourceAccessManager resourceAccessManager; + + public MapMLRequestMangler( + WMSMapContent mapContent, + MapMLDocumentBuilder.MapMLLayerMetadata mapMLLayerMetadata, + String baseUrlPattern, + String path, + HashMap params, + String proj) { + this.mapContent = mapContent; + this.mapMLLayerMetadata = mapMLLayerMetadata; + this.path = path; + this.params = params; + this.baseUrlPattern = baseUrlPattern; + this.proj = proj; + List resourceAccessManagerList = + GeoServerExtensions.extensions(ResourceAccessManager.class); + // In theory it will always be just a single bean + // Using extensions instead of bean For testing purposes, let's use the latest + // That's not optimal but I didn't find a faster approach to test DataAccessLimits. + this.resourceAccessManager = + resourceAccessManagerList.get(resourceAccessManagerList.size() - 1); + } + + public String getUrlTemplate() { + LayerInfo layerInfo = mapMLLayerMetadata.getLayerInfo(); + String urlTemplate = ""; + try { + if (!canCascade(layerInfo)) { + urlTemplate = + URLDecoder.decode( + ResponseUtils.buildURL( + baseUrlPattern, path, params, URLMangler.URLType.SERVICE), + "UTF-8"); + } else { + urlTemplate = tryCascading(path, params, layerInfo); + } + } catch (UnsupportedEncodingException uee) { + } + return urlTemplate; + } + + /** + * Check metadata, request Params and layerInfo configuration to verify if there are minimal + * requirements for a potential cascading to the remote service. + */ + private boolean canCascade(LayerInfo layerInfo) { + if (mapMLLayerMetadata.isUseRemote()) { + if (hasRestrictingAccessLimits(layerInfo)) return false; + if (hasVendorParams()) return false; + ResourceInfo resource = layerInfo.getResource(); + StoreInfo storeInfo = resource.getStore(); + // Not supporting cross-requests yet: + // GetTiles against remote WMS + // GetMap against remote WMTS + String service = params.get(SERVICE); + String request = params.get(REQUEST); + if (storeInfo instanceof WMTSStoreInfo) { + if (GETMAP.equalsIgnoreCase(request) + || (GETFEATUREINFO.equalsIgnoreCase(request) + && WMS.equalsIgnoreCase(service))) { + return false; + } + } else if (storeInfo instanceof WMSStoreInfo) { + if (GETTILE.equalsIgnoreCase(request) + || (GETFEATUREINFO.equalsIgnoreCase(request) + && WMTS.equalsIgnoreCase(service))) { + return false; + } + } + return true; + } + return false; + } + /** + * Try cascading to the remote Server unless any condition is preventing that (i.e. CRS not + * supported on the remote server) + */ + private String tryCascading(String path, HashMap params, LayerInfo layerInfo) + throws UnsupportedEncodingException { + String baseUrl = baseUrlPattern; + String version = "1.3.0"; + String reason = null; + boolean doCascade = false; + if (layerInfo != null) { + ResourceInfo resourceInfo = layerInfo.getResource(); + String layerName = resourceInfo.getNativeName(); + + String requestedCRS = proj; + String outputCRS = null; + for (CRSMapper mapper : crsMappers) { + if (mapper.isSupporting(requestedCRS)) { + outputCRS = mapper.getOutputCRS(); + requestedCRS = outputCRS; + break; + } + } + + if (resourceInfo != null) { + String capabilitiesURL = null; + String tileMatrixSet = null; + StoreInfo storeInfo = resourceInfo.getStore(); + reason = + "RequestedCRS " + requestedCRS + " is not supported by layer: " + layerName; + if (storeInfo instanceof WMSStoreInfo) { + WMSStoreInfo wmsStoreInfo = (WMSStoreInfo) storeInfo; + capabilitiesURL = wmsStoreInfo.getCapabilitiesURL(); + try { + WMSCapabilities capabilities = + wmsStoreInfo.getWebMapServer(null).getCapabilities(); + version = capabilities.getVersion(); + + if (!WMS_1_3_0.equals(version)) { + version = "1.1.1"; + } + List layerList = capabilities.getLayerList(); + boolean isSupportedCrs = false; + for (Layer layer : layerList) { + if (layerName.equals(layer.getName())) { + isSupportedCrs = isSRSInLayerOrParents(layer, requestedCRS); + break; + } + } + isSupportedCrs &= (outputCRS != null); + doCascade = isSupportedCrs; + } catch (IOException e) { + reason = + "Unable to extract the WMS remote capabilities. Cascading won't be performed"; + LOGGER.warning(reason + "due to:" + e); + doCascade = false; + } + } else if (storeInfo instanceof WMTSStoreInfo) { + WMTSStoreInfo wmtsStoreInfo = (WMTSStoreInfo) storeInfo; + capabilitiesURL = wmtsStoreInfo.getCapabilitiesURL(); + try { + WMTSCapabilities capabilities = + wmtsStoreInfo.getWebMapTileServer(null).getCapabilities(); + version = capabilities.getVersion(); + List layerList = capabilities.getLayerList(); + boolean isSupportedCrs = false; + // Let's check if the capabilities document has a matching layer + // supporting a compatible CRS/GridSet + for (WMTSLayer layer : layerList) { + if (layerName.equals(layer.getName())) { + tileMatrixSet = + getSupportedWMTSGridSet(layer, requestedCRS, capabilities); + isSupportedCrs = tileMatrixSet != null; + break; + } + } + isSupportedCrs &= (outputCRS != null); + doCascade = isSupportedCrs; + } catch (IOException | FactoryException e) { + reason = + "Unable to extract the WMTS remote capabilities. Cascading won't be performed"; + LOGGER.warning(reason + "due to:" + e); + doCascade = false; + } + } + if (doCascade) { + // Update the params + baseUrl = getBaseUrl(capabilitiesURL); + refineRequestParams(params, layerName, version, requestedCRS, tileMatrixSet); + } else { + LOGGER.fine("Cascading won't be performed, due to: " + reason); + } + } + } + + URLMangler.URLType urlType = + doCascade ? URLMangler.URLType.EXTERNAL : URLMangler.URLType.SERVICE; + String urlTemplate = + URLDecoder.decode(ResponseUtils.buildURL(baseUrl, path, params, urlType), "UTF-8"); + return urlTemplate; + } + + private void cleanupCRS(HashMap params, String version, String requestedCRS) { + boolean cleanupCrs = params.containsKey(CRS_PARAM) || params.containsKey(SRS_PARAM); + if (cleanupCrs) { + params.remove(CRS_PARAM); + params.remove(SRS_PARAM); + String crsName = WMS_1_3_0.equals(version) ? CRS_PARAM : SRS_PARAM; + params.put(crsName, requestedCRS); + } + } + + private void refineRequestParams( + HashMap params, + String layerName, + String version, + String requestedCRS, + String tileMatrixSetName) { + String requestType = params.get(REQUEST); + String service = params.get(SERVICE); + if (params.containsKey(LAYER)) { + params.put(LAYER, layerName); + } else if (params.containsKey(LAYERS)) { + params.put(LAYERS, layerName); + } + if (GETMAP.equalsIgnoreCase(requestType) || GETFEATUREINFO.equalsIgnoreCase(requestType)) { + params.put("version", version); + if (params.containsKey("query_layers")) { + params.put("query_layers", layerName); + } + if (params.containsKey("info_format")) { + params.put("info_format", "text/html"); + } else if (params.containsKey("infoformat")) { + params.put("infoformat", "text/html"); + } + cleanupCRS(params, version, requestedCRS); + } + // Extra settings for WMTS + if (WMTS.equalsIgnoreCase(service)) { + String[] tileMatrixSetSchema = tileMatrixSetName.split(";"); + tileMatrixSetName = tileMatrixSetSchema[0]; + if (tileMatrixSetSchema.length == 2) { + params.put("tilematrix", tileMatrixSetSchema[1] + "{z}"); + } + if (params.containsKey("tilematrixset")) { + params.put("tilematrixset", tileMatrixSetName); + } + params.remove("style"); + } + } + + @SuppressWarnings("PMD.UseCollectionIsEmpty") + private boolean hasVendorParams() { + GetMapRequest req = mapContent.getRequest(); + Map kvpMap = req.getRawKvp(); + + // The following vendor params have been retrieved from the WMSRequests class. + // format options + Map formatOptions = req.getFormatOptions(); + if (formatOptions != null + && formatOptions.size() >= 1 + && !formatOptions.containsKey( + MapMLConstants.MAPML_WMS_MIME_TYPE_OPTION.toUpperCase())) { + return true; + } + + // view params + if (req.getViewParams() != null && !req.getViewParams().isEmpty()) { + return true; + } + if (req.getEnv() != null && !req.getEnv().isEmpty()) { + return true; + } + + if (req.getMaxFeatures() != null + || req.getRemoteOwsType() != null + || req.getRemoteOwsURL() != null + || req.getScaleMethod() != null + || req.getStartIndex() != null) { + return true; + } + + if (!req.getStyleFormat().equals(SLDHandler.FORMAT)) { + return true; + } + if (req.getStyleVersion() != null) { + return true; + } + + // Boolean params + if (req.isTiled() || Boolean.TRUE.equals(req.getValidateSchema())) { + return true; + } + + if (hasProperty( + kvpMap, + "propertyName", + "bgcolor", + "tilesOrigin", + "palette", + "interpolations", + "clip")) { + return true; + } + + // numeric params + if (req.getBuffer() > 0 || Double.compare(req.getAngle(), 0.0) != 0) { + return true; + } + if (req.getCQLFilter() != null && !req.getCQLFilter().isEmpty()) { + return true; + } + + return false; + } + + private boolean hasProperty(Map kvpMap, String... properties) { + for (String property : properties) { + String prop = kvpMap.get(property); + if (StringUtils.hasText(prop)) { + return true; + } + } + return false; + } + + private boolean hasRestrictingAccessLimits(LayerInfo layerInfo) { + Authentication auth = SecurityContextHolder.getContext().getAuthentication(); + DataAccessLimits accessLimits = resourceAccessManager.getAccessLimits(auth, layerInfo); + + // If there is any access limits effectively affecting the layer + // we are not going to cascade so that the vendor param can be + // honored by the local GeoServer + if (accessLimits != null) { + Filter readFilter = accessLimits.getReadFilter(); + if (readFilter != null && readFilter != Filter.INCLUDE) { + return true; + } + Geometry geom = null; + if (accessLimits instanceof WMSAccessLimits) { + WMSAccessLimits limits = (WMSAccessLimits) accessLimits; + geom = limits.getRasterFilter(); + } + if (accessLimits instanceof WMTSAccessLimits) { + WMTSAccessLimits limits = (WMTSAccessLimits) accessLimits; + geom = limits.getRasterFilter(); + } + if (accessLimits instanceof VectorAccessLimits) { + VectorAccessLimits limits = (VectorAccessLimits) accessLimits; + geom = limits.getClipVectorFilter(); + } + if (accessLimits instanceof CoverageAccessLimits) { + CoverageAccessLimits limits = (CoverageAccessLimits) accessLimits; + geom = limits.getRasterFilter(); + } + if (geom != null) { + return true; + } + } + return false; + } + + private boolean isSRSSupported(Layer layer, String srs) { + Set supportedSRS = layer.getSrs(); + return supportedSRS != null && supportedSRS.contains(srs); + } + + private String getSupportedWMTSGridSet( + WMTSLayer layer, String srs, WMTSCapabilities capabilities) throws FactoryException { + TiledCRSParams inputCrs = TiledCRSConstants.lookupTCRS(srs); + if (inputCrs == null) { + return null; + } + Map tileMatrixLinks = layer.getTileMatrixLinks(); + Collection values = tileMatrixLinks.values(); + for (TileMatrixSetLink value : values) { + String tileMatrixSetName = value.getIdentifier(); + TileMatrixSet tileMatrixSet = capabilities.getMatrixSet(tileMatrixSetName); + + // First check: same CRS + // Simpler name equality may not work (i.e. urn:ogc:def:crs:EPSG::3857 vs + // urn:x-ogc:def:crs:EPSG:3857) + if (!CRS.isEquivalent( + CRS.decode(inputCrs.getCode()), CRS.decode(tileMatrixSet.getCrs()))) { + continue; + } + + List tileMatrices = tileMatrixSet.getMatrices(); + double[] tiledCRSScales = inputCrs.getScales(); + + // check same number of levels + if (tileMatrices.size() != tiledCRSScales.length) { + continue; + } + TileMatrix level0 = tileMatrices.get(0); + int tiledCRStileSize = inputCrs.getTILE_SIZE(); + if (tiledCRStileSize != level0.getTileHeight() + || tiledCRStileSize != level0.getTileWidth()) { + continue; + } + + // check same origin + org.locationtech.jts.geom.Point origin = level0.getTopLeft(); + Point tCRSorigin = inputCrs.getOrigin(); + + double deltaCoordinate = + tileMatrices.get(tileMatrices.size() - 1).getResolution() * ORIGIN_DELTA; + if (Math.abs(tCRSorigin.x - origin.getX()) > deltaCoordinate + || Math.abs(tCRSorigin.y - origin.getY()) > deltaCoordinate) { + continue; + } + + // check same scales + for (int i = 0; i < tileMatrices.size(); i++) { + if (Math.abs(tileMatrices.get(i).getDenominator() - tiledCRSScales[i]) + > SCALE_DELTA) { + continue; + } + } + String prefix = findCommonPrefix(tileMatrices); + if (prefix != null) { + tileMatrixSetName += (";" + prefix); + } + return tileMatrixSetName; + } + return null; + } + + private boolean isSRSInLayerOrParents(Layer layer, String srs) { + // Check if the current layer supports the SRS + if (isSRSSupported(layer, srs)) { + return true; + } + + // If not, check the parent layers recursively + Layer parentLayer = layer.getParent(); + while (parentLayer != null) { + if (isSRSSupported(parentLayer, srs)) { + return true; + } + parentLayer = parentLayer.getParent(); + } + + // Return false if no layer supports the SRS + return false; + } + + private String findCommonPrefix(List tileMatrixLevels) { + // Check for levels having a common prefix, e.g.: + // EPSG:4326:0 + // EPSG:4326:1 + // EPSG:4326:2 + // EPSG:4326:3 + + // Since TileMatrix is a {z} level in MapML client, we will + // prefix the value with the common prefix if available + if (tileMatrixLevels == null || tileMatrixLevels.isEmpty()) { + return null; + } + + // Start with the first level as the prefix candidate + String prefix = tileMatrixLevels.get(0).getIdentifier(); + + // Iterate over the rest of the levels and trim the prefix + for (int i = 1; i < tileMatrixLevels.size(); i++) { + while (tileMatrixLevels.get(i).getIdentifier().indexOf(prefix) != 0) { + // Trim the last character from the prefix until it matches + prefix = prefix.substring(0, prefix.length() - 1); + if (prefix.isEmpty()) { + return null; // No common prefix found + } + } + } + + // Check if the remaining prefix is actually a valid common prefix (not just a number) + if (prefix.matches("\\d+")) { + return null; // A prefix consisting of only numbers is not valid + } + + return prefix; + } + + private String getBaseUrl(String capabilitiesURL) { + try { + URL url = new URL(capabilitiesURL); + String protocol = url.getProtocol(); + String host = url.getHost(); + int port = url.getPort(); + String baseURL = protocol + "://" + host; + if (port != -1) { + baseURL += ":" + port; + } + // Optionally, add the context path if needed + String path = url.getPath(); + int contextPathEnd = path.indexOf("/", 1); + if (contextPathEnd != -1) { + baseURL += path.substring(0, contextPathEnd + 1); + } + return baseURL; + } catch (MalformedURLException e) { + return null; + } + } +} diff --git a/src/extension/mapml/src/main/resources/GeoServerApplication.properties b/src/extension/mapml/src/main/resources/GeoServerApplication.properties index 431dce842d2..6820ce6a2fc 100644 --- a/src/extension/mapml/src/main/resources/GeoServerApplication.properties +++ b/src/extension/mapml/src/main/resources/GeoServerApplication.properties @@ -16,6 +16,8 @@ MapMLLayerConfigurationPanel.mapmlLicenseLink=License Link MapMLLayerConfigurationPanel.mapmlTileSection=Tile Settings MapMLLayerConfigurationPanel.mapmlVectorSection=Vector Settings MapMLLayerConfigurationPanel.mapmlUseTiles=Use Tiles +MapMLLayerConfigurationPanel.mapmlClientRequestsSection=Client Requests +MapMLLayerConfigurationPanel.mapmlUseRemote=Remote MapMLLayerConfigurationPanel.mapmlUseFeatures=Use Features MapMLLayerConfigurationPanel.mapmlDimensionSection=Dimension Config MapMLLayerConfigurationPanel.mapmlDefaultMimeSection=Default Mime Type Config diff --git a/src/extension/mapml/src/test/java/org/geoserver/mapml/MapMLBaseProxyTest.java b/src/extension/mapml/src/test/java/org/geoserver/mapml/MapMLBaseProxyTest.java new file mode 100644 index 00000000000..1cc767535cb --- /dev/null +++ b/src/extension/mapml/src/test/java/org/geoserver/mapml/MapMLBaseProxyTest.java @@ -0,0 +1,153 @@ +/* (c) 2024 Open Source Geospatial Foundation - all rights reserved + * This code is licensed under the GPL 2.0 license, available at the root + * application directory. + */ +package org.geoserver.mapml; + +import static com.github.tomakehurst.wiremock.client.WireMock.aResponse; +import static com.github.tomakehurst.wiremock.client.WireMock.urlEqualTo; +import static com.github.tomakehurst.wiremock.core.WireMockConfiguration.wireMockConfig; +import static org.junit.Assert.assertTrue; + +import com.github.tomakehurst.wiremock.WireMockServer; +import com.github.tomakehurst.wiremock.client.WireMock; +import com.github.tomakehurst.wiremock.core.WireMockConfiguration; +import java.io.ByteArrayInputStream; +import java.io.StringWriter; +import java.util.Map; +import javax.xml.transform.Transformer; +import javax.xml.transform.TransformerException; +import javax.xml.transform.TransformerFactory; +import javax.xml.transform.dom.DOMSource; +import javax.xml.transform.stream.StreamResult; +import org.custommonkey.xmlunit.XMLUnit; +import org.custommonkey.xmlunit.XpathEngine; +import org.junit.AfterClass; +import org.junit.Before; +import org.springframework.http.MediaType; +import org.springframework.mock.web.MockHttpServletRequest; +import org.w3c.dom.Document; + +public class MapMLBaseProxyTest extends MapMLTestSupport { + + protected static String CAPABILITIES_URL; + + protected static String MOCK_SERVER; + + protected static String CONTEXT; + + protected static String PATH; + + protected static void initMockService(String server, String context, String path, String file) { + MOCK_SERVER = server; + CONTEXT = context; + PATH = path; + CAPABILITIES_URL = MOCK_SERVER + CONTEXT + "?" + PATH; + WireMockConfiguration config = wireMockConfig().dynamicPort(); + mockService = new WireMockServer(config); + mockService.start(); + mockService.stubFor( + WireMock.get(urlEqualTo(CAPABILITIES_URL)) + .willReturn( + aResponse() + .withStatus(200) + .withHeader("Content-Type", MediaType.TEXT_XML_VALUE) + .withBodyFile(file))); + } + + public static String getCapabilitiesURL() { + return CAPABILITIES_URL; + } + + @AfterClass + public static void afterClass() throws Exception { + mockService.shutdown(); + } + + protected static final String BASE_REQUEST = + "wms?LAYERS=cascadedLayer" + + "&STYLES=&FORMAT=" + + MapMLConstants.MAPML_MIME_TYPE + + "&SERVICE=WMS&VERSION=1.3.0" + + "&REQUEST=GetMap" + + "&SRS=EPSG:4326" + + "&BBOX=0,0,1,1" + + "&WIDTH=150" + + "&HEIGHT=150" + + "&format_options=" + + MapMLConstants.MAPML_WMS_MIME_TYPE_OPTION + + ":image/png"; + + protected XpathEngine xpath; + + protected static WireMockServer mockService; + + @Override + protected void registerNamespaces(Map namespaces) { + namespaces.put("wms", "http://www.opengis.net/wms"); + namespaces.put("ows", "http://www.opengis.net/ows"); + namespaces.put("html", "http://www.w3.org/1999/xhtml"); + } + + @Before + public void setup() { + xpath = XMLUnit.newXpathEngine(); + } + + protected void checkCascading(String path, boolean shouldCascade, String rel) throws Exception { + Document doc = getMapML(path); + // printDocument(doc); + String url = xpath.evaluate("//html:map-link[@rel='" + rel + "']/@tref", doc); + assertCascading(shouldCascade, url); + + url = xpath.evaluate("//html:map-link[@rel='query']/@tref", doc); + assertCascading(shouldCascade, url); + } + + protected void assertCascading(boolean shouldCascade, String url) { + if (shouldCascade) { + assertTrue( + url.startsWith( + "http://localhost:" + mockService.port() + MOCK_SERVER + CONTEXT)); + assertTrue(url.contains("layers=topp:states")); + } else { + assertTrue(url.startsWith("http://localhost:8080/geoserver" + CONTEXT)); + assertTrue(url.contains("layers=cascadedLayer")); + } + } + + /** + * Executes a request using the GET method and returns the result as an MapML document. + * + * @param path The portion of the request after the context, example: + * @return A result of the request parsed into a dom. + */ + protected org.w3c.dom.Document getMapML(final String path) throws Exception { + MockHttpServletRequest request = createRequest(path, false); + request.addHeader("Accept", "text/mapml"); + request.setMethod("GET"); + request.setContent(new byte[] {}); + String resp = dispatch(request, "UTF-8").getContentAsString(); + return dom(new ByteArrayInputStream(resp.getBytes()), true); + } + + /** For debugging purposes */ + public static void printDocument(Document doc) throws TransformerException { + // Initialize a transformer + TransformerFactory transformerFactory = TransformerFactory.newInstance(); + Transformer transformer = transformerFactory.newTransformer(); + + // Set output properties for the transformer (optional, for pretty print) + transformer.setOutputProperty(javax.xml.transform.OutputKeys.INDENT, "yes"); + transformer.setOutputProperty("{http://xml.apache.org/xslt}indent-amount", "2"); + + // Convert the DOM Document to a String + StringWriter writer = new StringWriter(); + StreamResult result = new StreamResult(writer); + DOMSource source = new DOMSource(doc); + transformer.transform(source, result); + + // Return the XML string + // System.out.println(writer.toString()); + } +} diff --git a/src/extension/mapml/src/test/java/org/geoserver/mapml/MapMLWMSDataAccessProxyTest.java b/src/extension/mapml/src/test/java/org/geoserver/mapml/MapMLWMSDataAccessProxyTest.java new file mode 100644 index 00000000000..6d12ac10fac --- /dev/null +++ b/src/extension/mapml/src/test/java/org/geoserver/mapml/MapMLWMSDataAccessProxyTest.java @@ -0,0 +1,108 @@ +/* (c) 2024 Open Source Geospatial Foundation - all rights reserved + * This code is licensed under the GPL 2.0 license, available at the root + * application directory. + */ +package org.geoserver.mapml; + +import java.util.Collections; +import java.util.List; +import org.geoserver.catalog.Catalog; +import org.geoserver.catalog.LayerInfo; +import org.geoserver.catalog.WMSLayerInfo; +import org.geoserver.catalog.WMSStoreInfo; +import org.geoserver.data.test.SystemTestData; +import org.geoserver.security.CatalogMode; +import org.geoserver.security.DataAccessLimits; +import org.geoserver.security.TestResourceAccessManager; +import org.geotools.api.filter.Filter; +import org.geotools.api.filter.FilterFactory; +import org.geotools.factory.CommonFactoryFinder; +import org.geotools.geometry.jts.JTSFactoryFinder; +import org.junit.BeforeClass; +import org.junit.Test; +import org.locationtech.jts.geom.GeometryFactory; +import org.locationtech.jts.geom.Polygon; +import org.locationtech.jts.io.WKTReader; +import org.springframework.security.authentication.UsernamePasswordAuthenticationToken; +import org.springframework.security.core.Authentication; +import org.springframework.security.core.authority.SimpleGrantedAuthority; +import org.springframework.security.core.context.SecurityContextHolder; +import org.springframework.security.core.userdetails.User; + +public class MapMLWMSDataAccessProxyTest extends MapMLBaseProxyTest { + + @BeforeClass + public static void beforeClass() { + initMockService( + "/mockgeoserver", + "/wms", + "REQUEST=GetCapabilities&VERSION=1.3.0&SERVICE=WMS", + "wmscaps.xml"); + } + + @Override + protected void setUpSpring(List springContextLocations) { + super.setUpSpring(springContextLocations); + springContextLocations.add("classpath:/org/geoserver/wms/ResourceAccessManagerContext.xml"); + } + + @Override + protected void onSetUp(SystemTestData testData) throws Exception { + super.onSetUp(testData); + Catalog catalog = getCatalog(); + + WMSStoreInfo wmsStore = catalog.getFactory().createWebMapServer(); + wmsStore.setName("wmsStore"); + wmsStore.setWorkspace(catalog.getDefaultWorkspace()); + wmsStore.setCapabilitiesURL( + "http://localhost:" + mockService.port() + getCapabilitiesURL()); + wmsStore.setEnabled(true); + catalog.add(wmsStore); + + // Create A layer with access limits + WMSLayerInfo wmsLayer2 = catalog.getFactory().createWMSLayer(); + wmsLayer2.setName("cascadedLayerAccessLimits"); + wmsLayer2.setNativeName("topp:states"); + wmsLayer2.setStore(wmsStore); + wmsLayer2.setAdvertised(true); + wmsLayer2.setEnabled(true); + wmsLayer2.getMetadata().put("mapml.useRemote", true); + + LayerInfo layer2 = catalog.getFactory().createLayer(); + layer2.setResource(wmsLayer2); + layer2.setDefaultStyle(catalog.getStyleByName("default")); + catalog.add(wmsLayer2); + catalog.add(layer2); + + TestResourceAccessManager tam = + (TestResourceAccessManager) applicationContext.getBean("testResourceAccessManager"); + + String username = "testUser"; + String password = "testPassword"; + SimpleGrantedAuthority authority = new SimpleGrantedAuthority("ROLE_USER"); + User user = new User(username, password, Collections.singletonList(authority)); + Authentication auth = + new UsernamePasswordAuthenticationToken(user, password, user.getAuthorities()); + SecurityContextHolder.getContext().setAuthentication(auth); + + String wkt = + "POLYGON((-31.266001 34.307144, 39.869301 34.307144, 39.869301 71.185474, -31.266001 71.185474, -31.266001 34.307144))"; + GeometryFactory geometryFactory = JTSFactoryFinder.getGeometryFactory(); + WKTReader wktReader = new WKTReader(geometryFactory); + Polygon polygon = (Polygon) wktReader.read(wkt); + + // Create a filter that performs a spatial intersection with the polygon + FilterFactory FF = CommonFactoryFinder.getFilterFactory(); + Filter intersectsFilter = FF.intersects(FF.property("the_geom"), FF.literal(polygon)); + tam.putLimits( + username, layer2, new DataAccessLimits(CatalogMode.CHALLENGE, intersectsFilter)); + } + + @Test + public void testMapMLNotCascadingWithAccessLimits() throws Exception { + // get the mapml doc for the layer + String path = BASE_REQUEST.replace("cascadedLayer", "cascadedLayerAccessLimits"); + + checkCascading(path, false, "image"); + } +} diff --git a/src/extension/mapml/src/test/java/org/geoserver/mapml/MapMLWMSProxyTest.java b/src/extension/mapml/src/test/java/org/geoserver/mapml/MapMLWMSProxyTest.java new file mode 100644 index 00000000000..d77693c730e --- /dev/null +++ b/src/extension/mapml/src/test/java/org/geoserver/mapml/MapMLWMSProxyTest.java @@ -0,0 +1,129 @@ +/* (c) 2024 Open Source Geospatial Foundation - all rights reserved + * This code is licensed under the GPL 2.0 license, available at the root + * application directory. + */ +package org.geoserver.mapml; + +import org.geoserver.catalog.Catalog; +import org.geoserver.catalog.LayerInfo; +import org.geoserver.catalog.ResourceInfo; +import org.geoserver.catalog.WMSLayerInfo; +import org.geoserver.catalog.WMSStoreInfo; +import org.geoserver.data.test.SystemTestData; +import org.junit.BeforeClass; +import org.junit.Test; + +public class MapMLWMSProxyTest extends MapMLBaseProxyTest { + + @BeforeClass + public static void beforeClass() { + initMockService( + "/mockgeoserver", + "/wms", + "REQUEST=GetCapabilities&VERSION=1.3.0&SERVICE=WMS", + "wmscaps.xml"); + } + + @Override + protected void onSetUp(SystemTestData testData) throws Exception { + super.onSetUp(testData); + Catalog catalog = getCatalog(); + + WMSStoreInfo wmsStore = catalog.getFactory().createWebMapServer(); + wmsStore.setName("wmsStore"); + wmsStore.setWorkspace(catalog.getDefaultWorkspace()); + wmsStore.setCapabilitiesURL( + "http://localhost:" + mockService.port() + getCapabilitiesURL()); + wmsStore.setEnabled(true); + catalog.add(wmsStore); + + // Create WMSLayerInfo using the Catalog factory + WMSLayerInfo wmsLayer = catalog.getFactory().createWMSLayer(); + wmsLayer.setName("cascadedLayer"); + wmsLayer.setNativeName("topp:states"); + wmsLayer.setStore(wmsStore); + wmsLayer.setAdvertised(true); + wmsLayer.setEnabled(true); + + // Add the layer to the catalog + LayerInfo layer = catalog.getFactory().createLayer(); + layer.setResource(wmsLayer); + layer.setDefaultStyle(catalog.getStyleByName("default")); + catalog.add(wmsLayer); + catalog.add(layer); + } + + @Test + public void testMapMLRemoteFlag() throws Exception { + Catalog cat = getCatalog(); + // Verify the layer was added + LayerInfo layerInfo = cat.getLayerByName("cascadedLayer"); + ResourceInfo layerMeta = layerInfo.getResource(); + layerMeta.getMetadata().put("mapml.useRemote", false); + cat.save(layerMeta); + + // get the mapml doc for the layer + String path = BASE_REQUEST; + + // Verify that Remote set to false is not cascading + checkCascading(path, false, "image"); + + // Now switching to use Remote URL + layerMeta.getMetadata().put("mapml.useRemote", true); + cat.save(layerMeta); + + // verify that is cascading to the remote URL + checkCascading(path, true, "image"); + } + + @Test + public void testMapMLUnsupportedCRSNotCascading() throws Exception { + Catalog cat = getCatalog(); + // Verify the layer was added + LayerInfo layerInfo = cat.getLayerByName("cascadedLayer"); + ResourceInfo layerMeta = layerInfo.getResource(); + layerMeta.getMetadata().put("mapml.useRemote", true); + cat.save(layerMeta); + + // get the mapml doc for the layer + String path = BASE_REQUEST.replace("EPSG:4326", "EPSG:3857"); + + // Verify that asking unsupported CRS in the remote layer is not cascading + checkCascading(path, false, "image"); + } + + @Test + public void testMapMLVendorOptionsNotCascading() throws Exception { + Catalog cat = getCatalog(); + // Verify the layer was added + LayerInfo layerInfo = cat.getLayerByName("cascadedLayer"); + ResourceInfo layerMeta = layerInfo.getResource(); + layerMeta.getMetadata().put("mapml.useRemote", true); + cat.save(layerMeta); + + // Setting up a WMS vendor options. + String path = + BASE_REQUEST + + "&interpolations=bilinear" + + "&clip=srid=3857;POLYGON ((-1615028.3514525702 7475148.401208023, 3844409.956787858 7475148.401208023, 3844409.956787858 3815954.983140064, -1615028.3514525702 3815954.983140064, -1615028.3514525702 7475148.401208023))"; + + // Verify vendor option is not cascading + checkCascading(path, false, "image"); + } + + @Test + public void testMapMLCQLFilterNotCascading() throws Exception { + Catalog cat = getCatalog(); + // Verify the layer was added + LayerInfo layerInfo = cat.getLayerByName("cascadedLayer"); + ResourceInfo layerMeta = layerInfo.getResource(); + layerMeta.getMetadata().put("mapml.useRemote", true); + cat.save(layerMeta); + + // Setting up a WMS vendor options. + String path = BASE_REQUEST + "&cql_filter=STATE_ABBR='MO'"; + + // Verify vendor option is not cascading + checkCascading(path, false, "image"); + } +} diff --git a/src/extension/mapml/src/test/java/org/geoserver/mapml/MapMLWMSTest.java b/src/extension/mapml/src/test/java/org/geoserver/mapml/MapMLWMSTest.java index bfb2e881b82..a6639129cd7 100644 --- a/src/extension/mapml/src/test/java/org/geoserver/mapml/MapMLWMSTest.java +++ b/src/extension/mapml/src/test/java/org/geoserver/mapml/MapMLWMSTest.java @@ -103,7 +103,7 @@ public class MapMLWMSTest extends MapMLTestSupport { - private XpathEngine xpath; + protected XpathEngine xpath; @Override protected void registerNamespaces(Map namespaces) { diff --git a/src/extension/mapml/src/test/java/org/geoserver/mapml/MapMLWMTSProxyTest.java b/src/extension/mapml/src/test/java/org/geoserver/mapml/MapMLWMTSProxyTest.java new file mode 100644 index 00000000000..6ea5071a145 --- /dev/null +++ b/src/extension/mapml/src/test/java/org/geoserver/mapml/MapMLWMTSProxyTest.java @@ -0,0 +1,160 @@ +/* (c) 2024 Open Source Geospatial Foundation - all rights reserved + * This code is licensed under the GPL 2.0 license, available at the root + * application directory. + */ +package org.geoserver.mapml; + +import static org.geowebcache.grid.GridSubsetFactory.createGridSubSet; +import static org.junit.Assert.assertEquals; +import static org.junit.Assert.assertNotNull; +import static org.junit.Assert.assertTrue; + +import java.util.Map; +import org.geoserver.catalog.Catalog; +import org.geoserver.catalog.LayerInfo; +import org.geoserver.catalog.ResourceInfo; +import org.geoserver.catalog.WMTSLayerInfo; +import org.geoserver.catalog.WMTSStoreInfo; +import org.geoserver.data.test.SystemTestData; +import org.geoserver.gwc.GWC; +import org.geoserver.gwc.config.GWCConfig; +import org.geoserver.gwc.layer.GeoServerTileLayer; +import org.geoserver.mapml.gwc.gridset.MapMLGridsets; +import org.geowebcache.grid.GridSubset; +import org.geowebcache.mime.TextMime; +import org.junit.BeforeClass; +import org.junit.Test; + +public class MapMLWMTSProxyTest extends MapMLBaseProxyTest { + + protected static final String BASE_WMTS_REQUEST = + "wms?LAYERS=cascadedLayer" + + "&STYLES=&FORMAT=" + + MapMLConstants.MAPML_MIME_TYPE + + "&SERVICE=WMS&VERSION=1.1.0" + + "&REQUEST=GetMap" + + "&SRS=MapML:OSMTILE" + + "&BBOX=-1.3885038382960921E7,2870337.130793682,-7455049.489182421,6338174.0557576185" + + "&WIDTH=768" + + "&HEIGHT=414" + + "&format_options=" + + MapMLConstants.MAPML_WMS_MIME_TYPE_OPTION + + ":image/png"; + + @Override + protected void registerNamespaces(Map namespaces) { + super.registerNamespaces(namespaces); + namespaces.put("wmts", "http://www.opengis.net/wmts/1.0"); + namespaces.put("gml", "http://www.opengis.net/gml"); + namespaces.put("xlink=", "http://www.w3.org/1999/xlink"); + } + + @BeforeClass + public static void beforeClass() { + initMockService( + "/mockgeoserver", + "/gwc/service/wmts", + "REQUEST=GetCapabilities&VERSION=1.0.0&SERVICE=WMTS", + "wmtscaps.xml"); + } + + @Override + protected void onSetUp(SystemTestData testData) throws Exception { + super.onSetUp(testData); + Catalog catalog = getCatalog(); + + WMTSStoreInfo wmtsStore = catalog.getFactory().createWebMapTileServer(); + wmtsStore.setName("wmtsStore"); + wmtsStore.setWorkspace(catalog.getDefaultWorkspace()); + wmtsStore.setCapabilitiesURL( + "http://localhost:" + mockService.port() + getCapabilitiesURL()); + wmtsStore.setEnabled(true); + catalog.add(wmtsStore); + + // Create WMSLayerInfo using the Catalog factory + WMTSLayerInfo wmtsLayer = catalog.getFactory().createWMTSLayer(); + wmtsLayer.setName("cascadedLayer"); + wmtsLayer.setNativeName("topp:states"); + wmtsLayer.setStore(wmtsStore); + wmtsLayer.setAdvertised(true); + wmtsLayer.setEnabled(true); + + // Add the layer to the catalog + LayerInfo layerInfo = catalog.getFactory().createLayer(); + layerInfo.setResource(wmtsLayer); + layerInfo.setDefaultStyle(catalog.getStyleByName("default")); + catalog.add(wmtsLayer); + catalog.add(layerInfo); + + GWC gwc = applicationContext.getBean(GWC.class); + GWCConfig defaults = GWCConfig.getOldDefaults(); + // it seems just the fact of retrieving the bean causes the + // GridSets to be added to the gwc GridSetBroker, but if you don't do + // this, they are not added automatically + MapMLGridsets mgs = applicationContext.getBean(MapMLGridsets.class); + GridSubset wgs84gridset = createGridSubSet(mgs.getGridSet("WGS84").get()); + GridSubset osmtilegridset = createGridSubSet(mgs.getGridSet("OSMTILE").get()); + + GeoServerTileLayer layerInfoTileLayer = + new GeoServerTileLayer(layerInfo, defaults, gwc.getGridSetBroker()); + layerInfoTileLayer.addGridSubset(wgs84gridset); + layerInfoTileLayer.addGridSubset(osmtilegridset); + layerInfoTileLayer.getInfo().getMimeFormats().add(TextMime.txtMapml.getMimeType()); + gwc.save(layerInfoTileLayer); + } + + @Test + public void testRemoteVsNotRemote() throws Exception { + Catalog cat = getCatalog(); + // Verify the layer was added + LayerInfo li = cat.getLayerByName("cascadedLayer"); + assertNotNull(li); + assertEquals("cascadedLayer", li.getName()); + + ResourceInfo layerMeta = li.getResource(); + layerMeta.getMetadata().put("mapml.useRemote", false); + layerMeta.getMetadata().put("mapml.useTiles", true); + cat.save(layerMeta); + + // get the mapml doc for the layer + String path = BASE_WMTS_REQUEST; + + checkCascading(path, false, "tile"); + + // Now switching to use Remote URL + layerMeta.getMetadata().put("mapml.useRemote", true); + cat.save(layerMeta); + + checkCascading(path, true, "tile"); + } + + @Override + protected void assertCascading(boolean shouldCascade, String url) { + assertTrue(url.contains("service=WMTS")); + if (shouldCascade) { + // The remote capabilities defines a custom GridSet that matches + // the OSMTILE with a different name: MATCHING_OSMTILE + // Identifiers are also not simple numbers but contain a common prefix. + assertTrue( + url.startsWith( + "http://localhost:" + mockService.port() + MOCK_SERVER + CONTEXT)); + assertTrue(url.contains("layer=topp:states")); + assertTrue(url.contains("tilematrixset=MATCHING_OSMTILE")); + // Common prefix has been pre-pended to the tilematrix z input + assertTrue(url.contains("tilematrix=OSM:{z}")); + // GetFeatureInfo + if (url.contains("infoformat")) { + assertTrue(url.contains("infoformat=text/html")); + } + } else { + assertTrue(url.startsWith("http://localhost:8080/geoserver" + CONTEXT)); + assertTrue(url.contains("layer=gs:cascadedLayer")); + assertTrue(url.contains("tilematrixset=OSMTILE")); + assertTrue(url.contains("tilematrix={z}")); + // GetFeatureInfo + if (url.contains("infoformat")) { + assertTrue(url.contains("infoformat=text/mapml")); + } + } + } +} diff --git a/src/extension/mapml/src/test/resources/__files/wmscaps.xml b/src/extension/mapml/src/test/resources/__files/wmscaps.xml new file mode 100644 index 00000000000..44c5a3d6c85 --- /dev/null +++ b/src/extension/mapml/src/test/resources/__files/wmscaps.xml @@ -0,0 +1,114 @@ + + + + WMS + WMS + Minimal test caps document for a WMS 1.3 server + + GEOSERVER + + + + + + + text/xml + + + + + + + + + + image/png + + + + + + + + + + text/plain + application/vnd.ogc.gml + application/vnd.ogc.gml/3.1.1 + text/html + + + + + + + + + + + XML + INIMAGE + BLANK + + + Root + + EPSG:4326 + CRS:84 + + -180 + 180 + -90 + 90 + + + world4326 + world4326 + EPSG:4326 + CRS:84 + + -180 + 180 + -90 + 90 + + + + + anotherLayer + A second layer + EPSG:4326 + CRS:84 + + -180 + 180 + -90 + 90 + + + + + topp:states + An old friend + EPSG:4326 + CRS:84 + + -180 + 180 + -90 + 90 + + + + + + diff --git a/src/extension/mapml/src/test/resources/__files/wmtscaps.xml b/src/extension/mapml/src/test/resources/__files/wmtscaps.xml new file mode 100644 index 00000000000..3fb36dded7d --- /dev/null +++ b/src/extension/mapml/src/test/resources/__files/wmtscaps.xml @@ -0,0 +1,2772 @@ + + + GeoServer cached tile services + Predefined map tiles generated from vector data, raster data, and imagery. + + OGC WMTS + 1.0.0 + NONE + NONE + + + GeoServer + + + Claudius Ptolomaeus + Chief Geographer + + + Alexandria + Egypt + claudius.ptolomaeus@gmail.com + + + + + + + + + + + + KVP + + + + + + + + + + + + + KVP + + + + + + + + + + + + + KVP + + + + + + + + + + USA Population + This is some census data on the states. + + -124.73142200000001 24.955967 + -66.969849 49.371735 + + topp:states + + application/vnd.mapbox-vector-tile + image/png + image/jpeg + text/plain + application/vnd.ogc.gml + text/xml + application/vnd.ogc.gml/3.1.1 + text/xml + text/html + application/json + text/mapml + + WebMercatorQuad + + + 0 + 0 + 0 + 0 + 0 + + + 1 + 0 + 0 + 0 + 0 + + + 2 + 1 + 1 + 0 + 1 + + + 3 + 2 + 3 + 1 + 2 + + + 4 + 5 + 6 + 2 + 5 + + + 5 + 10 + 13 + 4 + 10 + + + 6 + 21 + 27 + 9 + 20 + + + 7 + 43 + 54 + 19 + 40 + + + 8 + 87 + 109 + 39 + 80 + + + 9 + 175 + 219 + 78 + 160 + + + 10 + 350 + 438 + 157 + 321 + + + 11 + 700 + 877 + 314 + 643 + + + 12 + 1400 + 1754 + 628 + 1286 + + + 13 + 2800 + 3509 + 1257 + 2572 + + + 14 + 5600 + 7018 + 2515 + 5144 + + + 15 + 11201 + 14037 + 5030 + 10288 + + + 16 + 22402 + 28074 + 10061 + 20576 + + + 17 + 44805 + 56148 + 20122 + 41153 + + + 18 + 89611 + 112296 + 40245 + 82306 + + + 19 + 179223 + 224592 + 80490 + 164612 + + + 20 + 358447 + 449184 + 160981 + 329224 + + + 21 + 716895 + 898369 + 321962 + 658448 + + + 22 + 1433790 + 1796738 + 643925 + 1316896 + + + 23 + 2867580 + 3593477 + 1287851 + 2633793 + + + 24 + 5735161 + 7186954 + 2575702 + 5267586 + + + + + EPSG:4326 + + + EPSG:4326:0 + 0 + 0 + 0 + 0 + + + EPSG:4326:1 + 0 + 0 + 0 + 1 + + + EPSG:4326:2 + 0 + 1 + 1 + 2 + + + EPSG:4326:3 + 1 + 2 + 2 + 5 + + + EPSG:4326:4 + 3 + 5 + 4 + 10 + + + EPSG:4326:5 + 7 + 11 + 9 + 20 + + + EPSG:4326:6 + 14 + 23 + 19 + 40 + + + EPSG:4326:7 + 28 + 46 + 39 + 80 + + + EPSG:4326:8 + 57 + 92 + 78 + 160 + + + EPSG:4326:9 + 115 + 185 + 157 + 321 + + + EPSG:4326:10 + 231 + 370 + 314 + 643 + + + EPSG:4326:11 + 462 + 740 + 628 + 1286 + + + EPSG:4326:12 + 924 + 1480 + 1257 + 2572 + + + EPSG:4326:13 + 1849 + 2960 + 2515 + 5144 + + + EPSG:4326:14 + 3698 + 5920 + 5030 + 10288 + + + EPSG:4326:15 + 7396 + 11840 + 10061 + 20576 + + + EPSG:4326:16 + 14792 + 23681 + 20122 + 41153 + + + EPSG:4326:17 + 29584 + 47363 + 40245 + 82306 + + + EPSG:4326:18 + 59169 + 94727 + 80490 + 164612 + + + EPSG:4326:19 + 118338 + 189454 + 160981 + 329224 + + + EPSG:4326:20 + 236676 + 378908 + 321962 + 658448 + + + EPSG:4326:21 + 473353 + 757817 + 643925 + 1316896 + + + + + EPSG:900913x2 + + + EPSG:900913x2:0 + 0 + 0 + 0 + 0 + + + EPSG:900913x2:1 + 0 + 0 + 0 + 0 + + + EPSG:900913x2:2 + 1 + 1 + 0 + 1 + + + EPSG:900913x2:3 + 2 + 3 + 1 + 2 + + + EPSG:900913x2:4 + 5 + 6 + 2 + 5 + + + EPSG:900913x2:5 + 10 + 13 + 4 + 10 + + + EPSG:900913x2:6 + 21 + 27 + 9 + 20 + + + EPSG:900913x2:7 + 43 + 54 + 19 + 40 + + + EPSG:900913x2:8 + 87 + 109 + 39 + 80 + + + EPSG:900913x2:9 + 175 + 219 + 78 + 160 + + + EPSG:900913x2:10 + 350 + 438 + 157 + 321 + + + EPSG:900913x2:11 + 700 + 877 + 314 + 643 + + + EPSG:900913x2:12 + 1400 + 1754 + 628 + 1286 + + + EPSG:900913x2:13 + 2800 + 3509 + 1257 + 2572 + + + EPSG:900913x2:14 + 5600 + 7018 + 2515 + 5144 + + + EPSG:900913x2:15 + 11201 + 14037 + 5030 + 10288 + + + EPSG:900913x2:16 + 22402 + 28074 + 10061 + 20576 + + + EPSG:900913x2:17 + 44805 + 56148 + 20122 + 41153 + + + EPSG:900913x2:18 + 89611 + 112296 + 40245 + 82306 + + + EPSG:900913x2:19 + 179223 + 224592 + 80490 + 164612 + + + EPSG:900913x2:20 + 358447 + 449184 + 160981 + 329224 + + + EPSG:900913x2:21 + 716895 + 898369 + 321962 + 658448 + + + EPSG:900913x2:22 + 1433790 + 1796738 + 643925 + 1316896 + + + EPSG:900913x2:23 + 2867580 + 3593477 + 1287851 + 2633793 + + + EPSG:900913x2:24 + 5735161 + 7186954 + 2575702 + 5267586 + + + EPSG:900913x2:25 + 11470322 + 14373909 + 5151404 + 10535173 + + + EPSG:900913x2:26 + 22940645 + 28747819 + 10302809 + 21070347 + + + EPSG:900913x2:27 + 45881291 + 57495639 + 20605619 + 42140694 + + + EPSG:900913x2:28 + 91762583 + 114991279 + 41211238 + 84281389 + + + EPSG:900913x2:29 + 183525166 + 229982558 + 82422477 + 168562778 + + + EPSG:900913x2:30 + 367050332 + 459965116 + 164844954 + 337125556 + + + + + MATCHING_OSMTILE + + + 0 + 0 + 0 + 0 + 0 + + + 1 + 0 + 0 + 0 + 0 + + + 2 + 1 + 1 + 0 + 1 + + + 3 + 2 + 3 + 1 + 2 + + + 4 + 5 + 6 + 2 + 5 + + + 5 + 10 + 13 + 4 + 10 + + + 6 + 21 + 27 + 9 + 20 + + + 7 + 43 + 54 + 19 + 40 + + + 8 + 87 + 109 + 39 + 80 + + + 9 + 175 + 219 + 78 + 160 + + + 10 + 350 + 438 + 157 + 321 + + + 11 + 700 + 877 + 314 + 643 + + + 12 + 1400 + 1754 + 628 + 1286 + + + 13 + 2800 + 3509 + 1257 + 2572 + + + 14 + 5600 + 7018 + 2515 + 5144 + + + 15 + 11201 + 14037 + 5030 + 10288 + + + 16 + 22402 + 28074 + 10061 + 20576 + + + 17 + 44805 + 56148 + 20122 + 41153 + + + 18 + 89611 + 112296 + 40245 + 82306 + + + + + + + + + + + + + + + + + APSTILE + urn:ogc:def:crs:EPSG::5936 + + 0 + 8.528957619785715E8 + -2.8567784109254867E7 3.256778410925506E7 + 256 + 256 + 1 + 1 + + + 1 + 4.2644788098928577E8 + -2.8567784109254867E7 3.256778410925506E7 + 256 + 256 + 2 + 2 + + + 2 + 2.1322394049464253E8 + -2.8567784109254867E7 3.256778410925506E7 + 256 + 256 + 4 + 4 + + + 3 + 1.066119702473218E8 + -2.8567784109254867E7 3.256778410925506E7 + 256 + 256 + 8 + 8 + + + 4 + 5.330598512366072E7 + -2.8567784109254867E7 3.256778410925506E7 + 256 + 256 + 16 + 16 + + + 5 + 2.665299256183043E7 + -2.8567784109254867E7 3.256778410925506E7 + 256 + 256 + 32 + 32 + + + 6 + 1.332649628091568E7 + -2.8567784109254867E7 3.256778410925506E7 + 256 + 256 + 64 + 64 + + + 7 + 6663248.140457358 + -2.8567784109254867E7 3.256778410925506E7 + 256 + 256 + 128 + 128 + + + 8 + 3331624.070228686 + -2.8567784109254867E7 3.256778410925506E7 + 256 + 256 + 256 + 256 + + + 9 + 1665812.0351148143 + -2.8567784109254867E7 3.256778410925506E7 + 256 + 256 + 512 + 512 + + + 10 + 832906.0175574071 + -2.8567784109254867E7 3.256778410925506E7 + 256 + 256 + 1024 + 1024 + + + 11 + 416453.0087787036 + -2.8567784109254867E7 3.256778410925506E7 + 256 + 256 + 2048 + 2048 + + + 12 + 208226.50438887932 + -2.8567784109254867E7 3.256778410925506E7 + 256 + 256 + 4096 + 4096 + + + 13 + 104113.25219491216 + -2.8567784109254867E7 3.256778410925506E7 + 256 + 256 + 8192 + 8192 + + + 14 + 52056.62609745608 + -2.8567784109254867E7 3.256778410925506E7 + 256 + 256 + 16384 + 16384 + + + 15 + 26028.313048255575 + -2.8567784109254867E7 3.256778410925506E7 + 256 + 256 + 32768 + 32768 + + + 16 + 13014.15652460025 + -2.8567784109254867E7 3.256778410925506E7 + 256 + 256 + 65536 + 65536 + + + 17 + 6507.0782618276435 + -2.8567784109254867E7 3.256778410925506E7 + 256 + 256 + 131072 + 131072 + + + 18 + 3253.5391313863 + -2.8567784109254867E7 3.256778410925506E7 + 256 + 256 + 262144 + 262144 + + + 19 + 1626.7695652206787 + -2.8567784109254867E7 3.256778410925506E7 + 256 + 256 + 524288 + 524288 + + + + CBMTILE + urn:ogc:def:crs:EPSG::3978 + + 0 + 1.3701664308090523E8 + -3.46558E7 3.931E7 + 256 + 256 + 5 + 8 + + + 1 + 8.032010111639273E7 + -3.46558E7 3.931E7 + 256 + 256 + 8 + 14 + + + 2 + 4.7247118303760424E7 + -3.46558E7 3.931E7 + 256 + 256 + 14 + 24 + + + 3 + 2.8348270982256256E7 + -3.46558E7 3.931E7 + 256 + 256 + 22 + 39 + + + 4 + 1.653649140631615E7 + -3.46558E7 3.931E7 + 256 + 256 + 38 + 67 + + + 5 + 9449423.660752086 + -3.46558E7 3.931E7 + 256 + 256 + 66 + 116 + + + 6 + 5669654.196451251 + -3.46558E7 3.931E7 + 256 + 256 + 110 + 193 + + + 7 + 3307298.28126323 + -3.46558E7 3.931E7 + 256 + 256 + 189 + 331 + + + 8 + 1889884.732150417 + -3.46558E7 3.931E7 + 256 + 256 + 330 + 579 + + + 9 + 1133930.8392902503 + -3.46558E7 3.931E7 + 256 + 256 + 550 + 964 + + + 10 + 661459.656252646 + -3.46558E7 3.931E7 + 256 + 256 + 942 + 1652 + + + 11 + 396875.79375158757 + -3.46558E7 3.931E7 + 256 + 256 + 1570 + 2753 + + + 12 + 236235.59151880213 + -3.46558E7 3.931E7 + 256 + 256 + 2638 + 4625 + + + 13 + 137016.64308090523 + -3.46558E7 3.931E7 + 256 + 256 + 4547 + 7974 + + + 14 + 80320.10111639272 + -3.46558E7 3.931E7 + 256 + 256 + 7757 + 13602 + + + 15 + 47247.11830376043 + -3.46558E7 3.931E7 + 256 + 256 + 13186 + 23123 + + + 16 + 28348.270982256254 + -3.46558E7 3.931E7 + 256 + 256 + 21977 + 38539 + + + 17 + 16536.49140631615 + -3.46558E7 3.931E7 + 256 + 256 + 37674 + 66066 + + + 18 + 9449.423660752085 + -3.46558E7 3.931E7 + 256 + 256 + 65929 + 115615 + + + 19 + 5669.654196451251 + -3.46558E7 3.931E7 + 256 + 256 + 109882 + 192692 + + + 20 + 3307.29828126323 + -3.46558E7 3.931E7 + 256 + 256 + 188368 + 330329 + + + 21 + 1889.884732150417 + -3.46558E7 3.931E7 + 256 + 256 + 329644 + 578075 + + + 22 + 1133.93083929025 + -3.46558E7 3.931E7 + 256 + 256 + 549406 + 963458 + + + 23 + 661.4596562526459 + -3.46558E7 3.931E7 + 256 + 256 + 941839 + 1651642 + + + 24 + 396.87579375158754 + -3.46558E7 3.931E7 + 256 + 256 + 1569732 + 2752737 + + + 25 + 236.23559151880212 + -3.46558E7 3.931E7 + 256 + 256 + 2637149 + 4624598 + + + + EPSG:4326 + urn:ogc:def:crs:EPSG::4326 + + EPSG:4326:0 + 2.795411320143589E8 + 90.0 -180.0 + 256 + 256 + 2 + 1 + + + EPSG:4326:1 + 1.3977056600717944E8 + 90.0 -180.0 + 256 + 256 + 4 + 2 + + + EPSG:4326:2 + 6.988528300358972E7 + 90.0 -180.0 + 256 + 256 + 8 + 4 + + + EPSG:4326:3 + 3.494264150179486E7 + 90.0 -180.0 + 256 + 256 + 16 + 8 + + + EPSG:4326:4 + 1.747132075089743E7 + 90.0 -180.0 + 256 + 256 + 32 + 16 + + + EPSG:4326:5 + 8735660.375448715 + 90.0 -180.0 + 256 + 256 + 64 + 32 + + + EPSG:4326:6 + 4367830.1877243575 + 90.0 -180.0 + 256 + 256 + 128 + 64 + + + EPSG:4326:7 + 2183915.0938621787 + 90.0 -180.0 + 256 + 256 + 256 + 128 + + + EPSG:4326:8 + 1091957.5469310894 + 90.0 -180.0 + 256 + 256 + 512 + 256 + + + EPSG:4326:9 + 545978.7734655447 + 90.0 -180.0 + 256 + 256 + 1024 + 512 + + + EPSG:4326:10 + 272989.38673277234 + 90.0 -180.0 + 256 + 256 + 2048 + 1024 + + + EPSG:4326:11 + 136494.69336638617 + 90.0 -180.0 + 256 + 256 + 4096 + 2048 + + + EPSG:4326:12 + 68247.34668319309 + 90.0 -180.0 + 256 + 256 + 8192 + 4096 + + + EPSG:4326:13 + 34123.67334159654 + 90.0 -180.0 + 256 + 256 + 16384 + 8192 + + + EPSG:4326:14 + 17061.83667079827 + 90.0 -180.0 + 256 + 256 + 32768 + 16384 + + + EPSG:4326:15 + 8530.918335399136 + 90.0 -180.0 + 256 + 256 + 65536 + 32768 + + + EPSG:4326:16 + 4265.459167699568 + 90.0 -180.0 + 256 + 256 + 131072 + 65536 + + + EPSG:4326:17 + 2132.729583849784 + 90.0 -180.0 + 256 + 256 + 262144 + 131072 + + + EPSG:4326:18 + 1066.364791924892 + 90.0 -180.0 + 256 + 256 + 524288 + 262144 + + + EPSG:4326:19 + 533.182395962446 + 90.0 -180.0 + 256 + 256 + 1048576 + 524288 + + + EPSG:4326:20 + 266.591197981223 + 90.0 -180.0 + 256 + 256 + 2097152 + 1048576 + + + EPSG:4326:21 + 133.2955989906115 + 90.0 -180.0 + 256 + 256 + 4194304 + 2097152 + + + + EPSG:900913 + urn:ogc:def:crs:EPSG::900913 + + EPSG:900913:0 + 5.590822639508929E8 + -2.003750834E7 2.0037508E7 + 256 + 256 + 1 + 1 + + + EPSG:900913:1 + 2.7954113197544646E8 + -2.003750834E7 2.0037508E7 + 256 + 256 + 2 + 2 + + + EPSG:900913:2 + 1.3977056598772323E8 + -2.003750834E7 2.0037508E7 + 256 + 256 + 4 + 4 + + + EPSG:900913:3 + 6.988528299386162E7 + -2.003750834E7 2.0037508E7 + 256 + 256 + 8 + 8 + + + EPSG:900913:4 + 3.494264149693081E7 + -2.003750834E7 2.0037508E7 + 256 + 256 + 16 + 16 + + + EPSG:900913:5 + 1.7471320748465404E7 + -2.003750834E7 2.0037508E7 + 256 + 256 + 32 + 32 + + + EPSG:900913:6 + 8735660.374232702 + -2.003750834E7 2.0037508E7 + 256 + 256 + 64 + 64 + + + EPSG:900913:7 + 4367830.187116351 + -2.003750834E7 2.0037508E7 + 256 + 256 + 128 + 128 + + + EPSG:900913:8 + 2183915.0935581755 + -2.003750834E7 2.0037508E7 + 256 + 256 + 256 + 256 + + + EPSG:900913:9 + 1091957.5467790877 + -2.003750834E7 2.0037508E7 + 256 + 256 + 512 + 512 + + + EPSG:900913:10 + 545978.7733895439 + -2.003750834E7 2.0037508E7 + 256 + 256 + 1024 + 1024 + + + EPSG:900913:11 + 272989.38669477194 + -2.003750834E7 2.0037508E7 + 256 + 256 + 2048 + 2048 + + + EPSG:900913:12 + 136494.69334738597 + -2.003750834E7 2.0037508E7 + 256 + 256 + 4096 + 4096 + + + EPSG:900913:13 + 68247.34667369298 + -2.003750834E7 2.0037508E7 + 256 + 256 + 8192 + 8192 + + + EPSG:900913:14 + 34123.67333684649 + -2.003750834E7 2.0037508E7 + 256 + 256 + 16384 + 16384 + + + EPSG:900913:15 + 17061.836668423246 + -2.003750834E7 2.0037508E7 + 256 + 256 + 32768 + 32768 + + + EPSG:900913:16 + 8530.918334211623 + -2.003750834E7 2.0037508E7 + 256 + 256 + 65536 + 65536 + + + EPSG:900913:17 + 4265.4591671058115 + -2.003750834E7 2.0037508E7 + 256 + 256 + 131072 + 131072 + + + EPSG:900913:18 + 2132.7295835529058 + -2.003750834E7 2.0037508E7 + 256 + 256 + 262144 + 262144 + + + EPSG:900913:19 + 1066.3647917764529 + -2.003750834E7 2.0037508E7 + 256 + 256 + 524288 + 524288 + + + EPSG:900913:20 + 533.1823958882264 + -2.003750834E7 2.0037508E7 + 256 + 256 + 1048576 + 1048576 + + + EPSG:900913:21 + 266.5911979441132 + -2.003750834E7 2.0037508E7 + 256 + 256 + 2097152 + 2097152 + + + EPSG:900913:22 + 133.2955989720566 + -2.003750834E7 2.0037508E7 + 256 + 256 + 4194304 + 4194304 + + + EPSG:900913:23 + 66.6477994860283 + -2.003750834E7 2.0037508E7 + 256 + 256 + 8388608 + 8388608 + + + EPSG:900913:24 + 33.32389974301415 + -2.003750834E7 2.0037508E7 + 256 + 256 + 16777216 + 16777216 + + + EPSG:900913:25 + 16.661949871507076 + -2.003750834E7 2.0037508E7 + 256 + 256 + 33554432 + 33554432 + + + EPSG:900913:26 + 8.330974935753538 + -2.003750834E7 2.0037508E7 + 256 + 256 + 67108864 + 67108864 + + + EPSG:900913:27 + 4.165487467876769 + -2.003750834E7 2.0037508E7 + 256 + 256 + 134217728 + 134217728 + + + EPSG:900913:28 + 2.0827437339383845 + -2.003750834E7 2.0037508E7 + 256 + 256 + 268435456 + 268435456 + + + EPSG:900913:29 + 1.0413718669691923 + -2.003750834E7 2.0037508E7 + 256 + 256 + 536870912 + 536870912 + + + EPSG:900913:30 + 0.5206859334845961 + -2.003750834E7 2.0037508E7 + 256 + 256 + 1073741824 + 1073741824 + + + + EPSG:900913x2 + urn:ogc:def:crs:EPSG::900913 + + EPSG:900913x2:0 + 2.7954113197544646E8 + -2.003750834E7 2.0037508E7 + 512 + 512 + 1 + 1 + + + EPSG:900913x2:1 + 1.3977056598772323E8 + -2.003750834E7 2.0037508E7 + 512 + 512 + 2 + 2 + + + EPSG:900913x2:2 + 6.988528299386162E7 + -2.003750834E7 2.0037508E7 + 512 + 512 + 4 + 4 + + + EPSG:900913x2:3 + 3.494264149693081E7 + -2.003750834E7 2.0037508E7 + 512 + 512 + 8 + 8 + + + EPSG:900913x2:4 + 1.7471320748465404E7 + -2.003750834E7 2.0037508E7 + 512 + 512 + 16 + 16 + + + EPSG:900913x2:5 + 8735660.374232702 + -2.003750834E7 2.0037508E7 + 512 + 512 + 32 + 32 + + + EPSG:900913x2:6 + 4367830.187116351 + -2.003750834E7 2.0037508E7 + 512 + 512 + 64 + 64 + + + EPSG:900913x2:7 + 2183915.0935581755 + -2.003750834E7 2.0037508E7 + 512 + 512 + 128 + 128 + + + EPSG:900913x2:8 + 1091957.5467790877 + -2.003750834E7 2.0037508E7 + 512 + 512 + 256 + 256 + + + EPSG:900913x2:9 + 545978.7733895439 + -2.003750834E7 2.0037508E7 + 512 + 512 + 512 + 512 + + + EPSG:900913x2:10 + 272989.38669477194 + -2.003750834E7 2.0037508E7 + 512 + 512 + 1024 + 1024 + + + EPSG:900913x2:11 + 136494.69334738597 + -2.003750834E7 2.0037508E7 + 512 + 512 + 2048 + 2048 + + + EPSG:900913x2:12 + 68247.34667369298 + -2.003750834E7 2.0037508E7 + 512 + 512 + 4096 + 4096 + + + EPSG:900913x2:13 + 34123.67333684649 + -2.003750834E7 2.0037508E7 + 512 + 512 + 8192 + 8192 + + + EPSG:900913x2:14 + 17061.836668423246 + -2.003750834E7 2.0037508E7 + 512 + 512 + 16384 + 16384 + + + EPSG:900913x2:15 + 8530.918334211623 + -2.003750834E7 2.0037508E7 + 512 + 512 + 32768 + 32768 + + + EPSG:900913x2:16 + 4265.4591671058115 + -2.003750834E7 2.0037508E7 + 512 + 512 + 65536 + 65536 + + + EPSG:900913x2:17 + 2132.7295835529058 + -2.003750834E7 2.0037508E7 + 512 + 512 + 131072 + 131072 + + + EPSG:900913x2:18 + 1066.3647917764529 + -2.003750834E7 2.0037508E7 + 512 + 512 + 262144 + 262144 + + + EPSG:900913x2:19 + 533.1823958882264 + -2.003750834E7 2.0037508E7 + 512 + 512 + 524288 + 524288 + + + EPSG:900913x2:20 + 266.5911979441132 + -2.003750834E7 2.0037508E7 + 512 + 512 + 1048576 + 1048576 + + + EPSG:900913x2:21 + 133.2955989720566 + -2.003750834E7 2.0037508E7 + 512 + 512 + 2097152 + 2097152 + + + EPSG:900913x2:22 + 66.6477994860283 + -2.003750834E7 2.0037508E7 + 512 + 512 + 4194304 + 4194304 + + + EPSG:900913x2:23 + 33.32389974301415 + -2.003750834E7 2.0037508E7 + 512 + 512 + 8388608 + 8388608 + + + EPSG:900913x2:24 + 16.661949871507076 + -2.003750834E7 2.0037508E7 + 512 + 512 + 16777216 + 16777216 + + + EPSG:900913x2:25 + 8.330974935753538 + -2.003750834E7 2.0037508E7 + 512 + 512 + 33554432 + 33554432 + + + EPSG:900913x2:26 + 4.165487467876769 + -2.003750834E7 2.0037508E7 + 512 + 512 + 67108864 + 67108864 + + + EPSG:900913x2:27 + 2.0827437339383845 + -2.003750834E7 2.0037508E7 + 512 + 512 + 134217728 + 134217728 + + + EPSG:900913x2:28 + 1.0413718669691923 + -2.003750834E7 2.0037508E7 + 512 + 512 + 268435456 + 268435456 + + + EPSG:900913x2:29 + 0.5206859334845961 + -2.003750834E7 2.0037508E7 + 512 + 512 + 536870912 + 536870912 + + + EPSG:900913x2:30 + 0.26034296674229807 + -2.003750834E7 2.0037508E7 + 512 + 512 + 1073741824 + 1073741824 + + + + MATCHING_OSMTILE + urn:ogc:def:crs:EPSG::3857 + + OSM:0 + 5.590822639508929E8 + -2.003750834E7 2.003750834E7 + 256 + 256 + 1 + 1 + + + OSM:1 + 2.7954113197544646E8 + -2.003750834E7 2.003750834E7 + 256 + 256 + 2 + 2 + + + OSM:2 + 1.3977056598772323E8 + -2.003750834E7 2.003750834E7 + 256 + 256 + 4 + 4 + + + OSM:3 + 6.988528299386162E7 + -2.003750834E7 2.003750834E7 + 256 + 256 + 8 + 8 + + + OSM:4 + 3.494264149693081E7 + -2.003750834E7 2.003750834E7 + 256 + 256 + 16 + 16 + + + OSM:5 + 1.7471320748465404E7 + -2.003750834E7 2.003750834E7 + 256 + 256 + 32 + 32 + + + OSM:6 + 8735660.374232702 + -2.003750834E7 2.003750834E7 + 256 + 256 + 64 + 64 + + + OSM:7 + 4367830.187116351 + -2.003750834E7 2.003750834E7 + 256 + 256 + 128 + 128 + + + OSM:8 + 2183915.0935581755 + -2.003750834E7 2.003750834E7 + 256 + 256 + 256 + 256 + + + OSM:9 + 1091957.5467790877 + -2.003750834E7 2.003750834E7 + 256 + 256 + 512 + 512 + + + OSM:10 + 545978.7733895439 + -2.003750834E7 2.003750834E7 + 256 + 256 + 1024 + 1024 + + + OSM:11 + 272989.38669477194 + -2.003750834E7 2.003750834E7 + 256 + 256 + 2048 + 2048 + + + OSM:12 + 136494.69334738597 + -2.003750834E7 2.003750834E7 + 256 + 256 + 4096 + 4096 + + + OSM:13 + 68247.34667369298 + -2.003750834E7 2.003750834E7 + 256 + 256 + 8192 + 8192 + + + OSM:14 + 34123.67333684649 + -2.003750834E7 2.003750834E7 + 256 + 256 + 16384 + 16384 + + + OSM:15 + 17061.836668423246 + -2.003750834E7 2.003750834E7 + 256 + 256 + 32768 + 32768 + + + OSM:16 + 8530.918334211623 + -2.003750834E7 2.003750834E7 + 256 + 256 + 65536 + 65536 + + + OSM:17 + 4265.4591671058115 + -2.003750834E7 2.003750834E7 + 256 + 256 + 131072 + 131072 + + + OSM:18 + 2132.7295835529058 + -2.003750834E7 2.003750834E7 + 256 + 256 + 262144 + 262144 + + + + WGS84 + urn:ogc:def:crs:EPSG::4326 + + 0 + 2.795411320143589E8 + -180.0 90.0 + 256 + 256 + 2 + 1 + + + 1 + 1.3977056600717944E8 + -180.0 90.0 + 256 + 256 + 4 + 2 + + + 2 + 6.988528300358972E7 + -180.0 90.0 + 256 + 256 + 8 + 4 + + + 3 + 3.494264150179486E7 + -180.0 90.0 + 256 + 256 + 16 + 8 + + + 4 + 1.747132075089743E7 + -180.0 90.0 + 256 + 256 + 32 + 16 + + + 5 + 8735660.375448715 + -180.0 90.0 + 256 + 256 + 64 + 32 + + + 6 + 4367830.1877243575 + -180.0 90.0 + 256 + 256 + 128 + 64 + + + 7 + 2183915.0938621787 + -180.0 90.0 + 256 + 256 + 256 + 128 + + + 8 + 1091957.5469310894 + -180.0 90.0 + 256 + 256 + 512 + 256 + + + 9 + 545978.7734655447 + -180.0 90.0 + 256 + 256 + 1024 + 512 + + + 10 + 272989.38673277234 + -180.0 90.0 + 256 + 256 + 2048 + 1024 + + + 11 + 136494.69336638617 + -180.0 90.0 + 256 + 256 + 4096 + 2048 + + + 12 + 68247.34668319309 + -180.0 90.0 + 256 + 256 + 8192 + 4096 + + + 13 + 34123.67334159654 + -180.0 90.0 + 256 + 256 + 16384 + 8192 + + + 14 + 17061.83667079827 + -180.0 90.0 + 256 + 256 + 32768 + 16384 + + + 15 + 8530.918335399136 + -180.0 90.0 + 256 + 256 + 65536 + 32768 + + + 16 + 4265.459167699568 + -180.0 90.0 + 256 + 256 + 131072 + 65536 + + + 17 + 2132.729583849784 + -180.0 90.0 + 256 + 256 + 262144 + 131072 + + + 18 + 1066.364791924892 + -180.0 90.0 + 256 + 256 + 524288 + 262144 + + + 19 + 533.182395962446 + -180.0 90.0 + 256 + 256 + 1048576 + 524288 + + + 20 + 266.591197981223 + -180.0 90.0 + 256 + 256 + 2097152 + 1048576 + + + 21 + 133.2955989906115 + -180.0 90.0 + 256 + 256 + 4194304 + 2097152 + + + + WebMercatorQuad + urn:ogc:def:crs:EPSG::3857 + + 0 + 5.59082264028717E8 + -2.003750834E7 2.0037508E7 + 256 + 256 + 1 + 1 + + + 1 + 2.795411320143585E8 + -2.003750834E7 2.0037508E7 + 256 + 256 + 2 + 2 + + + 2 + 1.3977056600717926E8 + -2.003750834E7 2.0037508E7 + 256 + 256 + 4 + 4 + + + 3 + 6.988528300358963E7 + -2.003750834E7 2.0037508E7 + 256 + 256 + 8 + 8 + + + 4 + 3.4942641501794815E7 + -2.003750834E7 2.0037508E7 + 256 + 256 + 16 + 16 + + + 5 + 1.7471320750897408E7 + -2.003750834E7 2.0037508E7 + 256 + 256 + 32 + 32 + + + 6 + 8735660.375448704 + -2.003750834E7 2.0037508E7 + 256 + 256 + 64 + 64 + + + 7 + 4367830.187724352 + -2.003750834E7 2.0037508E7 + 256 + 256 + 128 + 128 + + + 8 + 2183915.093862176 + -2.003750834E7 2.0037508E7 + 256 + 256 + 256 + 256 + + + 9 + 1091957.546931088 + -2.003750834E7 2.0037508E7 + 256 + 256 + 512 + 512 + + + 10 + 545978.773465544 + -2.003750834E7 2.0037508E7 + 256 + 256 + 1024 + 1024 + + + 11 + 272989.386732772 + -2.003750834E7 2.0037508E7 + 256 + 256 + 2048 + 2048 + + + 12 + 136494.693366386 + -2.003750834E7 2.0037508E7 + 256 + 256 + 4096 + 4096 + + + 13 + 68247.346683193 + -2.003750834E7 2.0037508E7 + 256 + 256 + 8192 + 8192 + + + 14 + 34123.6733415965 + -2.003750834E7 2.0037508E7 + 256 + 256 + 16384 + 16384 + + + 15 + 17061.83667079825 + -2.003750834E7 2.0037508E7 + 256 + 256 + 32768 + 32768 + + + 16 + 8530.918335399125 + -2.003750834E7 2.0037508E7 + 256 + 256 + 65536 + 65536 + + + 17 + 4265.459167699562 + -2.003750834E7 2.0037508E7 + 256 + 256 + 131072 + 131072 + + + 18 + 2132.729583849781 + -2.003750834E7 2.0037508E7 + 256 + 256 + 262144 + 262144 + + + 19 + 1066.3647919248906 + -2.003750834E7 2.0037508E7 + 256 + 256 + 524288 + 524288 + + + 20 + 533.1823959624453 + -2.003750834E7 2.0037508E7 + 256 + 256 + 1048576 + 1048576 + + + 21 + 266.59119798122265 + -2.003750834E7 2.0037508E7 + 256 + 256 + 2097152 + 2097152 + + + 22 + 133.29559899061132 + -2.003750834E7 2.0037508E7 + 256 + 256 + 4194304 + 4194304 + + + 23 + 66.64779949530566 + -2.003750834E7 2.0037508E7 + 256 + 256 + 8388608 + 8388608 + + + 24 + 33.32389974765283 + -2.003750834E7 2.0037508E7 + 256 + 256 + 16777216 + 16777216 + + + + WorldCRS84Quad + urn:ogc:def:crs:EPSG::4326 + + 1 + 2.795411320143589E8 + 90.0 -180.0 + 256 + 256 + 2 + 1 + + + 2 + 1.3977056600717944E8 + 90.0 -180.0 + 256 + 256 + 4 + 2 + + + 3 + 6.988528300358972E7 + 90.0 -180.0 + 256 + 256 + 8 + 4 + + + 4 + 3.494264150179486E7 + 90.0 -180.0 + 256 + 256 + 16 + 8 + + + 5 + 1.747132075089743E7 + 90.0 -180.0 + 256 + 256 + 32 + 16 + + + 6 + 8735660.375448715 + 90.0 -180.0 + 256 + 256 + 64 + 32 + + + 7 + 4367830.1877243575 + 90.0 -180.0 + 256 + 256 + 128 + 64 + + + 8 + 2183915.0938621787 + 90.0 -180.0 + 256 + 256 + 256 + 128 + + + 9 + 1091957.5469310894 + 90.0 -180.0 + 256 + 256 + 512 + 256 + + + 10 + 545978.7734655447 + 90.0 -180.0 + 256 + 256 + 1024 + 512 + + + 11 + 272989.38673277234 + 90.0 -180.0 + 256 + 256 + 2048 + 1024 + + + 12 + 136494.69336638617 + 90.0 -180.0 + 256 + 256 + 4096 + 2048 + + + 13 + 68247.34668319309 + 90.0 -180.0 + 256 + 256 + 8192 + 4096 + + + 14 + 34123.67334159654 + 90.0 -180.0 + 256 + 256 + 16384 + 8192 + + + 15 + 17061.83667079827 + 90.0 -180.0 + 256 + 256 + 32768 + 16384 + + + 16 + 8530.918335399136 + 90.0 -180.0 + 256 + 256 + 65536 + 32768 + + + 17 + 4265.459167699568 + 90.0 -180.0 + 256 + 256 + 131072 + 65536 + + + 18 + 2132.729583849784 + 90.0 -180.0 + 256 + 256 + 262144 + 131072 + + + + + + \ No newline at end of file