Coverage Report

Created: 2025-08-11 09:23

/src/gdal/ogr/ogrsf_frmts/pmtiles/ogrpmtilesdataset.cpp
Line
Count
Source (jump to first uncovered line)
1
/******************************************************************************
2
 *
3
 * Project:  OpenGIS Simple Features Reference Implementation
4
 * Purpose:  Implementation of PMTiles
5
 * Author:   Even Rouault <even.rouault at spatialys.com>
6
 *
7
 ******************************************************************************
8
 * Copyright (c) 2023, Planet Labs
9
 *
10
 * SPDX-License-Identifier: MIT
11
 ****************************************************************************/
12
13
#include "ogr_pmtiles.h"
14
15
#include "cpl_json.h"
16
17
#include "mvtutils.h"
18
19
#include <math.h>
20
21
/************************************************************************/
22
/*                       ~OGRPMTilesDataset()                           */
23
/************************************************************************/
24
25
OGRPMTilesDataset::~OGRPMTilesDataset()
26
525
{
27
525
    if (!m_osMetadataFilename.empty())
28
0
        VSIUnlink(m_osMetadataFilename.c_str());
29
525
}
30
31
/************************************************************************/
32
/*                              GetLayer()                              */
33
/************************************************************************/
34
35
OGRLayer *OGRPMTilesDataset::GetLayer(int iLayer)
36
37
0
{
38
0
    if (iLayer < 0 || iLayer >= GetLayerCount())
39
0
        return nullptr;
40
0
    return m_apoLayers[iLayer].get();
41
0
}
42
43
/************************************************************************/
44
/*                     LongLatToSphericalMercator()                     */
45
/************************************************************************/
46
47
static void LongLatToSphericalMercator(double *x, double *y)
48
0
{
49
0
    double X = SPHERICAL_RADIUS * (*x) / 180 * M_PI;
50
0
    double Y = SPHERICAL_RADIUS * log(tan(M_PI / 4 + 0.5 * (*y) / 180 * M_PI));
51
0
    *x = X;
52
0
    *y = Y;
53
0
}
54
55
/************************************************************************/
56
/*                            GetCompression()                          */
57
/************************************************************************/
58
59
/*static*/ const char *OGRPMTilesDataset::GetCompression(uint8_t nVal)
60
4
{
61
4
    switch (nVal)
62
4
    {
63
1
        case pmtiles::COMPRESSION_UNKNOWN:
64
1
            return "unknown";
65
0
        case pmtiles::COMPRESSION_NONE:
66
0
            return "none";
67
0
        case pmtiles::COMPRESSION_GZIP:
68
0
            return "gzip";
69
0
        case pmtiles::COMPRESSION_BROTLI:
70
0
            return "brotli";
71
0
        case pmtiles::COMPRESSION_ZSTD:
72
0
            return "zstd";
73
3
        default:
74
3
            break;
75
4
    }
76
3
    return CPLSPrintf("invalid (%d)", nVal);
77
4
}
78
79
/************************************************************************/
80
/*                           GetTileType()                              */
81
/************************************************************************/
82
83
/* static */
84
const char *OGRPMTilesDataset::GetTileType(const pmtiles::headerv3 &sHeader)
85
86
{
86
86
    switch (sHeader.tile_type)
87
86
    {
88
6
        case pmtiles::TILETYPE_UNKNOWN:
89
6
            return "unknown";
90
0
        case pmtiles::TILETYPE_PNG:
91
0
            return "PNG";
92
1
        case pmtiles::TILETYPE_JPEG:
93
1
            return "JPEG";
94
0
        case pmtiles::TILETYPE_WEBP:
95
0
            return "WEBP";
96
0
        case pmtiles::TILETYPE_MVT:
97
0
            return "MVT";
98
79
        default:
99
79
            break;
100
86
    }
101
79
    return CPLSPrintf("invalid (%d)", sHeader.tile_type);
102
86
}
103
104
/************************************************************************/
105
/*                                Open()                                */
106
/************************************************************************/
107
108
bool OGRPMTilesDataset::Open(GDALOpenInfo *poOpenInfo)
109
525
{
110
525
    if (!poOpenInfo->fpL || poOpenInfo->nHeaderBytes < 127)
111
433
        return false;
112
113
92
    SetDescription(poOpenInfo->pszFilename);
114
115
    // Borrow file handle
116
92
    m_poFile.reset(poOpenInfo->fpL);
117
92
    poOpenInfo->fpL = nullptr;
118
119
    // Deserizalize header
120
92
    std::string osHeader;
121
92
    osHeader.assign(reinterpret_cast<const char *>(poOpenInfo->pabyHeader),
122
92
                    127);
123
92
    try
124
92
    {
125
92
        m_sHeader = pmtiles::deserialize_header(osHeader);
126
92
    }
127
92
    catch (const std::exception &)
128
92
    {
129
0
        return false;
130
0
    }
131
132
    // Check tile type
133
92
    const bool bAcceptAnyTileType = CPLTestBool(CSLFetchNameValueDef(
134
92
        poOpenInfo->papszOpenOptions, "ACCEPT_ANY_TILE_TYPE", "NO"));
135
92
    if (bAcceptAnyTileType)
136
0
    {
137
        // do nothing. Internal use only by /vsipmtiles/
138
0
    }
139
92
    else if (m_sHeader.tile_type != pmtiles::TILETYPE_MVT)
140
86
    {
141
86
        CPLError(CE_Failure, CPLE_AppDefined,
142
86
                 "Tile type %s not handled by the driver",
143
86
                 GetTileType(m_sHeader));
144
86
        return false;
145
86
    }
146
147
    // Check compression method for metadata and directories
148
6
    CPLDebugOnly("PMTiles", "internal_compression = %s",
149
6
                 GetCompression(m_sHeader.internal_compression));
150
151
6
    if (m_sHeader.internal_compression == pmtiles::COMPRESSION_GZIP)
152
2
    {
153
2
        m_psInternalDecompressor = CPLGetDecompressor("gzip");
154
2
    }
155
4
    else if (m_sHeader.internal_compression == pmtiles::COMPRESSION_ZSTD)
156
0
    {
157
0
        m_psInternalDecompressor = CPLGetDecompressor("zstd");
158
0
        if (m_psInternalDecompressor == nullptr)
159
0
        {
160
0
            CPLError(CE_Failure, CPLE_AppDefined,
161
0
                     "File %s requires ZSTD decompression, but not available "
162
0
                     "in this GDAL build",
163
0
                     poOpenInfo->pszFilename);
164
0
            return false;
165
0
        }
166
0
    }
167
4
    else if (m_sHeader.internal_compression != pmtiles::COMPRESSION_NONE)
168
4
    {
169
4
        CPLError(CE_Failure, CPLE_AppDefined,
170
4
                 "Unhandled internal_compression = %s",
171
4
                 GetCompression(m_sHeader.internal_compression));
172
4
        return false;
173
4
    }
174
175
    // Check compression for tile data
176
2
    if (!CPLTestBool(CSLFetchNameValueDef(poOpenInfo->papszOpenOptions,
177
2
                                          "DECOMPRESS_TILES", "YES")))
178
0
    {
179
        // do nothing. Internal use only by /vsipmtiles/
180
0
    }
181
2
    else
182
2
    {
183
2
        CPLDebugOnly("PMTiles", "tile_compression = %s",
184
2
                     GetCompression(m_sHeader.tile_compression));
185
186
2
        if (m_sHeader.tile_compression == pmtiles::COMPRESSION_UNKNOWN)
187
2
        {
188
            // Python pmtiles-convert generates this. The MVT driver can autodetect
189
            // uncompressed and GZip-compressed tiles automatically.
190
2
        }
191
0
        else if (m_sHeader.tile_compression == pmtiles::COMPRESSION_GZIP)
192
0
        {
193
0
            m_psTileDataDecompressor = CPLGetDecompressor("gzip");
194
0
        }
195
0
        else if (m_sHeader.tile_compression == pmtiles::COMPRESSION_ZSTD)
196
0
        {
197
0
            m_psTileDataDecompressor = CPLGetDecompressor("zstd");
198
0
            if (m_psTileDataDecompressor == nullptr)
199
0
            {
200
0
                CPLError(
201
0
                    CE_Failure, CPLE_AppDefined,
202
0
                    "File %s requires ZSTD decompression, but not available "
203
0
                    "in this GDAL build",
204
0
                    poOpenInfo->pszFilename);
205
0
                return false;
206
0
            }
207
0
        }
208
0
        else if (m_sHeader.tile_compression != pmtiles::COMPRESSION_NONE)
209
0
        {
210
0
            CPLError(CE_Failure, CPLE_AppDefined,
211
0
                     "Unhandled tile_compression = %s",
212
0
                     GetCompression(m_sHeader.tile_compression));
213
0
            return false;
214
0
        }
215
2
    }
216
217
    // Read metadata
218
2
    const auto *posMetadata =
219
2
        ReadInternal(m_sHeader.json_metadata_offset,
220
2
                     m_sHeader.json_metadata_bytes, "metadata");
221
2
    if (!posMetadata)
222
2
        return false;
223
0
    CPLDebugOnly("PMTiles", "Metadata = %s", posMetadata->c_str());
224
0
    m_osMetadata = *posMetadata;
225
226
0
    m_osMetadataFilename =
227
0
        VSIMemGenerateHiddenFilename("pmtiles_metadata.json");
228
0
    VSIFCloseL(VSIFileFromMemBuffer(m_osMetadataFilename.c_str(),
229
0
                                    reinterpret_cast<GByte *>(&m_osMetadata[0]),
230
0
                                    m_osMetadata.size(), false));
231
232
0
    CPLJSONDocument oJsonDoc;
233
0
    if (!oJsonDoc.LoadMemory(m_osMetadata))
234
0
    {
235
0
        CPLError(CE_Failure, CPLE_AppDefined, "Cannot parse metadata");
236
0
        return false;
237
0
    }
238
239
0
    auto oJsonRoot = oJsonDoc.GetRoot();
240
0
    for (const auto &oChild : oJsonRoot.GetChildren())
241
0
    {
242
0
        if (oChild.GetType() == CPLJSONObject::Type::String)
243
0
        {
244
0
            if (oChild.GetName() == "json")
245
0
            {
246
                // Tippecanoe metadata includes a "json" item, which is a
247
                // serialized JSON object with vector_layers[] and layers[]
248
                // arrays we are interested in later.
249
                // so use "json" content as the new root
250
0
                if (!oJsonDoc.LoadMemory(oChild.ToString()))
251
0
                {
252
0
                    CPLError(CE_Failure, CPLE_AppDefined,
253
0
                             "Cannot parse 'json' metadata item");
254
0
                    return false;
255
0
                }
256
0
                oJsonRoot = oJsonDoc.GetRoot();
257
0
            }
258
            // Tippecanoe generates a "strategies" member with serialized JSON
259
0
            else if (oChild.GetName() != "strategies")
260
0
            {
261
0
                SetMetadataItem(oChild.GetName().c_str(),
262
0
                                oChild.ToString().c_str());
263
0
            }
264
0
        }
265
0
    }
266
267
0
    double dfMinX = m_sHeader.min_lon_e7 / 10e6;
268
0
    double dfMinY = m_sHeader.min_lat_e7 / 10e6;
269
0
    double dfMaxX = m_sHeader.max_lon_e7 / 10e6;
270
0
    double dfMaxY = m_sHeader.max_lat_e7 / 10e6;
271
0
    LongLatToSphericalMercator(&dfMinX, &dfMinY);
272
0
    LongLatToSphericalMercator(&dfMaxX, &dfMaxY);
273
274
0
    m_nMinZoomLevel = m_sHeader.min_zoom;
275
0
    m_nMaxZoomLevel = m_sHeader.max_zoom;
276
0
    if (m_nMinZoomLevel > m_nMaxZoomLevel)
277
0
    {
278
0
        CPLError(CE_Failure, CPLE_AppDefined, "min_zoom(=%d) > max_zoom(=%d)",
279
0
                 m_nMinZoomLevel, m_nMaxZoomLevel);
280
0
        return false;
281
0
    }
282
0
    if (m_nMinZoomLevel > 30)
283
0
    {
284
0
        CPLError(CE_Warning, CPLE_AppDefined, "Clamping min_zoom from %d to %d",
285
0
                 m_nMinZoomLevel, 30);
286
0
        m_nMinZoomLevel = 30;
287
0
    }
288
0
    if (m_nMaxZoomLevel > 30)
289
0
    {
290
0
        CPLError(CE_Warning, CPLE_AppDefined, "Clamping max_zoom from %d to %d",
291
0
                 m_nMaxZoomLevel, 30);
292
0
        m_nMaxZoomLevel = 30;
293
0
    }
294
295
0
    if (bAcceptAnyTileType)
296
0
        return true;
297
298
    // If using the pmtiles go utility, vector_layers and tilestats are
299
    // moved from Tippecanoe's json metadata item to the root element.
300
0
    CPLJSONArray oVectorLayers = oJsonRoot.GetArray("vector_layers");
301
0
    if (oVectorLayers.Size() == 0)
302
0
    {
303
0
        CPLError(CE_Failure, CPLE_AppDefined,
304
0
                 "Missing vector_layers[] metadata");
305
0
        return false;
306
0
    }
307
308
0
    CPLJSONArray oTileStatLayers = oJsonRoot.GetArray("tilestats/layers");
309
310
0
    const int nZoomLevel =
311
0
        atoi(CSLFetchNameValueDef(poOpenInfo->papszOpenOptions, "ZOOM_LEVEL",
312
0
                                  CPLSPrintf("%d", m_nMaxZoomLevel)));
313
0
    if (nZoomLevel < m_nMinZoomLevel || nZoomLevel > m_nMaxZoomLevel)
314
0
    {
315
0
        CPLError(CE_Failure, CPLE_AppDefined,
316
0
                 "Invalid zoom level. Should be in [%d,%d] range",
317
0
                 m_nMinZoomLevel, m_nMaxZoomLevel);
318
0
        return false;
319
0
    }
320
0
    SetMetadataItem("ZOOM_LEVEL", CPLSPrintf("%d", nZoomLevel));
321
322
0
    m_osClipOpenOption =
323
0
        CSLFetchNameValueDef(poOpenInfo->papszOpenOptions, "CLIP", "");
324
325
0
    const bool bZoomLevelFromSpatialFilter = CPLFetchBool(
326
0
        poOpenInfo->papszOpenOptions, "ZOOM_LEVEL_AUTO",
327
0
        CPLTestBool(CPLGetConfigOption("MVT_ZOOM_LEVEL_AUTO", "NO")));
328
0
    const bool bJsonField =
329
0
        CPLFetchBool(poOpenInfo->papszOpenOptions, "JSON_FIELD", false);
330
331
0
    for (int i = 0; i < oVectorLayers.Size(); i++)
332
0
    {
333
0
        CPLJSONObject oId = oVectorLayers[i].GetObj("id");
334
0
        if (oId.IsValid() && oId.GetType() == CPLJSONObject::Type::String)
335
0
        {
336
0
            OGRwkbGeometryType eGeomType = wkbUnknown;
337
0
            if (oTileStatLayers.IsValid())
338
0
            {
339
0
                eGeomType = OGRMVTFindGeomTypeFromTileStat(
340
0
                    oTileStatLayers, oId.ToString().c_str());
341
0
            }
342
0
            if (eGeomType == wkbUnknown)
343
0
            {
344
0
                eGeomType = OGRPMTilesVectorLayer::GuessGeometryType(
345
0
                    this, oId.ToString().c_str(), nZoomLevel);
346
0
            }
347
348
0
            CPLJSONObject oFields = oVectorLayers[i].GetObj("fields");
349
0
            CPLJSONArray oAttributesFromTileStats =
350
0
                OGRMVTFindAttributesFromTileStat(oTileStatLayers,
351
0
                                                 oId.ToString().c_str());
352
353
0
            m_apoLayers.push_back(std::make_unique<OGRPMTilesVectorLayer>(
354
0
                this, oId.ToString().c_str(), oFields, oAttributesFromTileStats,
355
0
                bJsonField, dfMinX, dfMinY, dfMaxX, dfMaxY, eGeomType,
356
0
                nZoomLevel, bZoomLevelFromSpatialFilter));
357
0
        }
358
0
    }
359
360
0
    return true;
361
0
}
362
363
/************************************************************************/
364
/*                              Read()                                  */
365
/************************************************************************/
366
367
const std::string *OGRPMTilesDataset::Read(const CPLCompressor *psDecompressor,
368
                                           uint64_t nOffset, uint64_t nSize,
369
                                           const char *pszDataType)
