Line data Source code
1 : /******************************************************************************
2 : *
3 : * Project: OpenGIS Simple Features Reference Implementation
4 : * Purpose: Virtual file system /vsipmtiles/ PMTiles
5 : * Author: Even Rouault <even.rouault at spatialys.com>
6 : *
7 : ******************************************************************************
8 : * Copyright (c) 2023, Planet Labs
9 : *
10 : * Permission is hereby granted, free of charge, to any person obtaining a
11 : * copy of this software and associated documentation files (the "Software"),
12 : * to deal in the Software without restriction, including without limitation
13 : * the rights to use, copy, modify, merge, publish, distribute, sublicense,
14 : * and/or sell copies of the Software, and to permit persons to whom the
15 : * Software is furnished to do so, subject to the following conditions:
16 : *
17 : * The above copyright notice and this permission notice shall be included
18 : * in all copies or substantial portions of the Software.
19 : *
20 : * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS
21 : * OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
22 : * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL
23 : * THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
24 : * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING
25 : * FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER
26 : * DEALINGS IN THE SOFTWARE.
27 : ****************************************************************************/
28 :
29 : #include "cpl_vsi_virtual.h"
30 :
31 : #include "vsipmtiles.h"
32 : #include "ogr_pmtiles.h"
33 :
34 : #include "cpl_json.h"
35 :
36 : #include <set>
37 :
38 : #define ENDS_WITH_CI(a, b) \
39 : (strlen(a) >= strlen(b) && EQUAL(a + strlen(a) - strlen(b), b))
40 :
41 : constexpr const char *PMTILES_HEADER_JSON = "pmtiles_header.json";
42 : constexpr const char *METADATA_JSON = "metadata.json";
43 :
44 : /************************************************************************/
45 : /* VSIPMTilesFilesystemHandler */
46 : /************************************************************************/
47 :
48 : class VSIPMTilesFilesystemHandler final : public VSIFilesystemHandler
49 : {
50 : public:
51 1221 : VSIPMTilesFilesystemHandler() = default;
52 :
53 : VSIVirtualHandle *Open(const char *pszFilename, const char *pszAccess,
54 : bool bSetError, CSLConstList papszOptions) override;
55 : int Stat(const char *pszFilename, VSIStatBufL *pStatBuf,
56 : int nFlags) override;
57 : char **ReadDirEx(const char *pszDirname, int nMaxFiles) override;
58 : };
59 :
60 : /************************************************************************/
61 : /* VSIPMTilesGetTileExtension() */
62 : /************************************************************************/
63 :
64 36 : static const char *VSIPMTilesGetTileExtension(OGRPMTilesDataset *poDS)
65 : {
66 36 : const auto &sHeader = poDS->GetHeader();
67 36 : switch (sHeader.tile_type)
68 : {
69 0 : case pmtiles::TILETYPE_PNG:
70 0 : return ".png";
71 0 : case pmtiles::TILETYPE_JPEG:
72 0 : return ".jpg";
73 0 : case pmtiles::TILETYPE_WEBP:
74 0 : return ".webp";
75 36 : case pmtiles::TILETYPE_MVT:
76 36 : return ".mvt";
77 : }
78 0 : if (sHeader.tile_compression == pmtiles::COMPRESSION_GZIP)
79 0 : return ".bin.gz";
80 0 : if (sHeader.tile_compression == pmtiles::COMPRESSION_ZSTD)
81 0 : return ".bin.zstd";
82 0 : return ".bin";
83 : }
84 :
85 : /************************************************************************/
86 : /* VSIPMTilesGetPMTilesHeaderJson() */
87 : /************************************************************************/
88 :
89 4 : static std::string VSIPMTilesGetPMTilesHeaderJson(OGRPMTilesDataset *poDS)
90 : {
91 4 : const auto &sHeader = poDS->GetHeader();
92 8 : CPLJSONDocument oDoc;
93 8 : CPLJSONObject oRoot;
94 4 : oRoot.Set("root_dir_offset", sHeader.root_dir_offset);
95 4 : oRoot.Set("json_metadata_offset", sHeader.json_metadata_offset);
96 4 : oRoot.Set("json_metadata_bytes", sHeader.json_metadata_bytes);
97 4 : oRoot.Set("leaf_dirs_offset", sHeader.leaf_dirs_offset);
98 4 : oRoot.Set("leaf_dirs_bytes", sHeader.leaf_dirs_bytes);
99 4 : oRoot.Set("tile_data_offset", sHeader.tile_data_offset);
100 4 : oRoot.Set("tile_data_bytes", sHeader.tile_data_bytes);
101 4 : oRoot.Set("addressed_tiles_count", sHeader.addressed_tiles_count);
102 4 : oRoot.Set("tile_entries_count", sHeader.tile_entries_count);
103 4 : oRoot.Set("tile_contents_count", sHeader.tile_contents_count);
104 4 : oRoot.Set("clustered", sHeader.clustered);
105 4 : oRoot.Set("internal_compression", sHeader.internal_compression);
106 4 : oRoot.Set("internal_compression_str",
107 4 : OGRPMTilesDataset::GetCompression(sHeader.internal_compression));
108 4 : oRoot.Set("tile_compression", sHeader.tile_compression);
109 4 : oRoot.Set("tile_compression_str",
110 4 : OGRPMTilesDataset::GetCompression(sHeader.tile_compression));
111 4 : oRoot.Set("tile_type", sHeader.tile_type);
112 4 : oRoot.Set("tile_type_str", OGRPMTilesDataset::GetTileType(sHeader));
113 4 : oRoot.Set("min_zoom", sHeader.min_zoom);
114 4 : oRoot.Set("max_zoom", sHeader.max_zoom);
115 4 : oRoot.Set("min_lon_e7", sHeader.min_lon_e7);
116 4 : oRoot.Set("min_lon_e7_float", sHeader.min_lon_e7 / 10e6);
117 4 : oRoot.Set("min_lat_e7", sHeader.min_lat_e7);
118 4 : oRoot.Set("min_lat_e7_float", sHeader.min_lat_e7 / 10e6);
119 4 : oRoot.Set("max_lon_e7", sHeader.max_lon_e7);
120 4 : oRoot.Set("max_lon_e7_float", sHeader.max_lon_e7 / 10e6);
121 4 : oRoot.Set("max_lat_e7", sHeader.max_lat_e7);
122 4 : oRoot.Set("max_lat_e7_float", sHeader.max_lat_e7 / 10e6);
123 4 : oRoot.Set("center_zoom", sHeader.center_zoom);
124 4 : oRoot.Set("center_lon_e7", sHeader.center_lon_e7);
125 4 : oRoot.Set("center_lat_e7", sHeader.center_lat_e7);
126 4 : oDoc.SetRoot(oRoot);
127 8 : return oDoc.SaveAsString();
128 : }
129 :
130 : /************************************************************************/
131 : /* VSIPMTilesOpen() */
132 : /************************************************************************/
133 :
134 : static std::unique_ptr<OGRPMTilesDataset>
135 58 : VSIPMTilesOpen(const char *pszFilename, std::string &osSubfilename,
136 : int &nComponents, int &nZ, int &nX, int &nY)
137 : {
138 58 : if (!STARTS_WITH(pszFilename, "/vsipmtiles/"))
139 0 : return nullptr;
140 58 : pszFilename += strlen("/vsipmtiles/");
141 :
142 116 : std::string osFilename(pszFilename);
143 58 : if (!osFilename.empty() && osFilename.back() == '/')
144 0 : osFilename.resize(osFilename.size() - 1);
145 58 : pszFilename = osFilename.c_str();
146 :
147 58 : nZ = nX = nY = -1;
148 58 : nComponents = 0;
149 116 : std::string osPmtilesFilename;
150 :
151 58 : const char *pszPmtilesExt = strstr(pszFilename, ".pmtiles");
152 58 : if (!pszPmtilesExt)
153 1 : return nullptr;
154 :
155 114 : CPLStringList aosTokens;
156 : do
157 : {
158 57 : if (pszPmtilesExt[strlen(".pmtiles")] == '/')
159 : {
160 54 : const char *pszSubFile = pszPmtilesExt + strlen(".pmtiles/");
161 54 : osPmtilesFilename.assign(pszFilename, pszSubFile - pszFilename - 1);
162 54 : osSubfilename = pszPmtilesExt + strlen(".pmtiles/");
163 101 : if (osSubfilename == METADATA_JSON ||
164 47 : osSubfilename == PMTILES_HEADER_JSON)
165 : {
166 11 : break;
167 : }
168 : }
169 : else
170 : {
171 3 : osPmtilesFilename = pszFilename;
172 3 : osSubfilename.clear();
173 3 : break;
174 : }
175 :
176 43 : aosTokens = CSLTokenizeString2(osSubfilename.c_str(), "/", 0);
177 43 : nComponents = aosTokens.size();
178 43 : if (nComponents >= 4)
179 0 : return nullptr;
180 :
181 43 : if (CPLGetValueType(aosTokens[0]) != CPL_VALUE_INTEGER)
182 2 : return nullptr;
183 41 : nZ = atoi(aosTokens[0]);
184 41 : if (nComponents == 1)
185 3 : break;
186 :
187 38 : if (CPLGetValueType(aosTokens[1]) != CPL_VALUE_INTEGER)
188 1 : return nullptr;
189 37 : nX = atoi(aosTokens[1]);
190 37 : if (nComponents == 2)
191 4 : break;
192 :
193 33 : break;
194 : } while (false);
195 :
196 108 : GDALOpenInfo oOpenInfo(osPmtilesFilename.c_str(), GA_ReadOnly);
197 108 : CPLStringList aosOptions;
198 54 : aosOptions.SetNameValue("DECOMPRESS_TILES", "NO");
199 54 : aosOptions.SetNameValue("ACCEPT_ANY_TILE_TYPE", "YES");
200 54 : oOpenInfo.papszOpenOptions = aosOptions.List();
201 108 : auto poDS = std::make_unique<OGRPMTilesDataset>();
202 : {
203 54 : CPLErrorHandlerPusher oErrorHandler(CPLQuietErrorHandler);
204 54 : if (!poDS->Open(&oOpenInfo))
205 4 : return nullptr;
206 : }
207 :
208 50 : if (nComponents == 3)
209 : {
210 33 : const char *pszTileExt = VSIPMTilesGetTileExtension(poDS.get());
211 33 : if (!ENDS_WITH_CI(aosTokens[2], pszTileExt))
212 2 : return nullptr;
213 31 : aosTokens[2][strlen(aosTokens[2]) - strlen(pszTileExt)] = 0;
214 31 : if (CPLGetValueType(aosTokens[2]) != CPL_VALUE_INTEGER)
215 0 : return nullptr;
216 31 : nY = atoi(aosTokens[2]);
217 : }
218 48 : return poDS;
219 : }
220 :
221 : /************************************************************************/
222 : /* Open() */
223 : /************************************************************************/
224 :
225 : VSIVirtualHandle *
226 23 : VSIPMTilesFilesystemHandler::Open(const char *pszFilename,
227 : const char *pszAccess, bool /*bSetError*/,
228 : CSLConstList /*papszOptions*/)
229 : {
230 23 : if (strchr(pszAccess, '+') || strchr(pszAccess, 'w') ||
231 22 : strchr(pszAccess, 'a'))
232 1 : return nullptr;
233 44 : std::string osSubfilename;
234 : int nComponents;
235 : int nZ;
236 : int nX;
237 : int nY;
238 : auto poDS =
239 44 : VSIPMTilesOpen(pszFilename, osSubfilename, nComponents, nZ, nX, nY);
240 22 : if (!poDS)
241 7 : return nullptr;
242 :
243 15 : if (osSubfilename == METADATA_JSON)
244 : {
245 4 : return VSIFileFromMemBuffer(nullptr,
246 2 : reinterpret_cast<GByte *>(CPLStrdup(
247 2 : poDS->GetMetadataContent().c_str())),
248 4 : poDS->GetMetadataContent().size(), true);
249 : }
250 :
251 13 : if (osSubfilename == PMTILES_HEADER_JSON)
252 : {
253 6 : const auto osStr = VSIPMTilesGetPMTilesHeaderJson(poDS.get());
254 6 : return VSIFileFromMemBuffer(
255 3 : nullptr, reinterpret_cast<GByte *>(CPLStrdup(osStr.c_str())),
256 6 : osStr.size(), true);
257 : }
258 :
259 10 : if (nComponents != 3)
260 3 : return nullptr;
261 :
262 14 : CPLErrorStateBackuper oBackuper(CPLQuietErrorHandler);
263 :
264 14 : OGRPMTilesTileIterator oIter(poDS.get(), nZ, nX, nY, nX, nY);
265 7 : auto sTile = oIter.GetNextTile();
266 7 : if (sTile.offset == 0)
267 1 : return nullptr;
268 :
269 6 : const auto *posStr = poDS->ReadTileData(sTile.offset, sTile.length);
270 6 : if (!posStr)
271 : {
272 0 : return nullptr;
273 : }
274 :
275 6 : GByte *pabyData = static_cast<GByte *>(CPLMalloc(posStr->size()));
276 6 : memcpy(pabyData, posStr->data(), posStr->size());
277 6 : return VSIFileFromMemBuffer(nullptr, pabyData, posStr->size(), true);
278 : }
279 :
280 : /************************************************************************/
281 : /* Stat() */
282 : /************************************************************************/
283 :
284 26 : int VSIPMTilesFilesystemHandler::Stat(const char *pszFilename,
285 : VSIStatBufL *pStatBuf, int /*nFlags*/)
286 : {
287 26 : memset(pStatBuf, 0, sizeof(VSIStatBufL));
288 :
289 52 : std::string osSubfilename;
290 : int nComponents;
291 : int nZ;
292 : int nX;
293 : int nY;
294 : auto poDS =
295 52 : VSIPMTilesOpen(pszFilename, osSubfilename, nComponents, nZ, nX, nY);
296 26 : if (!poDS)
297 0 : return -1;
298 :
299 26 : if (osSubfilename.empty())
300 0 : return -1;
301 :
302 : VSIStatBufL sStatPmtiles;
303 26 : if (VSIStatL(poDS->GetDescription(), &sStatPmtiles) == 0)
304 : {
305 26 : pStatBuf->st_mtime = sStatPmtiles.st_mtime;
306 : }
307 :
308 26 : if (osSubfilename == METADATA_JSON)
309 : {
310 2 : pStatBuf->st_mode = S_IFREG;
311 2 : pStatBuf->st_size = poDS->GetMetadataContent().size();
312 2 : return 0;
313 : }
314 :
315 24 : if (osSubfilename == PMTILES_HEADER_JSON)
316 : {
317 1 : pStatBuf->st_mode = S_IFREG;
318 1 : pStatBuf->st_size = VSIPMTilesGetPMTilesHeaderJson(poDS.get()).size();
319 1 : return 0;
320 : }
321 :
322 46 : CPLErrorStateBackuper oBackuper(CPLQuietErrorHandler);
323 :
324 46 : OGRPMTilesTileIterator oIter(poDS.get(), nZ, nX, nY, nX, nY);
325 23 : auto sTile = oIter.GetNextTile();
326 23 : if (sTile.offset == 0)
327 1 : return -1;
328 :
329 22 : if (nComponents <= 2)
330 : {
331 0 : pStatBuf->st_mode = S_IFDIR;
332 0 : return 0;
333 : }
334 :
335 22 : pStatBuf->st_mode = S_IFREG;
336 22 : pStatBuf->st_size = sTile.length;
337 22 : return 0;
338 : }
339 :
340 : /************************************************************************/
341 : /* ReadDirEx() */
342 : /************************************************************************/
343 :
344 10 : char **VSIPMTilesFilesystemHandler::ReadDirEx(const char *pszFilename,
345 : int nMaxFiles)
346 : {
347 20 : std::string osSubfilename;
348 : int nComponents;
349 : int nZ;
350 : int nX;
351 : int nY;
352 : auto poDS =
353 20 : VSIPMTilesOpen(pszFilename, osSubfilename, nComponents, nZ, nX, nY);
354 10 : if (!poDS)
355 3 : return nullptr;
356 :
357 7 : if (osSubfilename.empty())
358 : {
359 2 : CPLStringList aosFiles;
360 1 : aosFiles.AddString(PMTILES_HEADER_JSON);
361 1 : aosFiles.AddString(METADATA_JSON);
362 4 : for (int i = poDS->GetMinZoomLevel(); i <= poDS->GetMaxZoomLevel(); ++i)
363 : {
364 3 : OGRPMTilesTileIterator oIter(poDS.get(), i);
365 3 : auto sTile = oIter.GetNextTile();
366 3 : if (sTile.offset != 0)
367 : {
368 3 : if (nMaxFiles > 0 && aosFiles.size() >= nMaxFiles)
369 0 : break;
370 3 : aosFiles.AddString(CPLSPrintf("%d", i));
371 : }
372 : }
373 1 : return aosFiles.StealList();
374 : }
375 :
376 6 : if (nComponents == 1)
377 : {
378 4 : std::set<int> oSetX;
379 4 : OGRPMTilesTileIterator oIter(poDS.get(), nZ);
380 : while (true)
381 : {
382 4 : auto sTile = oIter.GetNextTile();
383 4 : if (sTile.offset == 0)
384 2 : break;
385 2 : oSetX.insert(sTile.x);
386 2 : if (nMaxFiles > 0 && static_cast<int>(oSetX.size()) >= nMaxFiles)
387 0 : break;
388 2 : if (oSetX.size() == 1024 * 1024)
389 : {
390 0 : CPLError(CE_Failure, CPLE_AppDefined, "Too many tiles");
391 0 : return nullptr;
392 : }
393 2 : }
394 4 : CPLStringList aosFiles;
395 4 : for (int x : oSetX)
396 : {
397 2 : aosFiles.AddString(CPLSPrintf("%d", x));
398 : }
399 2 : return aosFiles.StealList();
400 : }
401 :
402 4 : if (nComponents == 2)
403 : {
404 6 : std::set<int> oSetY;
405 6 : OGRPMTilesTileIterator oIter(poDS.get(), nZ, nX, -1, nX, -1);
406 : while (true)
407 : {
408 4 : auto sTile = oIter.GetNextTile();
409 4 : if (sTile.offset == 0)
410 3 : break;
411 1 : oSetY.insert(sTile.y);
412 1 : if (nMaxFiles > 0 && static_cast<int>(oSetY.size()) >= nMaxFiles)
413 0 : break;
414 1 : if (oSetY.size() == 1024 * 1024)
415 : {
416 0 : CPLError(CE_Failure, CPLE_AppDefined, "Too many tiles");
417 0 : return nullptr;
418 : }
419 1 : }
420 6 : CPLStringList aosFiles;
421 3 : const char *pszTileExt = VSIPMTilesGetTileExtension(poDS.get());
422 4 : for (int y : oSetY)
423 : {
424 1 : aosFiles.AddString(CPLSPrintf("%d%s", y, pszTileExt));
425 : }
426 3 : return aosFiles.StealList();
427 : }
428 :
429 1 : return nullptr;
430 : }
431 :
432 : /************************************************************************/
433 : /* VSIPMTilesRegister() */
434 : /************************************************************************/
435 :
436 1221 : void VSIPMTilesRegister()
437 : {
438 1221 : if (VSIFileManager::GetHandler("/vsipmtiles/") ==
439 1221 : VSIFileManager::GetHandler("/"))
440 : {
441 1221 : VSIFileManager::InstallHandler("/vsipmtiles/",
442 1221 : new VSIPMTilesFilesystemHandler());
443 : }
444 1221 : }
|