Skip to content

Commit b99ee9b

Browse files
committed
MVT: allow generating tilesets with more than 1 tile at zoom level 0
Fixes #11749
1 parent 73eca73 commit b99ee9b

File tree

3 files changed

+170
-29
lines changed

3 files changed

+170
-29
lines changed

autotest/ogr/ogr_mvt.py

+43-1
Original file line numberDiff line numberDiff line change
@@ -1753,4 +1753,46 @@ def test_ogr_mvt_write_reuse_temp_db():
17531753

17541754

17551755
###############################################################################
1756-
#
1756+
1757+
1758+
@pytest.mark.require_driver("SQLite")
1759+
@pytest.mark.require_geos
1760+
@pytest.mark.parametrize(
1761+
"TILING_SCHEME",
1762+
["EPSG:4326,-180,90,180", "EPSG:4326,-180,90,180,2,1", "EPSG:4326,-180,90,180,1,1"],
1763+
)
1764+
def test_ogr_mvt_write_custom_tiling_scheme_WorldCRS84Quad(tmp_vsimem, TILING_SCHEME):
1765+
1766+
src_ds = gdal.GetDriverByName("Memory").Create("", 0, 0, 0, gdal.GDT_Unknown)
1767+
srs = osr.SpatialReference()
1768+
srs.SetFromUserInput("WGS84")
1769+
lyr = src_ds.CreateLayer("mylayer", srs=srs)
1770+
1771+
f = ogr.Feature(lyr.GetLayerDefn())
1772+
f.SetGeometry(ogr.CreateGeometryFromWkt("POINT(120 40)"))
1773+
lyr.CreateFeature(f)
1774+
1775+
filename = str(tmp_vsimem / "out")
1776+
out_ds = gdal.VectorTranslate(
1777+
filename,
1778+
src_ds,
1779+
format="MVT",
1780+
datasetCreationOptions=["TILING_SCHEME=" + TILING_SCHEME],
1781+
)
1782+
assert out_ds is not None
1783+
out_ds = None
1784+
1785+
if TILING_SCHEME == "EPSG:4326,-180,90,180,1,1":
1786+
# If explictly setting tile_matrix_width_zoom_0 == 1,
1787+
# we have no tiles beyond longitude 0 degree
1788+
with pytest.raises(Exception):
1789+
ogr.Open(filename + "/0")
1790+
else:
1791+
out_ds = ogr.Open(filename + "/0")
1792+
assert out_ds is not None
1793+
out_lyr = out_ds.GetLayerByName("mylayer")
1794+
assert out_lyr.GetSpatialRef().ExportToWkt().find("4326") >= 0
1795+
out_f = out_lyr.GetNextFeature()
1796+
ogrtest.check_feature_geometry(
1797+
out_f, "MULTIPOINT ((120.0146484375 39.990234375))"
1798+
)

doc/source/drivers/vector/mvt.rst

+21-2
Original file line numberDiff line numberDiff line change
@@ -144,7 +144,8 @@ scanning the features of the tile(s).
144144

145145
As an extension, OGR handles in reading and writing custom tiling
146146
schemes by using the *crs*, *tile_origin_upper_left_x*,
147-
*tile_origin_upper_left_y* and *tile_dimension_zoom_0* metadata items.
147+
*tile_origin_upper_left_y*, *tile_dimension_zoom_0*, *tile_matrix_width_zoom_0*
148+
and *tile_matrix_height_zoom_0* metadata items.
148149
For example, for the Finnish ETRS-TM35FIN (EPSG:3067) tiling scheme:
149150

150151
.. code-block:: json
@@ -157,6 +158,21 @@ For example, for the Finnish ETRS-TM35FIN (EPSG:3067) tiling scheme:
157158
"tile_dimension_zoom_0":2097152.0,
158159
}
159160
161+
Or for a ``WorldCRS84Quad`` tiling scheme with 2 tiles in the horizontal
162+
direction at zoom level 0:
163+
164+
.. code-block:: json
165+
166+
{
167+
"...": "...",
168+
"crs":"EPSG:4326",
169+
"tile_origin_upper_left_x":-180.0,
170+
"tile_origin_upper_left_y":90.0,
171+
"tile_dimension_zoom_0":180.0,
172+
"tile_matrix_width_zoom_0":2,
173+
"tile_matrix_height_zoom_0":1
174+
}
175+
160176
Opening options
161177
---------------
162178

