Coverage Report

Created: 2026-08-11 08:26

next uncovered line (L), next uncovered region (R), next uncovered branch (B)
/src/gdal/ogr/ogrsf_frmts/pmtiles/ogrpmtilesfrommbtiles.cpp
Line
Count
Source
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 "cpl_json.h"
14
15
#include "ogrsf_frmts.h"
16
#include "ogr_pmtiles.h"
17
#include "ogrpmtilesfrommbtiles.h"
18
19
#include "include_pmtiles.h"
20
21
#include "cpl_compressor.h"
22
#include "cpl_md5.h"
23
#include "cpl_string.h"
24
#include "cpl_vsi_virtual.h"
25
26
#include <algorithm>
27
#include <array>
28
#include <cassert>
29
#include <unordered_map>
30
#include <unordered_set>
31
#include <utility>
32
33
/************************************************************************/
34
/*                          ProcessMetadata()                           */
35
/************************************************************************/
36
37
static bool ProcessMetadata(GDALDataset *poSQLiteDS, pmtiles::headerv3 &sHeader,
38
                            std::string &osMetadata)
39
0
{
40
41
0
    auto poMetadata = poSQLiteDS->GetLayerByName("metadata");
42
0
    if (!poMetadata)
43
0
    {
44
0
        CPLError(CE_Failure, CPLE_AppDefined, "metadata table not found");
45
0
        return false;
46
0
    }
47
48
0
    const int iName = poMetadata->GetLayerDefn()->GetFieldIndex("name");
49
0
    const int iValue = poMetadata->GetLayerDefn()->GetFieldIndex("value");
50
0
    if (iName < 0 || iValue < 0)
51
0
    {
52
0
        CPLError(CE_Failure, CPLE_AppDefined,
53
0
                 "Bad structure for metadata table");
54
0
        return false;
55
0
    }
56
57
0
    CPLJSONObject oObj;
58
0
    CPLJSONDocument oJsonDoc;
59
0
    for (auto &&poFeature : poMetadata)
60
0
    {
61
0
        const char *pszName = poFeature->GetFieldAsString(iName);
62
0
        const char *pszValue = poFeature->GetFieldAsString(iValue);
63
0
        if (EQUAL(pszName, "json"))
64
0
        {
65
0
            if (!oJsonDoc.LoadMemory(pszValue))
66
0
            {
67
0
                CPLError(CE_Failure, CPLE_AppDefined,
68
0
                         "Cannot parse 'json' metadata item");
69
0
                return false;
70
0
            }
71
0
            for (const auto &oChild : oJsonDoc.GetRoot().GetChildren())
72
0
            {
73
0
                oObj.Add(oChild.GetName(), oChild);
74
0
            }
75
0
        }
76
0
        else
77
0
        {
78
0
            oObj.Add(pszName, pszValue);
79
0
        }
80
0
    }
81
82
    // MBTiles advertises scheme=tms. Override this
83
0
    oObj.Set("scheme", "xyz");
84
85
0
    const auto osFormat = oObj.GetString("format", "{missing}");
86
0
    uint8_t tile_type = pmtiles::TILETYPE_UNKNOWN;
87
0
    if (osFormat == "pbf")
88
0
        tile_type = pmtiles::TILETYPE_MVT;
89
0
    else if (osFormat == "png" || osFormat == "image/png")
90
0
        tile_type = pmtiles::TILETYPE_PNG;
91
0
    else if (osFormat == "jpg" || osFormat == "jpeg" ||
92
0
             osFormat == "image/jpeg")
93
0
        tile_type = pmtiles::TILETYPE_JPEG;
94
0
    else if (osFormat == "webp" || osFormat == "image/webp")
95
0
        tile_type = pmtiles::TILETYPE_WEBP;
96
0
    else if (osFormat == "avif" || osFormat == "image/avif")
97
0
        tile_type = pmtiles::TILETYPE_AVIF;
98
0
    else
99
0
    {
100
0
        CPLError(CE_Failure, CPLE_AppDefined, "format=%s unhandled",
101
0
                 osFormat.c_str());
102
0
        return false;
103
0
    }
104
105
0
    int nMinZoom = atoi(oObj.GetString("minzoom", "-1").c_str());
106
0
    if (nMinZoom < 0 || nMinZoom > 255)
107
0
    {
108
0
        CPLError(CE_Failure, CPLE_AppDefined, "Missing or invalid minzoom");
109
0
        return false;
110
0
    }
111
112
0
    int nMaxZoom = atoi(oObj.GetString("maxzoom", "-1").c_str());
113
0
    if (nMaxZoom < 0 || nMaxZoom > 255)
114
0
    {
115
0
        CPLError(CE_Failure, CPLE_AppDefined, "Missing or invalid maxzoom");
116
0
        return false;
117
0
    }
118
119
0
    const CPLStringList aosBounds(
120
0
        CSLTokenizeString2(oObj.GetString("bounds").c_str(), ",", 0));
121
0
    if (aosBounds.size() != 4)
122
0
    {
123
0
        CPLError(CE_Failure, CPLE_AppDefined, "Expected 4 values for bounds");
124
0
        return false;
125
0
    }
126
0
    const double dfMinX = CPLAtof(aosBounds[0]);
127
0
    const double dfMinY = CPLAtof(aosBounds[1]);
128
0
    const double dfMaxX = CPLAtof(aosBounds[2]);
129
0
    const double dfMaxY = CPLAtof(aosBounds[3]);
130
0
    if (std::fabs(dfMinX) > 180 || std::fabs(dfMinY) > 90 ||
131
0
        std::fabs(dfMaxX) > 180 || std::fabs(dfMaxY) > 90)
132
0
    {
133
0
        CPLError(CE_Failure, CPLE_AppDefined, "Invalid bounds");
134
0
        return false;
135
0
    }
136
137
0
    double dfCenterLong = 0;
138
0
    double dfCenterLat = 0;
139
0
    int nCenterZoom = 0;
140
0
    const auto osCenter = oObj.GetString("center");
141
0
    if (osCenter.empty())
142
0
    {
143
0
        dfCenterLong = (dfMinX + dfMaxX) / 2;
144
0
        dfCenterLat = (dfMinY + dfMaxY) / 2;
145
0
        nCenterZoom = nMaxZoom;
146
0
    }
147
0
    else
148
0
    {
149
0
        const CPLStringList aosCenter(
150
0
            CSLTokenizeString2(osCenter.c_str(), ",", 0));
151
0
        if (aosCenter.size() != 3)
152
0
        {
153
0
            CPLError(CE_Failure, CPLE_AppDefined,
154
0
                     "Expected 3 values for center");
155
0
            return false;
156
0
        }
157
0
        dfCenterLong = CPLAtof(aosCenter[0]);
158
0
        dfCenterLat = CPLAtof(aosCenter[1]);
159
0
        if (std::fabs(dfCenterLong) > 180 || std::fabs(dfCenterLat) > 90)
160
0
        {
161
0
            CPLError(CE_Failure, CPLE_AppDefined, "Invalid center");
162
0
            return false;
163
0
        }
164
0
        nCenterZoom = atoi(aosCenter[2]);
165
0
        if (nCenterZoom < 0 || nCenterZoom > 255)
166
0
        {
167
0
            CPLError(CE_Failure, CPLE_AppDefined,
168
0
                     "Missing or invalid center zoom");
169
0
            return false;
170
0
        }
171
0
    }
172
173
0
    CPLJSONDocument oMetadataDoc;
174
0
    oMetadataDoc.SetRoot(oObj);
175
0
    osMetadata = oMetadataDoc.SaveAsString();
176
    // CPLDebugOnly("PMTiles", "Metadata = %s", osMetadata.c_str());
177
178
0
    sHeader.root_dir_offset = PMTILES_HEADER_LENGTH;
179
0
    sHeader.root_dir_bytes = 0;
180
0
    sHeader.json_metadata_offset = 0;
181
0
    sHeader.json_metadata_bytes = 0;
182
0
    sHeader.leaf_dirs_offset = 0;
183
0
    sHeader.leaf_dirs_bytes = 0;
184
0
    sHeader.tile_data_offset = 0;
185
0
    sHeader.tile_data_bytes = 0;
186
0
    sHeader.addressed_tiles_count = 0;
187
0
    sHeader.tile_entries_count = 0;
188
0
    sHeader.tile_contents_count = 0;
189
0
    sHeader.clustered = true;
190
0
    sHeader.internal_compression = pmtiles::COMPRESSION_GZIP;
191
0
    sHeader.tile_compression = tile_type == pmtiles::TILETYPE_MVT
192
0
                                   ? pmtiles::COMPRESSION_GZIP
193
0
                                   : pmtiles::COMPRESSION_NONE;
194
0
    sHeader.tile_type = tile_type;
195
0
    sHeader.min_zoom = static_cast<uint8_t>(nMinZoom);
196
0
    sHeader.max_zoom = static_cast<uint8_t>(nMaxZoom);
197
0
    sHeader.min_lon_e7 = static_cast<int32_t>(dfMinX * 10e6);
198
0
    sHeader.min_lat_e7 = static_cast<int32_t>(dfMinY * 10e6);
199
0
    sHeader.max_lon_e7 = static_cast<int32_t>(dfMaxX * 10e6);
200
0
    sHeader.max_lat_e7 = static_cast<int32_t>(dfMaxY * 10e6);
201
0
    sHeader.center_zoom = static_cast<uint8_t>(nCenterZoom);
202
0
    sHeader.center_lon_e7 = static_cast<int32_t>(dfCenterLong * 10e6);
203
0
    sHeader.center_lat_e7 = static_cast<int32_t>(dfCenterLat * 10e6);
204
205
0
    return true;
206
0
}
207
208
/************************************************************************/
209
/*                    OGRPMTilesConvertFromMBTiles()                    */
210
/************************************************************************/
211
212
bool OGRPMTilesConvertFromMBTiles(const char *pszDestName,
213
                                  const char *pszSrcName)
