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 "ogr_pmtiles.h"
14 :
15 : #include "cpl_json.h"
16 :
17 : #include "mvtutils.h"
18 :
19 : #include <math.h>
20 :
21 : /************************************************************************/
22 : /* ~OGRPMTilesDataset() */
23 : /************************************************************************/
24 :
25 192 : OGRPMTilesDataset::~OGRPMTilesDataset()
26 : {
27 96 : if (!m_osMetadataFilename.empty())
28 92 : VSIUnlink(m_osMetadataFilename.c_str());
29 192 : }
30 :
31 : /************************************************************************/
32 : /* GetLayer() */
33 : /************************************************************************/
34 :
35 113 : OGRLayer *OGRPMTilesDataset::GetLayer(int iLayer)
36 :
37 : {
38 113 : if (iLayer < 0 || iLayer >= GetLayerCount())
39 10 : return nullptr;
40 103 : return m_apoLayers[iLayer].get();
41 : }
42 :
43 : /************************************************************************/
44 : /* LongLatToSphericalMercator() */
45 : /************************************************************************/
46 :
47 184 : static void LongLatToSphericalMercator(double *x, double *y)
48 : {
49 184 : double X = SPHERICAL_RADIUS * (*x) / 180 * M_PI;
50 184 : double Y = SPHERICAL_RADIUS * log(tan(M_PI / 4 + 0.5 * (*y) / 180 * M_PI));
51 184 : *x = X;
52 184 : *y = Y;
53 184 : }
54 :
55 : /************************************************************************/
56 : /* GetCompression() */
57 : /************************************************************************/
58 :
59 143 : /*static*/ const char *OGRPMTilesDataset::GetCompression(uint8_t nVal)
60 : {
61 143 : switch (nVal)
62 : {
63 6 : case pmtiles::COMPRESSION_UNKNOWN:
64 6 : return "unknown";
65 0 : case pmtiles::COMPRESSION_NONE:
66 0 : return "none";
67 137 : case pmtiles::COMPRESSION_GZIP:
68 137 : return "gzip";
69 0 : case pmtiles::COMPRESSION_BROTLI:
70 0 : return "brotli";
71 0 : case pmtiles::COMPRESSION_ZSTD:
72 0 : return "zstd";
73 0 : default:
74 0 : break;
75 : }
76 0 : return CPLSPrintf("invalid (%d)", nVal);
77 : }
78 :
79 : /************************************************************************/
80 : /* GetTileType() */
81 : /************************************************************************/
82 :
83 : /* static */
84 4 : const char *OGRPMTilesDataset::GetTileType(const pmtiles::headerv3 &sHeader)
85 : {
86 4 : switch (sHeader.tile_type)
87 : {
88 0 : case pmtiles::TILETYPE_UNKNOWN:
89 0 : return "unknown";
90 0 : case pmtiles::TILETYPE_PNG:
91 0 : return "PNG";
92 0 : case pmtiles::TILETYPE_JPEG:
93 0 : return "JPEG";
94 0 : case pmtiles::TILETYPE_WEBP:
95 0 : return "WEBP";
96 4 : case pmtiles::TILETYPE_MVT:
97 4 : return "MVT";
98 0 : default:
99 0 : break;
100 : }
101 0 : return CPLSPrintf("invalid (%d)", sHeader.tile_type);
102 : }
103 :
104 : /************************************************************************/
105 : /* Open() */
106 : /************************************************************************/
107 :
108 96 : bool OGRPMTilesDataset::Open(GDALOpenInfo *poOpenInfo)
109 : {
110 96 : if (!poOpenInfo->fpL || poOpenInfo->nHeaderBytes < 127)
111 2 : return false;
112 :
113 94 : SetDescription(poOpenInfo->pszFilename);
114 :
115 : // Borrow file handle
116 94 : m_poFile.reset(poOpenInfo->fpL);
117 94 : poOpenInfo->fpL = nullptr;
118 :
119 : // Deserizalize header
120 188 : std::string osHeader;
121 94 : osHeader.assign(reinterpret_cast<const char *>(poOpenInfo->pabyHeader),
122 94 : 127);
123 : try
124 : {
125 94 : m_sHeader = pmtiles::deserialize_header(osHeader);
126 : }
127 1 : catch (const std::exception &)
128 : {
129 1 : return false;
130 : }
131 :
132 : // Check tile type
133 93 : const bool bAcceptAnyTileType = CPLTestBool(CSLFetchNameValueDef(
134 93 : poOpenInfo->papszOpenOptions, "ACCEPT_ANY_TILE_TYPE", "NO"));
135 93 : if (bAcceptAnyTileType)
136 : {
137 : // do nothing. Internal use only by /vsipmtiles/
138 : }
139 42 : else if (m_sHeader.tile_type != pmtiles::TILETYPE_MVT)
140 : {
141 0 : CPLError(CE_Failure, CPLE_AppDefined,
142 : "Tile type %s not handled by the driver",
143 0 : GetTileType(m_sHeader));
144 0 : return false;
145 : }
146 :
147 : // Check compression method for metadata and directories
148 93 : CPLDebugOnly("PMTiles", "internal_compression = %s",
149 : GetCompression(m_sHeader.internal_compression));
150 :
151 93 : if (m_sHeader.internal_compression == pmtiles::COMPRESSION_GZIP)
152 : {
153 93 : m_psInternalDecompressor = CPLGetDecompressor("gzip");
154 : }
155 0 : else if (m_sHeader.internal_compression == pmtiles::COMPRESSION_ZSTD)
156 : {
157 0 : m_psInternalDecompressor = CPLGetDecompressor("zstd");
158 0 : if (m_psInternalDecompressor == nullptr)
159 : {
160 0 : CPLError(CE_Failure, CPLE_AppDefined,
161 : "File %s requires ZSTD decompression, but not available "
162 : "in this GDAL build",
163 : poOpenInfo->pszFilename);
164 0 : return false;
165 : }
166 : }
167 0 : else if (m_sHeader.internal_compression != pmtiles::COMPRESSION_NONE)
168 : {
169 0 : CPLError(CE_Failure, CPLE_AppDefined,
170 : "Unhandled internal_compression = %s",
171 0 : GetCompression(m_sHeader.internal_compression));
172 0 : return false;
173 : }
174 :
175 : // Check compression for tile data
176 93 : if (!CPLTestBool(CSLFetchNameValueDef(poOpenInfo->papszOpenOptions,
177 : "DECOMPRESS_TILES", "YES")))
178 : {
179 : // do nothing. Internal use only by /vsipmtiles/
180 : }
181 : else
182 : {
183 42 : CPLDebugOnly("PMTiles", "tile_compression = %s",
184 : GetCompression(m_sHeader.tile_compression));
185 :
186 42 : if (m_sHeader.tile_compression == pmtiles::COMPRESSION_UNKNOWN)
187 : {
188 : // Python pmtiles-convert generates this. The MVT driver can autodetect
189 : // uncompressed and GZip-compressed tiles automatically.
190 : }
191 38 : else if (m_sHeader.tile_compression == pmtiles::COMPRESSION_GZIP)
192 : {
193 38 : m_psTileDataDecompressor = CPLGetDecompressor("gzip");
194 : }
195 0 : else if (m_sHeader.tile_compression == pmtiles::COMPRESSION_ZSTD)
196 : {
197 0 : m_psTileDataDecompressor = CPLGetDecompressor("zstd");
198 0 : if (m_psTileDataDecompressor == nullptr)
199 : {
200 0 : CPLError(
201 : CE_Failure, CPLE_AppDefined,
202 : "File %s requires ZSTD decompression, but not available "
203 : "in this GDAL build",
204 : poOpenInfo->pszFilename);
205 0 : return false;
206 : }
207 : }
208 0 : else if (m_sHeader.tile_compression != pmtiles::COMPRESSION_NONE)
209 : {
210 0 : CPLError(CE_Failure, CPLE_AppDefined,
211 : "Unhandled tile_compression = %s",
212 0 : GetCompression(m_sHeader.tile_compression));
213 0 : return false;
214 : }
215 : }
216 :
217 : // Read metadata
218 : const auto *posMetadata =
219 93 : ReadInternal(m_sHeader.json_metadata_offset,
220 : m_sHeader.json_metadata_bytes, "metadata");
221 93 : if (!posMetadata)
222 1 : return false;
223 92 : CPLDebugOnly("PMTiles", "Metadata = %s", posMetadata->c_str());
224 92 : m_osMetadata = *posMetadata;
225 :
226 : m_osMetadataFilename =
227 92 : VSIMemGenerateHiddenFilename("pmtiles_metadata.json");
228 92 : VSIFCloseL(VSIFileFromMemBuffer(m_osMetadataFilename.c_str(),
229 92 : reinterpret_cast<GByte *>(&m_osMetadata[0]),
230 92 : m_osMetadata.size(), false));
231 :
232 184 : CPLJSONDocument oJsonDoc;
233 92 : if (!oJsonDoc.LoadMemory(m_osMetadata))
234 : {
235 0 : CPLError(CE_Failure, CPLE_AppDefined, "Cannot parse metadata");
236 0 : return false;
237 : }
238 :
239 184 : auto oJsonRoot = oJsonDoc.GetRoot();
240 1117 : for (const auto &oChild : oJsonRoot.GetChildren())
241 : {
242 1025 : if (oChild.GetType() == CPLJSONObject::Type::String)
243 : {
244 905 : if (oChild.GetName() == "json")
245 : {
246 : // Tippecanoe metadata includes a "json" item, which is a
247 : // serialized JSON object with vector_layers[] and layers[]
248 : // arrays we are interested in later.
249 : // so use "json" content as the new root
250 31 : if (!oJsonDoc.LoadMemory(oChild.ToString()))
251 : {
252 0 : CPLError(CE_Failure, CPLE_AppDefined,
253 : "Cannot parse 'json' metadata item");
254 0 : return false;
255 : }
256 31 : oJsonRoot = oJsonDoc.GetRoot();
257 : }
258 : // Tippecanoe generates a "strategies" member with serialized JSON
259 874 : else if (oChild.GetName() != "strategies")
260 : {
261 874 : SetMetadataItem(oChild.GetName().c_str(),
262 1748 : oChild.ToString().c_str());
263 : }
264 : }
265 : }
266 :
267 92 : double dfMinX = m_sHeader.min_lon_e7 / 10e6;
268 92 : double dfMinY = m_sHeader.min_lat_e7 / 10e6;
269 92 : double dfMaxX = m_sHeader.max_lon_e7 / 10e6;
270 92 : double dfMaxY = m_sHeader.max_lat_e7 / 10e6;
271 92 : LongLatToSphericalMercator(&dfMinX, &dfMinY);
272 92 : LongLatToSphericalMercator(&dfMaxX, &dfMaxY);
273 :
274 92 : m_nMinZoomLevel = m_sHeader.min_zoom;
275 92 : m_nMaxZoomLevel = m_sHeader.max_zoom;
276 92 : if (m_nMinZoomLevel > m_nMaxZoomLevel)
277 : {
278 1 : CPLError(CE_Failure, CPLE_AppDefined, "min_zoom(=%d) > max_zoom(=%d)",
279 : m_nMinZoomLevel, m_nMaxZoomLevel);
280 1 : return false;
281 : }
282 91 : if (m_nMinZoomLevel > 30)
283 : {
284 1 : CPLError(CE_Warning, CPLE_AppDefined, "Clamping min_zoom from %d to %d",
285 : m_nMinZoomLevel, 30);
286 1 : m_nMinZoomLevel = 30;
287 : }
288 91 : if (m_nMaxZoomLevel > 30)
289 : {
290 1 : CPLError(CE_Warning, CPLE_AppDefined, "Clamping max_zoom from %d to %d",
291 : m_nMaxZoomLevel, 30);
292 1 : m_nMaxZoomLevel = 30;
293 : }
294 :
295 91 : if (bAcceptAnyTileType)
296 50 : return true;
297 :
298 : // If using the pmtiles go utility, vector_layers and tilestats are
299 : // moved from Tippecanoe's json metadata item to the root element.
300 123 : CPLJSONArray oVectorLayers = oJsonRoot.GetArray("vector_layers");
301 41 : if (oVectorLayers.Size() == 0)
302 : {
303 5 : CPLError(CE_Failure, CPLE_AppDefined,
304 : "Missing vector_layers[] metadata");
305 5 : return false;
306 : }
307 :
308 108 : CPLJSONArray oTileStatLayers = oJsonRoot.GetArray("tilestats/layers");
309 :
310 : const int nZoomLevel =
311 36 : atoi(CSLFetchNameValueDef(poOpenInfo->papszOpenOptions, "ZOOM_LEVEL",
312 36 : CPLSPrintf("%d", m_nMaxZoomLevel)));
313 36 : if (nZoomLevel < m_nMinZoomLevel || nZoomLevel > m_nMaxZoomLevel)
314 : {
315 2 : CPLError(CE_Failure, CPLE_AppDefined,
316 : "Invalid zoom level. Should be in [%d,%d] range",
317 : m_nMinZoomLevel, m_nMaxZoomLevel);
318 2 : return false;
319 : }
320 34 : SetMetadataItem("ZOOM_LEVEL", CPLSPrintf("%d", nZoomLevel));
321 :
322 : m_osClipOpenOption =
323 34 : CSLFetchNameValueDef(poOpenInfo->papszOpenOptions, "CLIP", "");
324 :
325 68 : const bool bZoomLevelFromSpatialFilter = CPLFetchBool(
326 34 : poOpenInfo->papszOpenOptions, "ZOOM_LEVEL_AUTO",
327 34 : CPLTestBool(CPLGetConfigOption("MVT_ZOOM_LEVEL_AUTO", "NO")));
328 : const bool bJsonField =
329 34 : CPLFetchBool(poOpenInfo->papszOpenOptions, "JSON_FIELD", false);
330 :
331 75 : for (int i = 0; i < oVectorLayers.Size(); i++)
332 : {
333 123 : CPLJSONObject oId = oVectorLayers[i].GetObj("id");
334 41 : if (oId.IsValid() && oId.GetType() == CPLJSONObject::Type::String)
335 : {
336 41 : OGRwkbGeometryType eGeomType = wkbUnknown;
337 41 : if (oTileStatLayers.IsValid())
338 : {
339 39 : eGeomType = OGRMVTFindGeomTypeFromTileStat(
340 78 : oTileStatLayers, oId.ToString().c_str());
341 : }
342 41 : if (eGeomType == wkbUnknown)
343 : {
344 2 : eGeomType = OGRPMTilesVectorLayer::GuessGeometryType(
345 4 : this, oId.ToString().c_str(), nZoomLevel);
346 : }
347 :
348 123 : CPLJSONObject oFields = oVectorLayers[i].GetObj("fields");
349 : CPLJSONArray oAttributesFromTileStats =
350 : OGRMVTFindAttributesFromTileStat(oTileStatLayers,
351 82 : oId.ToString().c_str());
352 :
353 41 : m_apoLayers.push_back(std::make_unique<OGRPMTilesVectorLayer>(
354 82 : this, oId.ToString().c_str(), oFields, oAttributesFromTileStats,
355 : bJsonField, dfMinX, dfMinY, dfMaxX, dfMaxY, eGeomType,
356 : nZoomLevel, bZoomLevelFromSpatialFilter));
357 : }
358 : }
359 :
360 34 : return true;
361 : }
362 :
363 : /************************************************************************/
364 : /* Read() */
365 : /************************************************************************/
366 :
367 798 : const std::string *OGRPMTilesDataset::Read(const CPLCompressor *psDecompressor,
368 : uint64_t nOffset, uint64_t nSize,
369 : const char *pszDataType)
370 : {
371 798 : if (nSize > 10 * 1024 * 1024)
372 : {
373 0 : CPLError(CE_Failure, CPLE_AppDefined,
374 : "Too large amount of %s to read: " CPL_FRMT_GUIB
375 : " bytes at offset " CPL_FRMT_GUIB,
376 : pszDataType, static_cast<GUIntBig>(nSize),
377 : static_cast<GUIntBig>(nOffset));
378 0 : return nullptr;
379 : }
380 798 : m_osBuffer.resize(static_cast<size_t>(nSize));
381 798 : m_poFile->Seek(nOffset, SEEK_SET);
382 798 : if (m_poFile->Read(&m_osBuffer[0], m_osBuffer.size(), 1) != 1)
383 : {
384 1 : CPLError(CE_Failure, CPLE_AppDefined,
385 : "Cannot read %s of length %u at offset " CPL_FRMT_GUIB,
386 : pszDataType, unsigned(nSize), static_cast<GUIntBig>(nOffset));
387 1 : return nullptr;
388 : }
389 :
390 797 : if (psDecompressor)
391 : {
392 684 : m_osDecompressedBuffer.resize(32 + 16 * m_osBuffer.size());
393 684 : for (int iTry = 0; iTry < 2; ++iTry)
394 : {
395 684 : void *pOutputData = &m_osDecompressedBuffer[0];
396 684 : size_t nOutputSize = m_osDecompressedBuffer.size();
397 684 : if (!psDecompressor->pfnFunc(m_osBuffer.data(), m_osBuffer.size(),
398 : &pOutputData, &nOutputSize, nullptr,
399 684 : psDecompressor->user_data))
400 : {
401 0 : if (iTry == 0)
402 : {
403 0 : pOutputData = nullptr;
404 0 : nOutputSize = 0;
405 0 : if (psDecompressor->pfnFunc(
406 0 : m_osBuffer.data(), m_osBuffer.size(), &pOutputData,
407 0 : &nOutputSize, nullptr, psDecompressor->user_data))
408 : {
409 0 : CPLDebug("PMTiles",
410 : "Buffer of size %u uncompresses to %u bytes",
411 : unsigned(nSize), unsigned(nOutputSize));
412 0 : m_osDecompressedBuffer.resize(nOutputSize);
413 0 : continue;
414 : }
415 : }
416 :
417 0 : CPLError(CE_Failure, CPLE_AppDefined,
418 : "Cannot decompress %s of length %u at "
419 : "offset " CPL_FRMT_GUIB,
420 : pszDataType, unsigned(nSize),
421 : static_cast<GUIntBig>(nOffset));
422 0 : return nullptr;
423 : }
424 684 : m_osDecompressedBuffer.resize(nOutputSize);
425 684 : break;
426 : }
427 684 : return &m_osDecompressedBuffer;
428 : }
429 : else
430 : {
431 113 : return &m_osBuffer;
432 : }
433 : }
434 :
435 : /************************************************************************/
436 : /* ReadInternal() */
437 : /************************************************************************/
438 :
439 549 : const std::string *OGRPMTilesDataset::ReadInternal(uint64_t nOffset,
440 : uint64_t nSize,
441 : const char *pszDataType)
442 : {
443 549 : return Read(m_psInternalDecompressor, nOffset, nSize, pszDataType);
444 : }
445 :
446 : /************************************************************************/
447 : /* ReadTileData() */
448 : /************************************************************************/
449 :
450 249 : const std::string *OGRPMTilesDataset::ReadTileData(uint64_t nOffset,
451 : uint64_t nSize)
452 : {
453 249 : return Read(m_psTileDataDecompressor, nOffset, nSize, "tile data");
454 : }
|