@@ -351,7 +367,7 @@ The following dataset creation options are supported:
351367
metadata item, which is the center of :co:`BOUNDS` at minimum zoom level.
352368

353369
- .. co:: TILING_SCHEME
354-
:choices: <crs\,tile_origin_upper_left_x\,tile_origin_upper_left_y\,tile_dimension_zoom_0>
370+
:choices: <crs\,tile_origin_upper_left_x\,tile_origin_upper_left_y\,tile_dimension_zoom_0[\,tile_matrix_width_zoom_0\,tile_matrix_height_zoom_0]>
355371

356372
Define a custom tiling scheme with a CRS
357373
(typically given as EPSG:XXXX), the coordinates of the upper-left
@@ -365,6 +381,9 @@ The following dataset creation options are supported:
365381
scheme, the 'crs', 'tile_origin_upper_left_x',
366382
'tile_origin_upper_left_y' and 'tile_dimension_zoom_0' entries are
367383
added to the metadata.json, and are honoured by the OGR MVT reader.
384+
Starting with GDAL 3.10.2, 'tile_matrix_width_zoom_0' (resp.
385+
'tile_matrix_height_zoom_0') can be specified to indicate the number of
386+
tiles along the X (resp. Y) axis at zoom level 0.
368387

369388
Layer configuration
370389
-------------------

ogr/ogrsf_frmts/mvt/ogrmvtdataset.cpp

+106-26
Original file line numberDiff line numberDiff line change
@@ -292,9 +292,14 @@ class OGRMVTDataset final : public GDALDataset
292292
bool m_bClip = true;
293293
CPLString m_osTileExtension{"pbf"};
294294
OGRSpatialReference *m_poSRS = nullptr;
295-
double m_dfTileDim0 = 0.0;
296-
double m_dfTopXOrigin = 0.0;
297-
double m_dfTopYOrigin = 0.0;
295+
double m_dfTileDim0 =
296+
0.0; // Extent (in CRS units) of a tile at zoom level 0
297+
double m_dfTopXOrigin = 0.0; // top-left X of tile matrix scheme
298+
double m_dfTopYOrigin = 0.0; // top-left Y of tile matrix scheme
299+
int m_nTileMatrixWidth0 =
300+
1; // Number of tiles along X axis at zoom level 0
301+
int m_nTileMatrixHeight0 =
302+
1; // Number of tiles along Y axis at zoom level 0
298303

299304
static GDALDataset *OpenDirectory(GDALOpenInfo *);
300305

@@ -322,20 +327,30 @@ class OGRMVTDataset final : public GDALDataset
322327
return m_poSRS;
323328
}
324329

325-
double GetTileDim0() const
330+
inline double GetTileDim0() const
326331
{
327332
return m_dfTileDim0;
328333
}
329334

330-
double GetTopXOrigin() const
335+
inline double GetTopXOrigin() const
331336
{
332337
return m_dfTopXOrigin;
333338
}
334339

335-
double GetTopYOrigin() const
340+
inline double GetTopYOrigin() const
336341
{
337342
return m_dfTopYOrigin;
338343
}
344+
345+
inline int GetTileMatrixWidth0() const
346+
{
347+
return m_nTileMatrixWidth0;
348+
}
349+
350+
inline int GetTileMatrixHeight0() const
351+
{
352+
return m_nTileMatrixHeight0;
353+
}
339354
};
340355

341356
/************************************************************************/
@@ -1747,8 +1762,10 @@ void OGRMVTDirectoryLayer::SetSpatialFilter(OGRGeometry *poGeomIn)
17471762

