LCOV - code coverage report
Current view: top level - ogr/ogrsf_frmts/pmtiles - ogrpmtilestileiterator.cpp (source / functions) Hit Total Coverage
Test: gdal_filtered.info Lines: 190 234 81.2 %
Date: 2026-06-19 21:24:00 Functions: 5 5 100.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 "ogr_pmtiles.h"
      14             : 
      15             : #include <algorithm>
      16             : #include <limits>
      17             : 
      18             : /************************************************************************/
      19             : /*                   find_tile_idx_lesser_or_equal()                    */
      20             : /************************************************************************/
      21             : 
      22             : static int
      23        1270 : find_tile_idx_lesser_or_equal(const std::vector<pmtiles::entryv3> &entries,
      24             :                               uint64_t tile_id)
      25             : {
      26        1270 :     if (!entries.empty() && tile_id <= entries[0].tile_id)
      27         253 :         return 0;
      28             : 
      29        1017 :     int m = 0;
      30        1017 :     int n = static_cast<int>(entries.size()) - 1;
      31        3891 :     while (m <= n)
      32             :     {
      33        2965 :         int k = (n + m) >> 1;
      34        2965 :         if (tile_id > entries[k].tile_id)
      35             :         {
      36        1912 :             m = k + 1;
      37             :         }
      38        1053 :         else if (tile_id < entries[k].tile_id)
      39             :         {
      40         962 :             n = k - 1;
      41             :         }
      42             :         else
      43             :         {
      44          91 :             return k;
      45             :         }
      46             :     }
      47             : 
      48         926 :     return n;
      49             : }
      50             : 
      51             : /************************************************************************/
      52             : /*                         LoadRootDirectory()                          */
      53             : /************************************************************************/
      54             : 
      55         379 : bool OGRPMTilesTileIterator::LoadRootDirectory()
      56             : {
      57         379 :     if (m_nZoomLevel >= 0)
      58             :     {
      59         379 :         CPLDebugOnly("PMTiles", "minx=%d miny=%d maxx=%d maxy=%d", m_nMinX,
      60             :                      m_nMinY, m_nMaxX, m_nMaxY);
      61             :         // If we don't query too many tiles, establish the minimum
      62             :         // and maximum tile id, we are interested in.
      63             :         // (is there a clever way of figuring out this?)
      64         379 :         if (m_nMinX >= 0 && m_nMinY >= 0 && m_nMaxX >= m_nMinX &&
      65         296 :             m_nMaxY >= m_nMinY &&
      66         296 :             (m_nMaxX - m_nMinX + 1) <= 100 / (m_nMaxY - m_nMinY + 1))
      67             :         {
      68         376 :             for (int iY = m_nMinY; iY <= m_nMaxY; ++iY)
      69             :             {
      70         843 :                 for (int iX = m_nMinX; iX <= m_nMaxX; ++iX)
      71             :                 {
      72        1252 :                     const uint64_t nTileId = pmtiles::zxy_to_tileid(
      73         627 :                         static_cast<uint8_t>(m_nZoomLevel), iX, iY);
      74         625 :                     m_nMinTileId = std::min(m_nMinTileId, nTileId);
      75         625 :                     m_nMaxTileId = std::max(m_nMaxTileId, nTileId);
      76             :                 }
      77         158 :             }
      78             :         }
      79             :         else
      80             :         {
      81         438 :             m_nMinTileId = pmtiles::zxy_to_tileid(
      82         219 :                 static_cast<uint8_t>(m_nZoomLevel), 0, 0);
      83         438 :             m_nMaxTileId = pmtiles::zxy_to_tileid(
      84         219 :                                static_cast<uint8_t>(m_nZoomLevel) + 1, 0, 0) -
      85             :                            1;
      86             :         }
      87             : 
      88             :         // If filtering by bbox and that the gap between min and max
      89             :         // tile id is too big, use an iteration over (x, y) space rather
      90             :         // than tile id space.
      91             : 
      92             :         // Config option for debugging purposes
      93         377 :         const unsigned nThreshold = static_cast<unsigned>(atoi(
      94         377 :             CPLGetConfigOption("OGR_PMTILES_ITERATOR_THRESHOLD", "10000")));
      95         377 :         if (m_nMinX >= 0 && m_nMinY >= 0 && m_nMaxX >= m_nMinX &&
      96         294 :             m_nMaxY >= m_nMinY &&
      97         294 :             !(m_nMinX == 0 && m_nMinY == 0 &&
      98         157 :               m_nMaxX == (1 << m_nZoomLevel) - 1 &&
      99         144 :               m_nMaxY == (1 << m_nZoomLevel) - 1) &&
     100         152 :             m_nMaxTileId - m_nMinTileId > nThreshold)
     101             :         {
     102           4 :             m_nCurX = m_nMinX;
     103           4 :             m_nCurY = m_nMinY;
     104           8 :             m_nMinTileId = pmtiles::zxy_to_tileid(
     105           4 :                 static_cast<uint8_t>(m_nZoomLevel), m_nCurX, m_nCurY);
     106           4 :             m_nMaxTileId = m_nMinTileId;
     107             :         }
     108             :     }
     109             : 
     110         377 :     const auto &sHeader = m_poDS->GetHeader();
     111         754 :     const auto *posStr = m_poDS->ReadInternal(
     112         377 :         sHeader.root_dir_offset, static_cast<uint32_t>(sHeader.root_dir_bytes),
     113             :         "header");
     114         377 :     if (!posStr)
     115             :     {
     116           0 :         return false;
     117             :     }
     118             : 
     119         754 :     DirectoryContext sContext;
     120         377 :     sContext.sEntries = pmtiles::deserialize_directory(*posStr);
     121             : 
     122         377 :     if (m_nZoomLevel >= 0)
     123             :     {
     124         377 :         if (m_nCurX >= 0)
     125             :         {
     126             :             while (true)
     127             :             {
     128           4 :                 const int nMinEntryIdx = find_tile_idx_lesser_or_equal(
     129             :                     sContext.sEntries, m_nMinTileId);
     130           4 :                 if (nMinEntryIdx < 0)
     131             :                 {
     132           0 :                     m_nCurX++;
     133           0 :                     if (m_nCurX > m_nMaxX)
     134             :                     {
     135           0 :                         m_nCurX = m_nMinX;
     136           0 :                         m_nCurY++;
     137           0 :                         if (m_nCurY > m_nMaxY)
     138             :                         {
     139           0 :                             return false;
     140             :                         }
     141             :                     }
     142           0 :                     m_nMinTileId = pmtiles::zxy_to_tileid(
     143           0 :                         static_cast<uint8_t>(m_nZoomLevel), m_nCurX, m_nCurY);
     144           0 :                     m_nMaxTileId = m_nMinTileId;
     145             :                 }
     146             :                 else
     147             :                 {
     148           4 :                     sContext.nIdxInEntries = nMinEntryIdx;
     149           4 :                     break;
     150             :                 }
     151           0 :             }
     152             :         }
     153             :         else
     154             :         {
     155             :             const int nMinEntryIdx =
     156         373 :                 find_tile_idx_lesser_or_equal(sContext.sEntries, m_nMinTileId);
     157         373 :             if (nMinEntryIdx < 0)
     158             :             {
     159           0 :                 return false;
     160             :             }
     161         373 :             sContext.nIdxInEntries = nMinEntryIdx;
     162             :         }
     163             :     }
     164             : 
     165         377 :     m_aoStack.emplace(std::move(sContext));
     166         377 :     return true;
     167             : }
     168             : 
     169             : /************************************************************************/
     170             : /*                           SkipRunLength()                            */
     171             : /************************************************************************/
     172             : 
     173           2 : void OGRPMTilesTileIterator::SkipRunLength()
     174             : {
     175           2 :     if (!m_aoStack.empty())
     176             :     {
     177           2 :         auto &topContext = m_aoStack.top();
     178           2 :         if (topContext.nIdxInEntries < topContext.sEntries.size())
     179             :         {
     180             :             const auto &sCurrentEntry =
     181           2 :                 topContext.sEntries[topContext.nIdxInEntries];
     182           2 :             if (sCurrentEntry.run_length > 1)
     183             :             {
     184           2 :                 m_nLastTileId =
     185           2 :                     sCurrentEntry.tile_id + sCurrentEntry.run_length - 1;
     186           2 :                 topContext.nIdxInRunLength = sCurrentEntry.run_length;
     187             :             }
     188             :         }
     189             :     }
     190           2 : }
     191             : 
     192             : /************************************************************************/
     193             : /*                            GetNextTile()                             */
     194             : /************************************************************************/
     195             : 
     196        1057 : pmtiles::entry_zxy OGRPMTilesTileIterator::GetNextTile(uint32_t *pnRunLength)
     197             : {
     198        1057 :     if (m_bEOF)
     199          15 :         return pmtiles::entry_zxy(0, 0, 0, 0, 0);
     200             : 
     201        1042 :     const auto &sHeader = m_poDS->GetHeader();
     202             :     try
     203             :     {
     204             :         // Put the root directory as the first element of the stack
     205             :         // of directories, if the stack is empty
     206        1042 :         if (m_aoStack.empty())
     207             :         {
     208         379 :             if (!LoadRootDirectory())
     209             :             {
     210           0 :                 m_bEOF = true;
     211         850 :                 return pmtiles::entry_zxy(0, 0, 0, 0, 0);
     212             :             }
     213             :         }
     214             : 
     215        7171 :         const auto AdvanceToNextTile = [this]()
     216             :         {
     217        1704 :             if (m_nCurX >= 0)
     218             :             {
     219             :                 while (true)
     220             :                 {
     221         682 :                     m_nCurX++;
     222         682 :                     if (m_nCurX > m_nMaxX)
     223             :                     {
     224          35 :                         m_nCurX = m_nMinX;
     225          35 :                         m_nCurY++;
     226          35 :                         if (m_nCurY > m_nMaxY)
     227             :                         {
     228           4 :                             m_bEOF = true;
     229           4 :                             return false;
     230             :                         }
     231             :                     }
     232         678 :                     if (!m_bEOF)
     233             :                     {
     234        1356 :                         m_nMinTileId = pmtiles::zxy_to_tileid(
     235         678 :                             static_cast<uint8_t>(m_nZoomLevel), m_nCurX,
     236         678 :                             m_nCurY);
     237         678 :                         m_nMaxTileId = m_nMinTileId;
     238         678 :                         m_nLastTileId = INVALID_LAST_TILE_ID;
     239         678 :                         while (m_aoStack.size() > 1)
     240           0 :                             m_aoStack.pop();
     241        1356 :                         const int nMinEntryIdx = find_tile_idx_lesser_or_equal(
     242         678 :                             m_aoStack.top().sEntries, m_nMinTileId);
     243         678 :                         if (nMinEntryIdx < 0)
     244             :                         {
     245           0 :                             continue;
     246             :                         }
     247         678 :                         m_aoStack.top().nIdxInEntries = nMinEntryIdx;
     248         678 :                         m_aoStack.top().nIdxInRunLength = 0;
     249         678 :                         break;
     250             :                     }
     251           0 :                 }
     252         678 :                 return true;
     253             :             }
     254        1022 :             return false;
     255        1040 :         };
     256             : 
     257             :         while (true)
     258             :         {
     259        9120 :             if (m_aoStack.top().nIdxInEntries ==
     260        4560 :                 m_aoStack.top().sEntries.size())
     261             :             {
     262         433 :                 if (m_aoStack.size() == 1 && AdvanceToNextTile())
     263         129 :                     continue;
     264             : 
     265         304 :                 m_aoStack.pop();
     266         304 :                 if (m_aoStack.empty())
     267         153 :                     break;
     268         151 :                 continue;
     269             :             }
     270        4127 :             auto &topContext = m_aoStack.top();
     271        4127 :             auto &sCurrentEntry = topContext.sEntries[topContext.nIdxInEntries];
     272        4127 :             if (sCurrentEntry.run_length == 0)
     273             :             {
     274             :                 // Arbitrary limit. 5 seems to be the maximum value supported
     275             :                 // by pmtiles.hpp::get_tile()
     276         215 :                 if (m_aoStack.size() == 5)
     277             :                 {
     278           0 :                     m_bEOF = true;
     279           0 :                     CPLError(CE_Failure, CPLE_AppDefined,
     280             :                              "Too many levels of nested directories");
     281           0 :                     break;
     282             :                 }
     283             : 
     284         430 :                 if (sHeader.leaf_dirs_offset >
     285         215 :                     std::numeric_limits<uint64_t>::max() - sCurrentEntry.offset)
     286             :                 {
     287           0 :                     m_bEOF = true;
     288           0 :                     CPLError(CE_Failure, CPLE_AppDefined,
     289             :                              "Invalid directory offset");
     290           0 :                     break;
     291             :                 }
     292         430 :                 const auto *posStr = m_poDS->ReadInternal(
     293         215 :                     sHeader.leaf_dirs_offset + sCurrentEntry.offset,
     294         215 :                     sCurrentEntry.length, "directory");
     295         215 :                 if (!posStr)
     296             :                 {
     297           0 :                     m_bEOF = true;
     298           0 :                     CPLError(
     299             :                         CE_Failure, CPLE_AppDefined,
     300             :                         "PMTILES: cannot read directory of size " CPL_FRMT_GUIB
     301             :                         " at "
     302             :                         "offset " CPL_FRMT_GUIB,
     303           0 :                         static_cast<GUIntBig>(sHeader.leaf_dirs_offset +
     304           0 :                                               sCurrentEntry.offset),
     305           0 :                         static_cast<GUIntBig>(sCurrentEntry.length));
     306           0 :                     break;
     307             :                 }
     308             : 
     309         215 :                 DirectoryContext sContext;
     310         215 :                 sContext.sEntries = pmtiles::deserialize_directory(*posStr);
     311         215 :                 if (sContext.sEntries.empty())
     312             :                 {
     313           0 :                     m_bEOF = true;
     314             :                     // In theory empty directories could exist, but for now
     315             :                     // do not allow this to be more robust against hostile files
     316             :                     // that could create many such empty directories
     317           0 :                     CPLError(CE_Failure, CPLE_AppDefined,
     318             :                              "Empty directory found");
     319           0 :                     break;
     320             :                 }
     321             : 
     322         317 :                 if (m_nLastTileId != INVALID_LAST_TILE_ID &&
     323         102 :                     sContext.sEntries[0].tile_id <= m_nLastTileId)
     324             :                 {
     325           0 :                     m_bEOF = true;
     326           0 :                     CPLError(CE_Failure, CPLE_AppDefined,
     327             :                              "Non increasing tile_id");
     328           0 :                     break;
     329             :                 }
     330             : 
     331         215 :                 if (m_nZoomLevel >= 0)
     332             :                 {
     333         215 :                     const int nMinEntryIdx = find_tile_idx_lesser_or_equal(
     334             :                         sContext.sEntries, m_nMinTileId);
     335         215 :                     if (nMinEntryIdx < 0)
     336             :                     {
     337           0 :                         if (AdvanceToNextTile())
     338           0 :                             continue;
     339           0 :                         break;
     340             :                     }
     341         215 :                     sContext.nIdxInEntries = nMinEntryIdx;
     342             :                 }
     343         215 :                 m_nLastTileId =
     344         215 :                     sContext.sEntries[sContext.nIdxInEntries].tile_id;
     345             : 
     346         215 :                 m_aoStack.emplace(std::move(sContext));
     347             : 
     348         215 :                 topContext.nIdxInEntries++;
     349             :             }
     350             :             else
     351             :             {
     352        3912 :                 if (topContext.nIdxInRunLength == sCurrentEntry.run_length)
     353             :                 {
     354        1152 :                     topContext.nIdxInEntries++;
     355        1152 :                     topContext.nIdxInRunLength = 0;
     356             :                 }
     357             :                 else
     358             :                 {
     359        2760 :                     const auto nIdxInRunLength = topContext.nIdxInRunLength;
     360        2760 :                     const uint64_t nTileId =
     361        2760 :                         sCurrentEntry.tile_id + nIdxInRunLength;
     362        2760 :                     m_nLastTileId = nTileId;
     363        2760 :                     const pmtiles::zxy zxy = pmtiles::tileid_to_zxy(nTileId);
     364             : 
     365             :                     // Sanity check to limit risk of iterating forever on
     366             :                     // broken run_length value
     367        2789 :                     if (nIdxInRunLength == 0 && sCurrentEntry.run_length > 1 &&
     368          29 :                         sCurrentEntry.run_length >
     369          29 :                             pmtiles::zxy_to_tileid(zxy.z + 1, 0, 0) - nTileId)
     370             :                     {
     371             :                         // Hit on /vsis3/us-west-2.opendata.source.coop/protomaps/openstreetmap/v4.pmtiles
     372           0 :                         sCurrentEntry.run_length = static_cast<uint32_t>(
     373           0 :                             pmtiles::zxy_to_tileid(zxy.z + 1, 0, 0) - nTileId);
     374             :                     }
     375             : 
     376        2760 :                     topContext.nIdxInRunLength++;
     377             : 
     378        2760 :                     if (m_nZoomLevel >= 0)
     379             :                     {
     380        2760 :                         if (nTileId < m_nMinTileId)
     381             :                         {
     382         843 :                             if (sCurrentEntry.run_length > 1)
     383             :                             {
     384          19 :                                 if (sCurrentEntry.tile_id +
     385          19 :                                         sCurrentEntry.run_length <=
     386          19 :                                     m_nMinTileId)
     387             :                                 {
     388           0 :                                     topContext.nIdxInRunLength =
     389           0 :                                         sCurrentEntry.run_length;
     390             :                                 }
     391             :                                 else
     392             :                                 {
     393          19 :                                     topContext.nIdxInRunLength =
     394             :                                         static_cast<uint32_t>(
     395          19 :                                             m_nMinTileId -
     396          19 :                                             sCurrentEntry.tile_id);
     397             :                                 }
     398          19 :                                 m_nLastTileId = sCurrentEntry.tile_id +
     399          19 :                                                 topContext.nIdxInRunLength - 1;
     400             :                             }
     401        1873 :                             continue;
     402             :                         }
     403        1917 :                         else if (nTileId > m_nMaxTileId)
     404             :                         {
     405         572 :                             if (AdvanceToNextTile())
     406         535 :                                 continue;
     407          37 :                             break;
     408             :                         }
     409             : 
     410        1345 :                         if (m_nMinX >= 0 &&
     411        1284 :                             zxy.x < static_cast<uint32_t>(m_nMinX))
     412         173 :                             continue;
     413        1172 :                         if (m_nMinY >= 0 &&
     414        1083 :                             zxy.y < static_cast<uint32_t>(m_nMinY))
     415          42 :                             continue;
     416        1130 :                         if (m_nMaxX >= 0 &&
     417        1069 :                             zxy.x > static_cast<uint32_t>(m_nMaxX))
     418         154 :                             continue;
     419         976 :                         if (m_nMaxY >= 0 &&
     420         887 :                             zxy.y > static_cast<uint32_t>(m_nMaxY))
     421         126 :                             continue;
     422             :                     }
     423             : 
     424        1700 :                     if (sHeader.tile_data_offset >
     425         850 :                         std::numeric_limits<uint64_t>::max() -
     426         850 :                             sCurrentEntry.offset)
     427             :                     {
     428           0 :                         m_bEOF = true;
     429           0 :                         CPLError(CE_Failure, CPLE_AppDefined,
     430             :                                  "Invalid tile offset");
     431           0 :                         break;
     432             :                     }
     433             : 
     434         850 :                     if (pnRunLength)
     435             :                     {
     436          18 :                         *pnRunLength =
     437          18 :                             sCurrentEntry.run_length - nIdxInRunLength;
     438             :                     }
     439             : 
     440             :                     // We must capture the result, before the below code
     441             :                     // that updates (m_nCurX, m_nCurY) and invalidates
     442             :                     // sCurrentEntry
     443             :                     const auto res = pmtiles::entry_zxy(
     444         850 :                         zxy.z, zxy.x, zxy.y,
     445         850 :                         sHeader.tile_data_offset + sCurrentEntry.offset,
     446         850 :                         sCurrentEntry.length);
     447             : 
     448         850 :                     AdvanceToNextTile();
     449             : 
     450         850 :                     return res;
     451             :                 }
     452             :             }
     453        3520 :         }
     454             :     }
     455           4 :     catch (const std::exception &e)
     456             :     {
     457           2 :         CPLError(CE_Failure, CPLE_AppDefined, "GetNextTile() failed with %s",
     458           2 :                  e.what());
     459             :     }
     460             : 
     461         192 :     m_bEOF = true;
     462         192 :     return pmtiles::entry_zxy(0, 0, 0, 0, 0);
     463             : }
     464             : 
     465             : /************************************************************************/
     466             : /*                             DumpTiles()                              */
     467             : /************************************************************************/
     468             : 
     469             : #ifdef DEBUG_PMTILES
     470             : void OGRPMTilesTileIterator::DumpTiles()
     471             : {
     472             :     int count = 0;
     473             :     while (true)
     474             :     {
     475             :         const auto sTile = GetNextTile();
     476             :         if (sTile.offset == 0)
     477             :             break;
     478             :         ++count;
     479             :         printf("%d -> %d %d %d %d %d\n",  // ok
     480             :                count, int(sTile.z), int(sTile.x), int(sTile.y),
     481             :                int(sTile.offset), int(sTile.length));
     482             :     }
     483             : }
     484             : #endif

Generated by: LCOV version 1.14