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