17481763
if (sEnvelope.IsInit() && sEnvelope.MinX >= -10 * m_poDS->GetTileDim0() &&
17491764
sEnvelope.MinY >= -10 * m_poDS->GetTileDim0() &&
1750-
sEnvelope.MaxX <= 10 * m_poDS->GetTileDim0() &&
1751-
sEnvelope.MaxY <= 10 * m_poDS->GetTileDim0())
1765+
sEnvelope.MaxX <=
1766+
10 * m_poDS->GetTileDim0() * m_poDS->GetTileMatrixWidth0() &&
1767+
sEnvelope.MaxY <=
1768+
10 * m_poDS->GetTileDim0() * m_poDS->GetTileMatrixHeight0())
17521769
{
17531770
const double dfTileDim = m_poDS->GetTileDim0() / (1 << m_nZ);
17541771
m_nFilterMinX = std::max(
@@ -1760,18 +1777,30 @@ void OGRMVTDirectoryLayer::SetSpatialFilter(OGRGeometry *poGeomIn)
17601777
m_nFilterMaxX = std::min(
17611778
static_cast<int>(
17621779
ceil((sEnvelope.MaxX - m_poDS->GetTopXOrigin()) / dfTileDim)),
1763-
(1 << m_nZ) - 1);
1780+
static_cast<int>(std::min<int64_t>(
1781+
INT_MAX, (static_cast<int64_t>(1) << m_nZ) *
1782+
m_poDS->GetTileMatrixWidth0() -
1783+
1)));
17641784
m_nFilterMaxY = std::min(
17651785
static_cast<int>(
17661786
ceil((m_poDS->GetTopYOrigin() - sEnvelope.MinY) / dfTileDim)),
1767-
(1 << m_nZ) - 1);
1787+
static_cast<int>(std::min<int64_t>(
1788+
INT_MAX, (static_cast<int64_t>(1) << m_nZ) *
1789+
m_poDS->GetTileMatrixHeight0() -
1790+
1)));
17681791
}
17691792
else
17701793
{
17711794
m_nFilterMinX = 0;
17721795
m_nFilterMinY = 0;
1773-
m_nFilterMaxX = (1 << m_nZ) - 1;
1774-
m_nFilterMaxY = (1 << m_nZ) - 1;
1796+
m_nFilterMaxX = static_cast<int>(
1797+
std::min<int64_t>(INT_MAX, (static_cast<int64_t>(1) << m_nZ) *
1798+
m_poDS->GetTileMatrixWidth0() -
1799+
1));
1800+
m_nFilterMaxY = static_cast<int>(
1801+
std::min<int64_t>(INT_MAX, (static_cast<int64_t>(1) << m_nZ) *
1802+
m_poDS->GetTileMatrixHeight0() -
1803+
1));
17751804
}
17761805
}
17771806

@@ -2432,6 +2461,7 @@ static bool LoadMetadata(const CPLString &osMetadataFile,
24322461
CPLJSONArray &oTileStatLayers, CPLJSONObject &oBounds,
24332462
OGRSpatialReference *poSRS, double &dfTopX,
24342463
double &dfTopY, double &dfTileDim0,
2464+
int &nTileMatrixWidth0, int &nTileMatrixHeight0,
24352465
const CPLString &osMetadataMemFilename)
24362466

