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