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) 2026, Even Rouault <even.rouault at spatialys.com>
9 : *
10 : * SPDX-License-Identifier: MIT
11 : ****************************************************************************/
12 :
13 : #include "ogrpmtilesfromtileset.h"
14 : #include "ogr_pmtiles.h"
15 :
16 : #include "cpl_json.h"
17 : #include "cpl_md5.h"
18 : #include "cpl_vsi_virtual.h"
19 : #include "gdal_priv.h"
20 :
21 : #include "include_pmtiles.h"
22 :
23 : #include <algorithm>
24 : #include <array>
25 : #include <cassert>
26 : #include <map>
27 : #include <unordered_map>
28 : #include <unordered_set>
29 : #include <utility>
30 :
31 : /************************************************************************/
32 : /* OGRPMTilesConvertFromTilesetInitializeHeader() */
33 : /************************************************************************/
34 :
35 12 : static void OGRPMTilesConvertFromTilesetInitializeHeader(
36 : GDALDataset *poSrcDS, CSLConstList papszOptions, int nMinZoom, int nMaxZoom,
37 : pmtiles::headerv3 &sHeader, const char *&pszExt, std::string &osMetadata)
38 : {
39 12 : OGREnvelope sExtent;
40 12 : poSrcDS->GetExtentWGS84LongLat(&sExtent);
41 :
42 12 : constexpr double MAX_LAT = 85.0511287798066;
43 12 : sExtent.MinY = std::max(-MAX_LAT, sExtent.MinY);
44 12 : sExtent.MaxY = std::min(MAX_LAT, sExtent.MaxY);
45 :
46 24 : CPLJSONObject oObj;
47 : const char *pszTileFormat =
48 12 : CSLFetchNameValueDef(papszOptions, "TILE_FORMAT", "PNG");
49 12 : const char *pszVersion = CSLFetchNameValueDef(
50 12 : papszOptions, "VERSION", EQUAL(pszTileFormat, "WEBP") ? "1.3" : "1.1");
51 12 : oObj.Set("version", pszVersion);
52 12 : oObj.Set("name", CSLFetchNameValueDef(papszOptions, "NAME", ""));
53 12 : oObj.Set("description",
54 12 : CSLFetchNameValueDef(papszOptions, "DESCRIPTION", ""));
55 12 : oObj.Set("type", CSLFetchNameValueDef(papszOptions, "TYPE", "overlay"));
56 12 : if (const char *pszElevationType =
57 12 : CSLFetchNameValue(papszOptions, "ELEVATION_TYPE"))
58 0 : oObj.Set("elevation_type", pszElevationType);
59 12 : uint8_t tile_type = pmtiles::TILETYPE_UNKNOWN;
60 12 : if (EQUAL(pszTileFormat, "PNG"))
61 : {
62 10 : pszExt = "png";
63 10 : tile_type = pmtiles::TILETYPE_PNG;
64 : }
65 2 : else if (EQUAL(pszTileFormat, "JPEG"))
66 : {
67 1 : pszExt = "jpg";
68 1 : tile_type = pmtiles::TILETYPE_JPEG;
69 : }
70 1 : else if (EQUAL(pszTileFormat, "WEBP"))
71 : {
72 1 : pszExt = "webp";
73 1 : tile_type = pmtiles::TILETYPE_WEBP;
74 : }
75 : else
76 0 : CPLAssert(false);
77 :
78 12 : oObj.Set("format", pszExt);
79 12 : oObj.Set("scheme", "xyz");
80 12 : oObj.Set("bounds", CPLSPrintf("%.17g,%.17g,%.17g,%.17g", sExtent.MinX,
81 : sExtent.MinY, sExtent.MaxX, sExtent.MaxY));
82 12 : const double dfCenterLong = (sExtent.MinX + sExtent.MaxX) / 2;
83 12 : const double dfCenterLat = (sExtent.MinY + sExtent.MaxY) / 2;
84 12 : const int nCenterZoom = nMaxZoom;
85 12 : oObj.Set("center", CPLSPrintf("%.17g,%.17g,%d", dfCenterLong, dfCenterLat,
86 : nCenterZoom));
87 12 : oObj.Set("minzoom", CPLSPrintf("%d", nMinZoom));
88 12 : oObj.Set("maxzoom", CPLSPrintf("%d", nMaxZoom));
89 :
90 12 : CPLJSONDocument oMetadataDoc;
91 12 : oMetadataDoc.SetRoot(oObj);
92 12 : osMetadata = oMetadataDoc.SaveAsString();
93 : // CPLDebugOnly("PMTiles", "Metadata = %s", osMetadata.c_str());
94 :
95 12 : sHeader.root_dir_offset = PMTILES_HEADER_LENGTH;
96 12 : sHeader.root_dir_bytes = 0;
97 12 : sHeader.json_metadata_offset = 0;
98 12 : sHeader.json_metadata_bytes = 0;
99 12 : sHeader.leaf_dirs_offset = 0;
100 12 : sHeader.leaf_dirs_bytes = 0;
101 12 : sHeader.tile_data_offset = 0;
102 12 : sHeader.tile_data_bytes = 0;
103 12 : sHeader.addressed_tiles_count = 0;
104 12 : sHeader.tile_entries_count = 0;
105 12 : sHeader.tile_contents_count = 0;
106 12 : sHeader.clustered = true;
107 12 : sHeader.internal_compression = pmtiles::COMPRESSION_GZIP;
108 12 : sHeader.tile_compression = pmtiles::COMPRESSION_NONE;
109 12 : sHeader.tile_type = tile_type;
110 12 : sHeader.min_zoom = static_cast<uint8_t>(nMinZoom);
111 12 : sHeader.max_zoom = static_cast<uint8_t>(nMaxZoom);
112 12 : sHeader.min_lon_e7 = static_cast<int32_t>(sExtent.MinX * 10e6);
113 12 : sHeader.min_lat_e7 = static_cast<int32_t>(sExtent.MinY * 10e6);
114 12 : sHeader.max_lon_e7 = static_cast<int32_t>(sExtent.MaxX * 10e6);
115 12 : sHeader.max_lat_e7 = static_cast<int32_t>(sExtent.MaxY * 10e6);
116 12 : sHeader.center_zoom = static_cast<uint8_t>(nCenterZoom);
117 12 : sHeader.center_lon_e7 = static_cast<int32_t>(dfCenterLong * 10e6);
118 12 : sHeader.center_lat_e7 = static_cast<int32_t>(dfCenterLat * 10e6);
119 12 : }
120 :
121 : /************************************************************************/
122 : /* OGRPMTilesConvertFromTileset() */
123 : /************************************************************************/
124 :
125 12 : bool OGRPMTilesConvertFromTileset(const char *pszDestName,
126 : const char *pszSrcDirectory,
127 : GDALDataset *poSrcDS,
128 : CSLConstList papszOptions)
129 : {
130 24 : const CPLStringList aosZoomLevelDirs(VSIReadDir(pszSrcDirectory));
131 12 : int nMinZoom = INT_MAX;
132 12 : int nMaxZoom = 0;
133 94 : for (const char *pszName : cpl::Iterate(aosZoomLevelDirs))
134 : {
135 82 : if (CPLGetValueType(pszName) == CPL_VALUE_INTEGER)
136 : {
137 16 : const int nZoom = atoi(pszName);
138 16 : nMinZoom = std::min(nMinZoom, nZoom);
139 16 : nMaxZoom = std::max(nMaxZoom, nZoom);
140 : }
141 : }
142 12 : if (nMinZoom > nMaxZoom)
143 : {
144 0 : CPLError(CE_Failure, CPLE_AppDefined, "No valid tile found");
145 0 : return false;
146 : }
147 :
148 : pmtiles::headerv3 sHeader;
149 12 : const char *pszExt = "";
150 24 : std::string osMetadata;
151 12 : OGRPMTilesConvertFromTilesetInitializeHeader(
152 : poSrcDS, papszOptions, nMinZoom, nMaxZoom, sHeader, pszExt, osMetadata);
153 :
154 : struct TileEntry
155 : {
156 : uint64_t nTileId;
157 : std::array<unsigned char, 16> abyMD5;
158 : };
159 :
160 : // In a first step browse through the tiles table to compute the PMTiles
161 : // tile_id of each tile, and compute a hash of the tile data for
162 : // deduplication
163 24 : std::vector<TileEntry> asTileEntries;
164 24 : std::vector<GByte> abyTileData;
165 24 : std::map<uint64_t, uint32_t> oMapTileIdToFileSize;
166 28 : for (int nZoom = nMinZoom; nZoom <= nMaxZoom; ++nZoom)
167 : {
168 : const std::string osZoomDir(CPLFormFilenameSafe(
169 16 : pszSrcDirectory, CPLSPrintf("%d", nZoom), nullptr));
170 : std::unique_ptr<VSIDIR, decltype(&VSICloseDir)> psDirX(
171 16 : VSIOpenDir(osZoomDir.c_str(), 0, nullptr), VSICloseDir);
172 16 : if (!psDirX)
173 0 : return false;
174 41 : while (const VSIDIREntry *psXEntry = VSIGetNextDirEntry(psDirX.get()))
175 : {
176 : const std::string osXDir(CPLFormFilenameSafe(
177 25 : osZoomDir.c_str(), psXEntry->pszName, nullptr));
178 25 : const int nX = atoi(psXEntry->pszName);
179 : std::unique_ptr<VSIDIR, decltype(&VSICloseDir)> psDirY(
180 25 : VSIOpenDir(osXDir.c_str(), 0, nullptr), VSICloseDir);
181 25 : if (!psDirY)
182 0 : return false;
183 : while (const VSIDIREntry *psYEntry =
184 72 : VSIGetNextDirEntry(psDirY.get()))
185 : {
186 47 : const int nY = atoi(psYEntry->pszName);
187 : uint64_t nTileId;
188 : try
189 : {
190 47 : nTileId = pmtiles::zxy_to_tileid(
191 : static_cast<uint8_t>(nZoom), nX, nY);
192 : }
193 0 : catch (const std::exception &e)
194 : {
195 0 : CPLError(CE_Failure, CPLE_AppDefined,
196 0 : "Cannot compute tile id: %s", e.what());
197 0 : return false;
198 : }
199 :
200 : const std::string osTileFilename(CPLFormFilenameSafe(
201 47 : osXDir.c_str(), psYEntry->pszName, nullptr));
202 :
203 : VSIStatBufL sStatBuf;
204 47 : if (VSIStatL(osTileFilename.c_str(), &sStatBuf) != 0)
205 : {
206 0 : CPLError(CE_Failure, CPLE_AppDefined,
207 : "Cannot stat file: %s", osTileFilename.c_str());
208 0 : return false;
209 : }
210 :
211 : // Arbitrary (but must not be larger than UINT32_MAX per PMTiles spec)
212 47 : constexpr uint32_t MAX_TILE_SIZE = 100 * 1024 * 1024;
213 47 : if (sStatBuf.st_size > MAX_TILE_SIZE)
214 : {
215 0 : CPLError(CE_Failure, CPLE_AppDefined, "Too large file: %s",
216 : osTileFilename.c_str());
217 0 : return false;
218 : }
219 :
220 47 : const uint32_t nFileSize =
221 47 : static_cast<uint32_t>(sStatBuf.st_size);
222 :
223 47 : if (abyTileData.size() < nFileSize)
224 20 : abyTileData.resize(nFileSize);
225 :
226 : auto fp = VSIVirtualHandleUniquePtr(
227 47 : VSIFOpenL(osTileFilename.c_str(), "rb"));
228 47 : if (!fp)
229 : {
230 0 : CPLError(CE_Failure, CPLE_AppDefined, "Cannot open %s",
231 : osTileFilename.c_str());
232 0 : return false;
233 : }
234 47 : if (fp->Read(abyTileData.data(), nFileSize) != nFileSize)
235 : {
236 0 : return false;
237 : }
238 :
239 47 : oMapTileIdToFileSize[nTileId] = nFileSize;
240 :
241 : TileEntry sEntry;
242 47 : sEntry.nTileId = nTileId;
243 :
244 : CPLMD5Context md5context;
245 47 : CPLMD5Init(&md5context);
246 47 : CPLMD5Update(&md5context, abyTileData.data(), nFileSize);
247 47 : CPLMD5Final(&sEntry.abyMD5[0], &md5context);
248 : try
249 : {
250 47 : asTileEntries.push_back(sEntry);
251 : }
252 0 : catch (const std::exception &e)
253 : {
254 0 : CPLError(CE_Failure, CPLE_AppDefined,
255 : "Out of memory browsing through tiles: %s",
256 0 : e.what());
257 0 : return false;
258 : }
259 47 : }
260 25 : }
261 : }
262 :
263 : // Sort the tiles by ascending tile_id. This is a requirement to build
264 : // the PMTiles directories.
265 12 : std::sort(asTileEntries.begin(), asTileEntries.end(),
266 141 : [](const TileEntry &a, const TileEntry &b)
267 141 : { return a.nTileId < b.nTileId; });
268 :
269 : // Let's gather tile data in
270 : // a way that corresponds to the "clustered" mode, that is
271 : // "offsets are either contiguous with the previous offset+length, or
272 : // refer to a lesser offset, when writing with deduplication."
273 :
274 24 : std::vector<pmtiles::entryv3> asPMTilesEntries;
275 12 : uint64_t nFileOffset = 0;
276 : std::unordered_map<std::array<unsigned char, 16>,
277 : std::pair<uint64_t, uint32_t>,
278 : HashArray<unsigned char, 16>>
279 24 : oMapMD5ToOffsetLen;
280 : {
281 12 : uint64_t nLastTileId = 0;
282 12 : std::array<unsigned char, 16> abyLastMD5{0, 0, 0, 0, 0, 0, 0, 0,
283 : 0, 0, 0, 0, 0, 0, 0, 0};
284 59 : for (const auto &sEntry : asTileEntries)
285 : {
286 66 : if (sEntry.nTileId == nLastTileId + 1 &&
287 19 : sEntry.abyMD5 == abyLastMD5)
288 : {
289 : // If the tile id immediately follows the previous one and
290 : // has the same tile data, increase the run_length
291 0 : asPMTilesEntries.back().run_length++;
292 : }
293 : else
294 : {
295 47 : pmtiles::entryv3 sPMTilesEntry;
296 47 : sPMTilesEntry.tile_id = sEntry.nTileId;
297 47 : sPMTilesEntry.run_length = 1;
298 :
299 47 : auto oIter = oMapMD5ToOffsetLen.find(sEntry.abyMD5);
300 47 : if (oIter != oMapMD5ToOffsetLen.end())
301 : {
302 : // Point to previously written tile data if this content
303 : // has already been written
304 0 : sPMTilesEntry.offset = oIter->second.first;
305 0 : sPMTilesEntry.length = oIter->second.second;
306 : }
307 : else
308 : {
309 : const auto oIterToFileSize =
310 47 : oMapTileIdToFileSize.find(sEntry.nTileId);
311 47 : CPLAssert(oIterToFileSize != oMapTileIdToFileSize.end());
312 47 : const uint32_t nTileDataLength = oIterToFileSize->second;
313 :
314 47 : sPMTilesEntry.offset = nFileOffset;
315 47 : sPMTilesEntry.length = nTileDataLength;
316 :
317 47 : oMapMD5ToOffsetLen[sEntry.abyMD5] =
318 47 : std::pair<uint64_t, uint32_t>(nFileOffset,
319 47 : nTileDataLength);
320 :
321 47 : nFileOffset += nTileDataLength;
322 : }
323 :
324 47 : asPMTilesEntries.push_back(sPMTilesEntry);
325 :
326 47 : nLastTileId = sEntry.nTileId;
327 47 : abyLastMD5 = sEntry.abyMD5;
328 : }
329 : }
330 : }
331 :
332 12 : const CPLCompressor *psCompressor = CPLGetCompressor("gzip");
333 12 : assert(psCompressor);
334 24 : std::string osCompressed;
335 :
336 : struct compression_exception : std::exception
337 : {
338 0 : const char *what() const noexcept override
339 : {
340 0 : return "Compression failed";
341 : }
342 : };
343 :
344 24 : const auto oCompressFunc = [psCompressor,
345 : &osCompressed](const std::string &osBytes,
346 144 : uint8_t) -> std::string
347 : {
348 24 : osCompressed.resize(32 + osBytes.size() * 2);
349 24 : size_t nOutputSize = osCompressed.size();
350 24 : void *pOutputData = &osCompressed[0];
351 24 : if (!psCompressor->pfnFunc(osBytes.data(), osBytes.size(), &pOutputData,
352 : &nOutputSize, nullptr,
353 24 : psCompressor->user_data))
354 : {
355 0 : throw compression_exception();
356 : }
357 24 : osCompressed.resize(nOutputSize);
358 48 : return osCompressed;
359 12 : };
360 :
361 24 : std::string osCompressedMetadata;
362 :
363 24 : std::string osRootBytes;
364 24 : std::string osLeaveBytes;
365 : int nNumLeaves;
366 : try
367 : {
368 : osCompressedMetadata =
369 12 : oCompressFunc(osMetadata, pmtiles::COMPRESSION_GZIP);
370 :
371 : // Build the root and leave directories (one depth max)
372 12 : std::tie(osRootBytes, osLeaveBytes, nNumLeaves) =
373 24 : pmtiles::make_root_leaves(oCompressFunc, pmtiles::COMPRESSION_GZIP,
374 12 : asPMTilesEntries);
375 : }
376 0 : catch (const std::exception &e)
377 : {
378 0 : CPLError(CE_Failure, CPLE_AppDefined, "Cannot build directories: %s",
379 0 : e.what());
380 0 : return false;
381 : }
382 :
383 : // Finalize the header fields related to offsets and size of the
384 : // different parts of the file
385 12 : sHeader.root_dir_bytes = osRootBytes.size();
386 12 : sHeader.json_metadata_offset =
387 12 : sHeader.root_dir_offset + sHeader.root_dir_bytes;
388 12 : sHeader.json_metadata_bytes = osCompressedMetadata.size();
389 12 : sHeader.leaf_dirs_offset =
390 12 : sHeader.json_metadata_offset + sHeader.json_metadata_bytes;
391 12 : sHeader.leaf_dirs_bytes = osLeaveBytes.size();
392 12 : sHeader.tile_data_offset =
393 12 : sHeader.leaf_dirs_offset + sHeader.leaf_dirs_bytes;
394 12 : sHeader.tile_data_bytes = nFileOffset;
395 :
396 : // Number of tiles that are addressable in the PMTiles archive, that is
397 : // the number of tiles we would have if not deduplicating them
398 12 : sHeader.addressed_tiles_count = asTileEntries.size();
399 :
400 : // Number of tile entries in root and leave directories
401 : // ie entries whose run_length >= 1
402 12 : sHeader.tile_entries_count = asPMTilesEntries.size();
403 :
404 : // Number of distinct tile blobs
405 12 : sHeader.tile_contents_count = oMapMD5ToOffsetLen.size();
406 :
407 : // Now build the file!
408 24 : auto poFile = VSIVirtualHandleUniquePtr(VSIFOpenL(pszDestName, "wb"));
409 12 : if (!poFile)
410 : {
411 0 : CPLError(CE_Failure, CPLE_FileIO, "Cannot open %s for write",
412 : pszDestName);
413 0 : return false;
414 : }
415 24 : const auto osHeader = sHeader.serialize();
416 :
417 12 : if (poFile->Write(osHeader.data(), osHeader.size(), 1) != 1 ||
418 12 : poFile->Write(osRootBytes.data(), osRootBytes.size(), 1) != 1 ||
419 12 : poFile->Write(osCompressedMetadata.data(), osCompressedMetadata.size(),
420 24 : 1) != 1 ||
421 12 : (!osLeaveBytes.empty() &&
422 0 : poFile->Write(osLeaveBytes.data(), osLeaveBytes.size(), 1) != 1))
423 : {
424 0 : CPLError(CE_Failure, CPLE_FileIO, "Failed writing");
425 0 : return false;
426 : }
427 :
428 : // Copy tile content at end of the output file.
429 : {
430 12 : uint64_t nLastTileId = 0;
431 12 : uint64_t nFileOffset2 = 0;
432 12 : std::array<unsigned char, 16> abyLastMD5{0, 0, 0, 0, 0, 0, 0, 0,
433 : 0, 0, 0, 0, 0, 0, 0, 0};
434 : std::unordered_set<std::array<unsigned char, 16>,
435 : HashArray<unsigned char, 16>>
436 12 : oSetMD5;
437 59 : for (const auto &sEntry : asTileEntries)
438 : {
439 66 : if (sEntry.nTileId == nLastTileId + 1 &&
440 19 : sEntry.abyMD5 == abyLastMD5)
441 : {
442 : // If the tile id immediately follows the previous one and
443 : // has the same tile data, do nothing
444 : }
445 : else
446 : {
447 47 : auto oIter = oSetMD5.find(sEntry.abyMD5);
448 47 : if (oIter == oSetMD5.end())
449 : {
450 : int nZ, nX, nY;
451 : try
452 : {
453 : const auto sXYZ =
454 47 : pmtiles::tileid_to_zxy(sEntry.nTileId);
455 47 : nZ = sXYZ.z;
456 47 : nY = sXYZ.y;
457 47 : nX = sXYZ.x;
458 : }
459 0 : catch (const std::exception &e)
460 : {
461 : // shouldn't happen given previous checks
462 0 : CPLError(CE_Failure, CPLE_AppDefined,
463 0 : "Cannot compute xyz: %s", e.what());
464 0 : return false;
465 : }
466 :
467 : const std::string osZoomDir(CPLFormFilenameSafe(
468 47 : pszSrcDirectory, CPLSPrintf("%d", nZ), nullptr));
469 : const std::string osXDir(CPLFormFilenameSafe(
470 47 : osZoomDir.c_str(), CPLSPrintf("%d", nX), nullptr));
471 : const std::string osTileFilename(CPLFormFilenameSafe(
472 47 : osXDir.c_str(), CPLSPrintf("%d", nY), pszExt));
473 :
474 : const auto oIterToFileSize =
475 47 : oMapTileIdToFileSize.find(sEntry.nTileId);
476 47 : CPLAssert(oIterToFileSize != oMapTileIdToFileSize.end());
477 47 : const uint32_t nTileDataLength = oIterToFileSize->second;
478 :
479 : auto fp = VSIVirtualHandleUniquePtr(
480 47 : VSIFOpenL(osTileFilename.c_str(), "rb"));
481 47 : if (!fp)
482 : {
483 0 : CPLError(CE_Failure, CPLE_AppDefined, "Cannot open %s",
484 : osTileFilename.c_str());
485 0 : return false;
486 : }
487 47 : if (fp->Read(abyTileData.data(), nTileDataLength) !=
488 47 : nTileDataLength)
489 : {
490 0 : return false;
491 : }
492 :
493 47 : oSetMD5.insert(sEntry.abyMD5);
494 :
495 47 : if (poFile->Write(abyTileData.data(), nTileDataLength, 1) !=
496 : 1)
497 : {
498 0 : CPLError(CE_Failure, CPLE_FileIO, "Failed writing");
499 0 : return false;
500 : }
501 :
502 47 : nFileOffset2 += nTileDataLength;
503 : }
504 :
505 47 : nLastTileId = sEntry.nTileId;
506 47 : abyLastMD5 = sEntry.abyMD5;
507 : }
508 : }
509 :
510 12 : CPL_IGNORE_RET_VAL(nFileOffset2);
511 12 : CPLAssert(nFileOffset2 == nFileOffset);
512 : }
513 :
514 12 : if (poFile->Close() != 0)
515 0 : return false;
516 :
517 12 : return true;
518 : }
|