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