370
2
{
371
2
    if (nSize > 10 * 1024 * 1024)
372
1
    {
373
1
        CPLError(CE_Failure, CPLE_AppDefined,
374
1
                 "Too large amount of %s to read: " CPL_FRMT_GUIB
375
1
                 " bytes at offset " CPL_FRMT_GUIB,
376
1
                 pszDataType, static_cast<GUIntBig>(nSize),
377
1
                 static_cast<GUIntBig>(nOffset));
378
1
        return nullptr;
379
1
    }
380
1
    m_osBuffer.resize(static_cast<size_t>(nSize));
381
1
    m_poFile->Seek(nOffset, SEEK_SET);
382
1
    if (m_poFile->Read(&m_osBuffer[0], m_osBuffer.size(), 1) != 1)
383
0
    {
384
0
        CPLError(CE_Failure, CPLE_AppDefined,
385
0
                 "Cannot read %s of length %u at offset " CPL_FRMT_GUIB,
386
0
                 pszDataType, unsigned(nSize), static_cast<GUIntBig>(nOffset));
387
0
        return nullptr;
388
0
    }
389
390
1
    if (psDecompressor)
391
1
    {
392
1
        m_osDecompressedBuffer.resize(32 + 16 * m_osBuffer.size());
393
1
        for (int iTry = 0; iTry < 2; ++iTry)
394
1
        {
395
1
            void *pOutputData = &m_osDecompressedBuffer[0];
396
1
            size_t nOutputSize = m_osDecompressedBuffer.size();
397
1
            if (!psDecompressor->pfnFunc(m_osBuffer.data(), m_osBuffer.size(),
398
1
                                         &pOutputData, &nOutputSize, nullptr,
399
1
                                         psDecompressor->user_data))
400
1
            {
401
1
                if (iTry == 0)
402
1
                {
403
1
                    pOutputData = nullptr;
404
1
                    nOutputSize = 0;
405
1
                    if (psDecompressor->pfnFunc(
406
1
                            m_osBuffer.data(), m_osBuffer.size(), &pOutputData,
407
1
                            &nOutputSize, nullptr, psDecompressor->user_data))
408
0
                    {
409
0
                        CPLDebug("PMTiles",
410
0
                                 "Buffer of size %u uncompresses to %u bytes",
411
0
                                 unsigned(nSize), unsigned(nOutputSize));
412
0
                        m_osDecompressedBuffer.resize(nOutputSize);
413
0
                        continue;
414
0
                    }
415
1
                }
416
417
1
                CPLError(CE_Failure, CPLE_AppDefined,
418
1
                         "Cannot decompress %s of length %u at "
419
1
                         "offset " CPL_FRMT_GUIB,
420
1
                         pszDataType, unsigned(nSize),
421
1
                         static_cast<GUIntBig>(nOffset));
422
1
                return nullptr;
423
1
            }
424
0
            m_osDecompressedBuffer.resize(nOutputSize);
425
0
            break;
426
1
        }
427
0
        return &m_osDecompressedBuffer;
428
1
    }
429
0
    else
430
0
    {
431
0
        return &m_osBuffer;
432
0
    }
433
1
}
434
435
/************************************************************************/
436
/*                              ReadInternal()                          */
437
/************************************************************************/
438
439
const std::string *OGRPMTilesDataset::ReadInternal(uint64_t nOffset,
440
                                                   uint64_t nSize,
441
                                                   const char *pszDataType)
442
2
{
443
2
    return Read(m_psInternalDecompressor, nOffset, nSize, pszDataType);
444
2
}
445
446
/************************************************************************/
447
/*                              ReadTileData()                          */
448
/************************************************************************/
449
450
const std::string *OGRPMTilesDataset::ReadTileData(uint64_t nOffset,
451
                                                   uint64_t nSize)
452
0
{
453
0
    return Read(m_psTileDataDecompressor, nOffset, nSize, "tile data");
454
0
}