24372467
{
@@ -2454,17 +2484,36 @@ static bool LoadMetadata(const CPLString &osMetadataFile,
24542484
if (!bLoadOK)
24552485
return false;
24562486

2457-
CPLJSONObject oCrs(oDoc.GetRoot().GetObj("crs"));
2458-
CPLJSONObject oTopX(oDoc.GetRoot().GetObj("tile_origin_upper_left_x"));
2459-
CPLJSONObject oTopY(oDoc.GetRoot().GetObj("tile_origin_upper_left_y"));
2460-
CPLJSONObject oTileDim0(oDoc.GetRoot().GetObj("tile_dimension_zoom_0"));
2487+
const CPLJSONObject oCrs(oDoc.GetRoot().GetObj("crs"));
2488+
const CPLJSONObject oTopX(
2489+
oDoc.GetRoot().GetObj("tile_origin_upper_left_x"));
2490+
const CPLJSONObject oTopY(
2491+
oDoc.GetRoot().GetObj("tile_origin_upper_left_y"));
2492+
const CPLJSONObject oTileDim0(
2493+
oDoc.GetRoot().GetObj("tile_dimension_zoom_0"));
2494+
nTileMatrixWidth0 = 1;
2495+
nTileMatrixHeight0 = 1;
24612496
if (oCrs.IsValid() && oTopX.IsValid() && oTopY.IsValid() &&
24622497
oTileDim0.IsValid())
24632498
{
24642499
poSRS->SetFromUserInput(oCrs.ToString().c_str());
24652500
dfTopX = oTopX.ToDouble();
24662501
dfTopY = oTopY.ToDouble();
24672502
dfTileDim0 = oTileDim0.ToDouble();
2503+
const CPLJSONObject oTMWidth0(
2504+
oDoc.GetRoot().GetObj("tile_matrix_width_zoom_0"));
2505+
if (oTMWidth0.GetType() == CPLJSONObject::Type::Integer)
2506+
nTileMatrixWidth0 = std::max(1, oTMWidth0.ToInteger());
2507+
2508+
const CPLJSONObject oTMHeight0(
2509+
oDoc.GetRoot().GetObj("tile_matrix_height_zoom_0"));
2510+
if (oTMHeight0.GetType() == CPLJSONObject::Type::Integer)
2511+
nTileMatrixHeight0 = std::max(1, oTMHeight0.ToInteger());
2512+
2513+
// Assumes WorldCRS84Quad with 2 tiles in width
2514+
// cf https://github.com/OSGeo/gdal/issues/11749
2515+
if (!oTMWidth0.IsValid() && dfTopX == -180 && dfTileDim0 == 180)
2516+
nTileMatrixWidth0 = 2;
24682517
}
24692518

24702519
oVectorLayers.Deinit();
@@ -2803,7 +2852,8 @@ GDALDataset *OGRMVTDataset::OpenDirectory(GDALOpenInfo *poOpenInfo)
28032852
if (!LoadMetadata(osMetadataFile, osMetadataContent, oVectorLayers,
28042853
oTileStatLayers, oBounds, poDS->m_poSRS,
28052854
poDS->m_dfTopXOrigin, poDS->m_dfTopYOrigin,
2806-
poDS->m_dfTileDim0, osMetadataMemFilename))
2855+
poDS->m_dfTileDim0, poDS->m_nTileMatrixWidth0,
2856+
poDS->m_nTileMatrixHeight0, osMetadataMemFilename))
28072857
{
28082858
delete poDS;
28092859
return nullptr;
@@ -3137,7 +3187,8 @@ GDALDataset *OGRMVTDataset::Open(GDALOpenInfo *poOpenInfo, bool bRecurseAllowed)
31373187
LoadMetadata(osMetadataFile, CPLString(), oVectorLayers,
31383188
oTileStatLayers, oBounds, poDS->m_poSRS,
31393189
poDS->m_dfTopXOrigin, poDS->m_dfTopYOrigin,
3140-
poDS->m_dfTileDim0, CPLString());
3190+
poDS->m_dfTileDim0, poDS->m_nTileMatrixWidth0,
3191+
poDS->m_nTileMatrixHeight0, CPLString());
31413192
}
31423193

