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