LCOV - code coverage report
Current view: top level - ogr/ogrsf_frmts/pmtiles - ogrpmtilesfrommbtiles.cpp (source / functions) Hit Total Coverage
Test: gdal_filtered.info Lines: 223 298 74.8 %
Date: 2026-05-29 23:25:07 Functions: 4 5 80.0 %

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

Generated by: LCOV version 1.14