31433194
const char *pszGeorefTopX =
@@ -3166,8 +3217,10 @@ GDALDataset *OGRMVTDataset::Open(GDALOpenInfo *poOpenInfo, bool bRecurseAllowed)
31663217
int nX = atoi(osX);
31673218
int nY = atoi(osY);
31683219
int nZ = atoi(osZ);
3169-
if (nZ >= 0 && nZ < 30 && nX >= 0 && nX < (1 << nZ) && nY >= 0 &&
3170-
nY < (1 << nZ))
3220+
if (nZ >= 0 && nZ < 30 && nX >= 0 &&
3221+
nX < (static_cast<int64_t>(1) << nZ) * poDS->m_nTileMatrixWidth0 &&
3222+
nY >= 0 &&
3223+
nY < (static_cast<int64_t>(1) << nZ) * poDS->m_nTileMatrixHeight0)
31713224
{
31723225
poDS->m_bGeoreferenced = true;
31733226
poDS->m_dfTileDimX = poDS->m_dfTileDim0 / (1 << nZ);
@@ -3336,6 +3389,10 @@ class OGRMVTWriterDataset final : public GDALDataset
33363389
double m_dfTopX = 0.0;
33373390
double m_dfTopY = 0.0;
33383391
double m_dfTileDim0 = 0.0;
3392+
int m_nTileMatrixWidth0 =
3393+
1; // Number of tiles along X axis at zoom level 0
3394+
int m_nTileMatrixHeight0 =
3395+
1; // Number of tiles along Y axis at zoom level 0
33393396
bool m_bReuseTempFile = false; // debug only
33403397

33413398
OGRErr PreGenerateForTile(
@@ -5528,6 +5585,10 @@ bool OGRMVTWriterDataset::GenerateMetadata(
55285585
oRoot);
55295586
WriteMetadataItem("tile_dimension_zoom_0", m_dfTileDim0, m_hDBMBTILES,
55305587
oRoot);
5588+
WriteMetadataItem("tile_matrix_width_zoom_0", m_nTileMatrixWidth0,
5589+
m_hDBMBTILES, oRoot);
5590+
WriteMetadataItem("tile_matrix_height_zoom_0", m_nTileMatrixHeight0,
5591+
m_hDBMBTILES, oRoot);
55315592
}
55325593

55335594
CPLJSONDocument oJsonDoc;
@@ -5830,11 +5891,17 @@ OGRErr OGRMVTWriterDataset::WriteFeature(OGRMVTWriterLayer *poLayer,
58305891
const int nTileMaxX =
58315892
std::min(static_cast<int>((sExtent.MaxX - m_dfTopX + dfBuffer) /
58325893
dfTileDim),
5833-
(1 << nZ) - 1);
5894+
static_cast<int>(std::min<int64_t>(
5895+
INT_MAX, (static_cast<int64_t>(1) << nZ) *
5896+
m_nTileMatrixWidth0 -
5897+
1)));
58345898
const int nTileMaxY =
58355899
std::min(static_cast<int>((m_dfTopY - sExtent.MinY + dfBuffer) /
58365900
dfTileDim),
5837-
(1 << nZ) - 1);
5901+
static_cast<int>(std::min<int64_t>(
5902+
INT_MAX, (static_cast<int64_t>(1) << nZ) *
5903+
m_nTileMatrixHeight0 -
5904+
1)));
58385905
for (int iX = nTileMinX; iX <= nTileMaxX; iX++)
58395906
{
58405907
for (int iY = nTileMinY; iY <= nTileMaxY; iY++)
@@ -6181,20 +6248,32 @@ GDALDataset *OGRMVTWriterDataset::Create(const char *pszFilename, int nXSize,
61816248
return nullptr;
61826249
}
61836250

6184-
CPLStringList aoList(CSLTokenizeString2(pszTilingScheme, ",", 0));
6185-
if (aoList.Count() == 4)
6251+
const CPLStringList aoList(CSLTokenizeString2(pszTilingScheme, ",", 0));
6252+
if (aoList.Count() >= 4)
61866253
{
61876254
poDS->m_poSRS->SetFromUserInput(aoList[0]);
61886255
poDS->m_dfTopX = CPLAtof(aoList[1]);
61896256
poDS->m_dfTopY = CPLAtof(aoList[2]);
61906257
poDS->m_dfTileDim0 = CPLAtof(aoList[3]);
6258+
if (aoList.Count() == 6)
6259+
{
6260+
poDS->m_nTileMatrixWidth0 = std::max(1, atoi(aoList[4]));
6261+
poDS->m_nTileMatrixHeight0 = std::max(1, atoi(aoList[5]));
6262+
}
6263+
else if (poDS->m_dfTopX == -180 && poDS->m_dfTileDim0 == 180)
6264+
{
6265+
// Assumes WorldCRS84Quad with 2 tiles in width
6266+
// cf https://github.com/OSGeo/gdal/issues/11749
6267+
poDS->m_nTileMatrixWidth0 = 2;
6268+
}
61916269
}
61926270
else
61936271
{
61946272
CPLError(CE_Failure, CPLE_AppDefined,
61956273
"Wrong format for TILING_SCHEME. "
61966274
"Expecting EPSG:XXXX,tile_origin_upper_left_x,"
6197-
"tile_origin_upper_left_y,tile_dimension_zoom_0");
6275+
"tile_origin_upper_left_y,tile_dimension_zoom_0[,tile_"
6276+
"matrix_width_zoom_0,tile_matrix_height_zoom_0]");
61986277
delete poDS;
61996278
return nullptr;
62006279
}
@@ -6347,7 +6426,8 @@ void RegisterOGRMVT()
63476426
" <Option name='TILING_SCHEME' type='string' "
63486427
"description='Custom tiling scheme with following format "
63496428
"\"EPSG:XXXX,tile_origin_upper_left_x,tile_origin_upper_left_y,"
6350-
"tile_dimension_zoom_0\"'/>"
6429+
"tile_dimension_zoom_0[,tile_matrix_width_zoom_0,tile_matrix_height_"
6430+
"zoom_0]\"'/>"
63516431
"</CreationOptionList>");
63526432
#endif // HAVE_MVT_WRITE_SUPPORT
63536433

0 commit comments

Comments
 (0)