214
0
{
215
0
    const char *const apszAllowedDrivers[] = {"SQLite", nullptr};
216
0
    auto poSQLiteDS = std::unique_ptr<GDALDataset>(
217
0
        GDALDataset::Open(pszSrcName, GDAL_OF_VECTOR, apszAllowedDrivers));
218
0
    if (!poSQLiteDS)
219
0
    {
220
0
        CPLError(CE_Failure, CPLE_AppDefined,
221
0
                 "Cannot open %s with SQLite driver", pszSrcName);
222
0
        return false;
223
0
    }
224
225
0
    pmtiles::headerv3 sHeader;
226
0
    std::string osMetadata;
227
0
    if (!ProcessMetadata(poSQLiteDS.get(), sHeader, osMetadata))
228
0
        return false;
229
230
0
    auto poTilesLayer = poSQLiteDS->GetLayerByName("tiles");
231
0
    if (!poTilesLayer)
232
0
    {
233
0
        CPLError(CE_Failure, CPLE_AppDefined, "tiles table not found");
234
0
        return false;
235
0
    }
236
237
0
    const int iZoomLevel =
238
0
        poTilesLayer->GetLayerDefn()->GetFieldIndex("zoom_level");
239
0
    const int iTileColumn =
240
0
        poTilesLayer->GetLayerDefn()->GetFieldIndex("tile_column");
241
0
    const int iTileRow =
242
0
        poTilesLayer->GetLayerDefn()->GetFieldIndex("tile_row");
243
0
    const int iTileData =
244
0
        poTilesLayer->GetLayerDefn()->GetFieldIndex("tile_data");
245
0
    if (iZoomLevel < 0 || iTileColumn < 0 || iTileRow < 0 || iTileData < 0)
246
0
    {
247
0
        CPLError(CE_Failure, CPLE_AppDefined, "Bad structure for tiles table");
248
0
        return false;
249
0
    }
250
251
0
    struct TileEntry
252
0
    {
253
0
        uint64_t nTileId;
254
0
        std::array<unsigned char, 16> abyMD5;
255
0
    };
256
257
    // In a first step browse through the tiles table to compute the PMTiles
258
    // tile_id of each tile, and compute a hash of the tile data for
259
    // deduplication
260
0
    std::vector<TileEntry> asTileEntries;
261
0
    for (auto &&poFeature : poTilesLayer)
262
0
    {
263
0
        const int nZoomLevel = poFeature->GetFieldAsInteger(iZoomLevel);
264
0
        if (nZoomLevel < 0 || nZoomLevel > 30)
265
0
        {
266
0
            CPLError(CE_Warning, CPLE_AppDefined,
267
0
                     "Skipping tile with missing or invalid zoom_level");
268
0
            continue;
269
0
        }
270
0
        const int nColumn = poFeature->GetFieldAsInteger(iTileColumn);
271
0
        if (nColumn < 0 || nColumn >= (1 << nZoomLevel))
272
0
        {
273
0
            CPLError(CE_Warning, CPLE_AppDefined,
274
0
                     "Skipping tile with missing or invalid tile_column");
275
0
            continue;
276
0
        }
277
0
        const int nRow = poFeature->GetFieldAsInteger(iTileRow);
278
0
        if (nRow < 0 || nRow >= (1 << nZoomLevel))
279
0
        {
280
0
            CPLError(CE_Warning, CPLE_AppDefined,
281
0
                     "Skipping tile with missing or invalid tile_row");
282
0
            continue;
283
0
        }
284
        // MBTiles uses a 0=bottom-most row, whereas PMTiles uses
285
        // 0=top-most row
286
0
        const int nY = (1 << nZoomLevel) - 1 - nRow;
287
0
        uint64_t nTileId;
288
0
        try
289
0
        {
290
0
            nTileId = pmtiles::zxy_to_tileid(static_cast<uint8_t>(nZoomLevel),
291
0
                                             nColumn, nY);
292
0
        }
293
0
        catch (const std::exception &e)
294
0
        {
295
            // shouldn't happen given previous checks
296
0
            CPLError(CE_Failure, CPLE_AppDefined, "Cannot compute tile id: %s",
297
0
                     e.what());
298
0
            return false;
299
0
        }
300
0
        int nTileDataLength = 0;
301
0
        const GByte *pabyData =
302
0
            poFeature->GetFieldAsBinary(iTileData, &nTileDataLength);
303
0
        if (!pabyData)
304
0
        {
305
0
            CPLError(CE_Failure, CPLE_AppDefined, "Missing tile_data");
306
0
            return false;
307
0
        }
308
309
0
        TileEntry sEntry;
310
0
        sEntry.nTileId = nTileId;
311
312
0
        CPLMD5Context md5context;
313
0
        CPLMD5Init(&md5context);
314
0
        CPLMD5Update(&md5context, pabyData, nTileDataLength);
315
0
        CPLMD5Final(&sEntry.abyMD5[0], &md5context);
316
0
        try
317
0
        {
318
0
            asTileEntries.push_back(sEntry);
319
0
        }
320
0
        catch (const std::exception &e)
321
0
        {
322
0
            CPLError(CE_Failure, CPLE_AppDefined,
323
0
                     "Out of memory browsing through tiles: %s", e.what());
324
0
            return false;
325
0
        }
326
0
    }
327
328
    // Sort the tiles by ascending tile_id. This is a requirement to build
329
    // the PMTiles directories.
330
0
    std::sort(asTileEntries.begin(), asTileEntries.end(),
331
0
              [](const TileEntry &a, const TileEntry &b)
332
0
              { return a.nTileId < b.nTileId; });
333
334
    // Let's gather tile data in
335
    // a way that corresponds to the "clustered" mode, that is
336
    // "offsets are either contiguous with the previous offset+length, or
337
    // refer to a lesser offset, when writing with deduplication."
338
339
0
    std::vector<pmtiles::entryv3> asPMTilesEntries;
340
0
    uint64_t nFileOffset = 0;
341
0
    std::unordered_map<std::array<unsigned char, 16>,
342
0
                       std::pair<uint64_t, uint32_t>,
343
0
                       HashArray<unsigned char, 16>>
344
0
        oMapMD5ToOffsetLen;
345
0
    {
346
0
        uint64_t nLastTileId = 0;
347
0
        std::array<unsigned char, 16> abyLastMD5{0, 0, 0, 0, 0, 0, 0, 0,
348
0
                                                 0, 0, 0, 0, 0, 0, 0, 0};
349
0
        for (const auto &sEntry : asTileEntries)
350
0
        {
351
0
            if (sEntry.nTileId == nLastTileId + 1 &&
352
0
                sEntry.abyMD5 == abyLastMD5)
353
0
            {
354
                // If the tile id immediately follows the previous one and
355
                // has the same tile data, increase the run_length
356
0
                asPMTilesEntries.back().run_length++;
357
0
            }
358
0
            else
359
0
            {
360
0
                pmtiles::entryv3 sPMTilesEntry;
361
0
                sPMTilesEntry.tile_id = sEntry.nTileId;
362
0
                sPMTilesEntry.run_length = 1;
363
364
0
                auto oIter = oMapMD5ToOffsetLen.find(sEntry.abyMD5);
365
0
                if (oIter != oMapMD5ToOffsetLen.end())
366
0
                {
367
                    // Point to previously written tile data if this content
368
                    // has already been written
369
0
                    sPMTilesEntry.offset = oIter->second.first;
370
0
                    sPMTilesEntry.length = oIter->second.second;
371
0
                }
372
0
                else
373
0
                {
374
0
                    try
375
0
                    {
376
0
                        const auto sXYZ =
377
0
                            pmtiles::tileid_to_zxy(sEntry.nTileId);
378
0
                        poTilesLayer->SetAttributeFilter(CPLSPrintf(
379
0
                            "zoom_level = %d AND tile_column = %u AND tile_row "
380
0
                            "= "
381
0
                            "%u",
382
0
                            sXYZ.z, sXYZ.x, (1U << sXYZ.z) - 1U - sXYZ.y));
383
0
                    }
384
0
                    catch (const std::exception &e)
385
0
                    {
386
                        // shouldn't happen given previous checks
387
0
                        CPLError(CE_Failure, CPLE_AppDefined,
388
0
                                 "Cannot compute xyz: %s", e.what());
389
0
                        return false;
390
0
                    }
391
0
                    poTilesLayer->ResetReading();
392
0
                    auto poFeature = std::unique_ptr<OGRFeature>(
393
0
                        poTilesLayer->GetNextFeature());
394
0
                    if (!poFeature)
395
0
                    {
396
0
                        CPLError(CE_Failure, CPLE_AppDefined,
397
0
                                 "Cannot find tile");
398
0
                        return false;
399
0
                    }
400
0
                    int nTileDataLength = 0;
401
0
                    const GByte *pabyData = poFeature->GetFieldAsBinary(
402
0
                        iTileData, &nTileDataLength);
403
0
                    if (!pabyData)
404
0
                    {
405
0
                        CPLError(CE_Failure, CPLE_AppDefined,
406
0
                                 "Missing tile_data");
407
0
                        return false;
408
0
                    }
409
410
0
                    sPMTilesEntry.offset = nFileOffset;
411
0
                    sPMTilesEntry.length = nTileDataLength;
412
413
0
                    oMapMD5ToOffsetLen[sEntry.abyMD5] =
414
0
                        std::pair<uint64_t, uint32_t>(nFileOffset,
415
0
                                                      nTileDataLength);
416
417
0
                    nFileOffset += nTileDataLength;
418
0
                }
419
420
0
                asPMTilesEntries.push_back(sPMTilesEntry);
421
422
0
                nLastTileId = sEntry.nTileId;
423
0
                abyLastMD5 = sEntry.abyMD5;
424
0
            }
425
0
        }
426
0
    }
427
428
0
    const CPLCompressor *psCompressor = CPLGetCompressor("gzip");
429
0
    assert(psCompressor);
430
0
    std::string osCompressed;
431
432
0
    struct compression_exception : std::exception
433
0
    {
434
0
        const char *what() const noexcept override
435
0
        {
436
0
            return "Compression failed";
437
0
        }
438
0
    };
439
440
0
    const auto oCompressFunc = [psCompressor,
441
0
                                &osCompressed](const std::string &osBytes,
442
0
                                               uint8_t) -> std::string
443
0
    {
444
0
        osCompressed.resize(32 + osBytes.size() * 2);
445
0
        size_t nOutputSize = osCompressed.size();
446
0
        void *pOutputData = &osCompressed[0];
447
0
        if (!psCompressor->pfnFunc(osBytes.data(), osBytes.size(), &pOutputData,
448
0
                                   &nOutputSize, nullptr,
449
0
                                   psCompressor->user_data))
450
0
        {
451
0
            throw compression_exception();
452
0
        }
453
0
        osCompressed.resize(nOutputSize);
454
0
        return osCompressed;
455
0
    };
456
457
0
    std::string osCompressedMetadata;
458
459
0
    std::string osRootBytes;
460
0
    std::string osLeaveBytes;
461
0
    int nNumLeaves;
462
0
    try
463
0
    {
464
0
        osCompressedMetadata =
465
0
            oCompressFunc(osMetadata, pmtiles::COMPRESSION_GZIP);
466
467
        // Build the root and leave directories (one depth max)
468
0
        std::tie(osRootBytes, osLeaveBytes, nNumLeaves) =
469
0
            pmtiles::make_root_leaves(oCompressFunc, pmtiles::COMPRESSION_GZIP,
470
0
                                      asPMTilesEntries);
471
0
    }
472
0
    catch (const std::exception &e)
473
0
    {
474
0
        CPLError(CE_Failure, CPLE_AppDefined, "Cannot build directories: %s",
475
0
                 e.what());
476
0
        return false;
477
0
    }
478
479
    // Finalize the header fields related to offsets and size of the
480
    // different parts of the file
481
0
    sHeader.root_dir_bytes = osRootBytes.size();
482
0
    sHeader.json_metadata_offset =
483
0
        sHeader.root_dir_offset + sHeader.root_dir_bytes;
484
0
    sHeader.json_metadata_bytes = osCompressedMetadata.size();
485
0
    sHeader.leaf_dirs_offset =
486
0
        sHeader.json_metadata_offset + sHeader.json_metadata_bytes;
487
0
    sHeader.leaf_dirs_bytes = osLeaveBytes.size();
488
0
    sHeader.tile_data_offset =
489
0
        sHeader.leaf_dirs_offset + sHeader.leaf_dirs_bytes;
490
0
    sHeader.tile_data_bytes = nFileOffset;
491
492
    // Number of tiles that are addressable in the PMTiles archive, that is
493
    // the number of tiles we would have if not deduplicating them
494
0
    sHeader.addressed_tiles_count = asTileEntries.size();
495
496
    // Number of tile entries in root and leave directories
497
    // ie entries whose run_length >= 1
498
0
    sHeader.tile_entries_count = asPMTilesEntries.size();
499
500
    // Number of distinct tile blobs
501
0
    sHeader.tile_contents_count = oMapMD5ToOffsetLen.size();
502
503
    // Now build the file!
504
0
    auto poFile = VSIVirtualHandleUniquePtr(VSIFOpenL(pszDestName, "wb"));
505
0
    if (!poFile)
506
0
    {
507
0
        CPLError(CE_Failure, CPLE_FileIO, "Cannot open %s for write",
508
0
                 pszDestName);
509
0
        return false;
510
0
    }
511
0
    const auto osHeader = sHeader.serialize();
512
513
0
    if (poFile->Write(osHeader.data(), osHeader.size(), 1) != 1 ||
514
0
        poFile->Write(osRootBytes.data(), osRootBytes.size(), 1) != 1 ||
515
0
        poFile->Write(osCompressedMetadata.data(), osCompressedMetadata.size(),
516
0
                      1) != 1 ||
517
0
        (!osLeaveBytes.empty() &&
518
0
         poFile->Write(osLeaveBytes.data(), osLeaveBytes.size(), 1) != 1))
519
0
    {
520
0
        CPLError(CE_Failure, CPLE_FileIO, "Failed writing");
521
0
        return false;
522
0
    }
523
524
    // Copy tile content at end of the output file.
525
0
    {
526
0
        uint64_t nLastTileId = 0;
527
0
        uint64_t nFileOffset2 = 0;
528
0
        std::array<unsigned char, 16> abyLastMD5{0, 0, 0, 0, 0, 0, 0, 0,
529
0
                                                 0, 0, 0, 0, 0, 0, 0, 0};
530
0
        std::unordered_set<std::array<unsigned char, 16>,
531
0
                           HashArray<unsigned char, 16>>
532
0
            oSetMD5;
533
0
        for (const auto &sEntry : asTileEntries)
534
0
        {
535
0
            if (sEntry.nTileId == nLastTileId + 1 &&
536
0
                sEntry.abyMD5 == abyLastMD5)
537
0
            {
538
                // If the tile id immediately follows the previous one and
539
                // has the same tile data, do nothing
540
0
            }
541
0
            else
542
0
            {
543
0
                auto oIter = oSetMD5.find(sEntry.abyMD5);
544
0
                if (oIter == oSetMD5.end())
545
0
                {
546
0
                    try
547
0
                    {
548
0
                        const auto sXYZ =
549
0
                            pmtiles::tileid_to_zxy(sEntry.nTileId);
550
0
                        poTilesLayer->SetAttributeFilter(CPLSPrintf(
551
0
                            "zoom_level = %d AND tile_column = %u AND tile_row "
552
0
                            "= "
553
0
                            "%u",
554
0
                            sXYZ.z, sXYZ.x, (1U << sXYZ.z) - 1U - sXYZ.y));
555
0
                    }
556
0
                    catch (const std::exception &e)
557
0
                    {
558
                        // shouldn't happen given previous checks
559
0
                        CPLError(CE_Failure, CPLE_AppDefined,
560
0
                                 "Cannot compute xyz: %s", e.what());
561
0
                        return false;
562
0
                    }
563
0
                    poTilesLayer->ResetReading();
564
0
                    auto poFeature = std::unique_ptr<OGRFeature>(
565
0
                        poTilesLayer->GetNextFeature());
566
0
                    if (!poFeature)
567
0
                    {
568
0
                        CPLError(CE_Failure, CPLE_AppDefined,
569
0
                                 "Cannot find tile");
570
0
                        return false;
571
0
                    }
572
0
                    int nTileDataLength = 0;
573
0
                    const GByte *pabyData = poFeature->GetFieldAsBinary(
574
0
                        iTileData, &nTileDataLength);
575
0
                    if (!pabyData)
576
0
                    {
577
0
                        CPLError(CE_Failure, CPLE_AppDefined,
578
0
                                 "Missing tile_data");
579
0
                        return false;
580
0
                    }
581
582
0
                    oSetMD5.insert(sEntry.abyMD5);
583
584
0
                    if (poFile->Write(pabyData, nTileDataLength, 1) != 1)
585
0
                    {
586
0
                        CPLError(CE_Failure, CPLE_FileIO, "Failed writing");
587
0
                        return false;
588
0
                    }
589
590
0
                    nFileOffset2 += nTileDataLength;
591
0
                }
592
593
0
                nLastTileId = sEntry.nTileId;
594
0
                abyLastMD5 = sEntry.abyMD5;
595
0
            }
596
0
        }
597
598
0
        CPL_IGNORE_RET_VAL(nFileOffset2);
599
0
        CPLAssert(nFileOffset2 == nFileOffset);
600
0
    }
601
602
0
    if (poFile->Close() != 0)
603
0
        return false;
604
605
0
    return true;
606
0
}