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