Line data Source code
1 : /******************************************************************************
2 : * Purpose: Racurs PHOTOMOD tiled format reader (http://www.racurs.ru)
3 : * Author: Andrew Sudorgin (drons [a] list dot ru)
4 : ******************************************************************************
5 : * Copyright (c) 2016, Andrew Sudorgin
6 : *
7 : * SPDX-License-Identifier: MIT
8 : ****************************************************************************/
9 :
10 : #include "cpl_minixml.h"
11 : #include "gdal.h"
12 : #include "gdal_frmts.h"
13 : #include "gdal_priv.h"
14 : #include "gdal_proxy.h"
15 : #include "../vrt/vrtdataset.h"
16 :
17 : enum ph_format
18 : {
19 : ph_megatiff,
20 : ph_xdem
21 : };
22 :
23 : #define PH_PRF_DRIVER "PRF"
24 : #define PH_PRF_EXT "prf"
25 : #define PH_DEM_EXT "x-dem"
26 : #define PH_GEOREF_SHIFT_Y (1.0)
27 :
28 : class PhPrfBand final : public VRTSourcedRasterBand
29 : {
30 : std::vector<GDALRasterBand *> osOverview{};
31 :
32 : public:
33 7 : PhPrfBand(GDALDataset *poDataset, int nBandCount, GDALDataType eType,
34 : int nXSize, int nYSize)
35 7 : : VRTSourcedRasterBand(poDataset, nBandCount, eType, nXSize, nYSize)
36 : {
37 7 : }
38 :
39 3 : void AddOverview(GDALRasterBand *ov)
40 : {
41 3 : osOverview.push_back(ov);
42 3 : }
43 :
44 : int GetOverviewCount() override;
45 :
46 1 : GDALRasterBand *GetOverview(int i) override
47 : {
48 1 : size_t n = static_cast<size_t>(i);
49 1 : if (n < osOverview.size())
50 : {
51 1 : return osOverview[n];
52 : }
53 : else
54 : {
55 0 : return VRTSourcedRasterBand::GetOverview(i);
56 : }
57 : }
58 : };
59 :
60 2 : int PhPrfBand::GetOverviewCount()
61 : {
62 2 : if (!osOverview.empty())
63 : {
64 1 : return static_cast<int>(osOverview.size());
65 : }
66 : else
67 : {
68 1 : return VRTSourcedRasterBand::GetOverviewCount();
69 : }
70 : }
71 :
72 : class PhPrfDataset final : public VRTDataset
73 : {
74 : std::vector<GDALDataset *> osSubTiles{};
75 :
76 : public:
77 : PhPrfDataset(GDALAccess eAccess, int nSizeX, int nSizeY, int nBandCount,
78 : GDALDataType eType, const char *pszName);
79 : ~PhPrfDataset() override;
80 : bool AddTile(const char *pszPartName, GDALAccess eAccess, int nWidth,
81 : int nHeight, int nOffsetX, int nOffsetY, int nScale);
82 : int CloseDependentDatasets() override;
83 : static int Identify(GDALOpenInfo *poOpenInfo);
84 : static GDALDataset *Open(GDALOpenInfo *poOpenInfo);
85 : };
86 :
87 7 : PhPrfDataset::PhPrfDataset(GDALAccess _eAccess, int nSizeX, int nSizeY,
88 : int nBandCount, GDALDataType eType,
89 7 : const char *pszName)
90 7 : : VRTDataset(nSizeX, nSizeY)
91 : {
92 7 : poDriver = GetGDALDriverManager()->GetDriverByName(PH_PRF_DRIVER);
93 7 : eAccess = _eAccess;
94 7 : SetWritable(FALSE); // Avoid rewrite of *.prf file with 'vrt' file
95 7 : SetDescription(pszName);
96 :
97 14 : for (int i = 0; i != nBandCount; ++i)
98 : {
99 7 : PhPrfBand *poBand = new PhPrfBand(this, i + 1, eType, nSizeX, nSizeY);
100 7 : SetBand(i + 1, poBand);
101 : }
102 7 : }
103 :
104 14 : PhPrfDataset::~PhPrfDataset()
105 : {
106 7 : PhPrfDataset::CloseDependentDatasets();
107 14 : }
108 :
109 39 : bool PhPrfDataset::AddTile(const char *pszPartName, GDALAccess eAccessType,
110 : int nWidth, int nHeight, int nOffsetX, int nOffsetY,
111 : int nScale)
112 : {
113 : GDALProxyPoolDataset *poTileDataset;
114 39 : poTileDataset = new GDALProxyPoolDataset(pszPartName, nWidth, nHeight,
115 39 : eAccessType, FALSE);
116 :
117 78 : for (int nBand = 1; nBand != GetRasterCount() + 1; ++nBand)
118 : {
119 39 : PhPrfBand *poBand = dynamic_cast<PhPrfBand *>(GetRasterBand(nBand));
120 :
121 39 : if (poBand == nullptr)
122 : {
123 0 : delete poTileDataset;
124 0 : return false;
125 : }
126 :
127 : // Block sizes (nBlockXSize&nBlockYSize) passed as zeros.
128 : // They will be loaded when RefUnderlyingRasterBand
129 : // function is called on first open of tile's dataset 'poTileDataset'.
130 39 : poTileDataset->AddSrcBandDescription(poBand->GetRasterDataType(), 0, 0);
131 39 : GDALRasterBand *poTileBand = poTileDataset->GetRasterBand(nBand);
132 :
133 39 : if (0 == nScale)
134 : {
135 36 : poBand->AddSimpleSource(poTileBand, 0, 0, nWidth, nHeight, nOffsetX,
136 : nOffsetY, nWidth, nHeight);
137 : }
138 : else
139 : {
140 3 : poBand->AddOverview(poTileBand);
141 : }
142 : }
143 :
144 39 : osSubTiles.push_back(poTileDataset);
145 :
146 39 : return true;
147 : }
148 :
149 7 : int PhPrfDataset::CloseDependentDatasets()
150 : {
151 7 : int bDroppedRef = VRTDataset::CloseDependentDatasets();
152 46 : for (std::vector<GDALDataset *>::iterator ii(osSubTiles.begin());
153 46 : ii != osSubTiles.end(); ++ii)
154 : {
155 39 : delete (*ii);
156 39 : bDroppedRef = TRUE;
157 : }
158 7 : osSubTiles.clear();
159 7 : return bDroppedRef;
160 : }
161 :
162 58281 : int PhPrfDataset::Identify(GDALOpenInfo *poOpenInfo)
163 : {
164 58281 : if (poOpenInfo->pabyHeader == nullptr || poOpenInfo->nHeaderBytes < 20)
165 : {
166 55152 : return FALSE;
167 : }
168 :
169 3129 : if (strstr(reinterpret_cast<char *>(poOpenInfo->pabyHeader), "phini") ==
170 : nullptr)
171 : {
172 3122 : return FALSE;
173 : }
174 :
175 7 : if (poOpenInfo->IsExtensionEqualToCI(PH_PRF_EXT))
176 : {
177 4 : return TRUE;
178 : }
179 3 : else if (poOpenInfo->IsExtensionEqualToCI(PH_DEM_EXT))
180 : {
181 3 : return TRUE;
182 : }
183 :
184 0 : return FALSE;
185 : }
186 :
187 745 : static void GetXmlNameValuePair(const CPLXMLNode *psElt, CPLString &osName,
188 : CPLString &osValue)
189 : {
190 2079 : for (const CPLXMLNode *psAttr = psElt->psChild; psAttr != nullptr;
191 1334 : psAttr = psAttr->psNext)
192 : {
193 1334 : if (psAttr->eType != CXT_Attribute || psAttr->pszValue == nullptr ||
194 819 : psAttr->psChild == nullptr || psAttr->psChild->pszValue == nullptr)
195 : {
196 515 : continue;
197 : }
198 819 : if (EQUAL(psAttr->pszValue, "n"))
199 : {
200 438 : osName = psAttr->psChild->pszValue;
201 : }
202 381 : else if (EQUAL(psAttr->pszValue, "v"))
203 : {
204 381 : osValue = psAttr->psChild->pszValue;
205 : }
206 : }
207 745 : }
208 :
209 39 : static CPLString GetXmlAttribute(const CPLXMLNode *psElt,
210 : const CPLString &osAttrName,
211 : const CPLString &osDef = CPLString())
212 : {
213 39 : for (const CPLXMLNode *psAttr = psElt->psChild; psAttr != nullptr;
214 0 : psAttr = psAttr->psNext)
215 : {
216 39 : if (psAttr->eType != CXT_Attribute || psAttr->pszValue == nullptr ||
217 39 : psAttr->psChild == nullptr || psAttr->psChild->pszValue == nullptr)
218 : {
219 0 : continue;
220 : }
221 39 : if (EQUAL(psAttr->pszValue, osAttrName))
222 : {
223 39 : return psAttr->psChild->pszValue;
224 : }
225 : }
226 0 : return osDef;
227 : }
228 :
229 4 : static bool ParseGeoref(const CPLXMLNode *psGeorefElt, GDALGeoTransform >)
230 : {
231 4 : bool abOk[6] = {false, false, false, false, false, false};
232 : static const char *const apszGeoKeys[6] = {"A_0", "A_1", "A_2",
233 : "B_0", "B_1", "B_2"};
234 32 : for (const CPLXMLNode *elt = psGeorefElt->psChild; elt != nullptr;
235 28 : elt = elt->psNext)
236 : {
237 56 : CPLString osName;
238 56 : CPLString osValue;
239 28 : GetXmlNameValuePair(elt, osName, osValue);
240 196 : for (int k = 0; k != 6; ++k)
241 : {
242 168 : if (EQUAL(osName, apszGeoKeys[k]))
243 : {
244 24 : gt[k] = CPLAtof(osValue);
245 24 : abOk[k] = true;
246 : }
247 : }
248 : }
249 :
250 24 : for (int k = 0; k != 6; ++k)
251 : {
252 24 : if (!abOk[k])
253 : {
254 0 : break;
255 : }
256 24 : if (k == 5)
257 : {
258 4 : gt[3] -= PH_GEOREF_SHIFT_Y * gt[4];
259 4 : gt[3] -= PH_GEOREF_SHIFT_Y * gt[5];
260 4 : return true;
261 : }
262 : }
263 0 : return false;
264 : }
265 :
266 3 : static bool ParseDemShift(const CPLXMLNode *psDemShiftElt, double *padfDemShift)
267 : {
268 3 : bool abOk[6] = {false, false, false, false, false, false};
269 : static const char *const apszDemShiftKeys[6] = {"x", "y", "z", "", "", ""};
270 :
271 15 : for (const CPLXMLNode *elt = psDemShiftElt->psChild; elt != nullptr;
272 12 : elt = elt->psNext)
273 : {
274 24 : CPLString osName;
275 24 : CPLString osValue;
276 12 : GetXmlNameValuePair(elt, osName, osValue);
277 48 : for (int k = 0; k != 3; ++k)
278 : {
279 36 : if (EQUAL(osName, apszDemShiftKeys[k]))
280 : {
281 9 : padfDemShift[k] = CPLAtof(osValue);
282 9 : abOk[k] = true;
283 : }
284 : }
285 : }
286 3 : return abOk[0] && abOk[1] && abOk[2];
287 : }
288 :
289 7 : static GDALDataType ParseChannelsInfo(const CPLXMLNode *psElt)
290 : {
291 14 : CPLString osType;
292 14 : CPLString osBytesPS;
293 14 : CPLString osChannels;
294 :
295 35 : for (const CPLXMLNode *psChild = psElt->psChild; psChild != nullptr;
296 28 : psChild = psChild->psNext)
297 : {
298 28 : if (psChild->eType != CXT_Element)
299 : {
300 7 : continue;
301 : }
302 :
303 42 : CPLString osName;
304 42 : CPLString osValue;
305 :
306 21 : GetXmlNameValuePair(psChild, osName, osValue);
307 :
308 21 : if (EQUAL(osName, "type"))
309 : {
310 7 : osType = std::move(osValue);
311 : }
312 14 : else if (EQUAL(osName, "bytes_ps"))
313 : {
314 7 : osBytesPS = std::move(osValue);
315 : }
316 7 : else if (EQUAL(osName, "channels"))
317 : {
318 7 : osChannels = std::move(osValue);
319 : }
320 : }
321 :
322 7 : const int nDataTypeSize = atoi(osBytesPS);
323 7 : if (osType == "U")
324 : {
325 4 : switch (nDataTypeSize)
326 : {
327 0 : case 1:
328 0 : return GDT_Byte;
329 4 : case 2:
330 4 : return GDT_UInt16;
331 0 : case 4:
332 0 : return GDT_UInt32;
333 0 : default:
334 0 : CPLError(CE_Failure, CPLE_OpenFailed,
335 : "Unsupported datatype size %d", nDataTypeSize);
336 0 : return GDT_Unknown;
337 : }
338 : }
339 3 : else if (osType == "F")
340 : {
341 3 : switch (nDataTypeSize)
342 : {
343 3 : case 4:
344 3 : return GDT_Float32;
345 0 : case 8:
346 0 : return GDT_Float64;
347 0 : default:
348 0 : CPLError(CE_Failure, CPLE_OpenFailed,
349 : "Unsupported datatype size %d", nDataTypeSize);
350 0 : return GDT_Unknown;
351 : }
352 : }
353 :
354 0 : return GDT_Unknown;
355 : }
356 :
357 7 : GDALDataset *PhPrfDataset::Open(GDALOpenInfo *poOpenInfo)
358 : {
359 : ph_format eFormat;
360 :
361 7 : if (poOpenInfo->IsExtensionEqualToCI(PH_PRF_EXT))
362 : {
363 4 : eFormat = ph_megatiff;
364 : }
365 3 : else if (poOpenInfo->IsExtensionEqualToCI(PH_DEM_EXT))
366 : {
367 3 : eFormat = ph_xdem;
368 : }
369 : else
370 : {
371 0 : return nullptr;
372 : }
373 :
374 14 : CPLXMLTreeCloser oDoc(CPLParseXMLFile(poOpenInfo->pszFilename));
375 :
376 7 : if (oDoc.get() == nullptr)
377 : {
378 0 : return nullptr;
379 : }
380 :
381 7 : const CPLXMLNode *psPhIni(CPLSearchXMLNode(oDoc.get(), "=phini"));
382 7 : if (psPhIni == nullptr)
383 : {
384 0 : return nullptr;
385 : }
386 :
387 7 : int nSizeX = 0;
388 7 : int nSizeY = 0;
389 7 : int nBandCount = 0;
390 7 : GDALDataType eResultDatatype = GDT_Unknown;
391 14 : CPLString osPartsBasePath(CPLGetPathSafe(poOpenInfo->pszFilename));
392 14 : CPLString osPartsPath(osPartsBasePath + "/" +
393 21 : CPLGetBasenameSafe(poOpenInfo->pszFilename));
394 14 : CPLString osPartsExt;
395 7 : GDALGeoTransform gt{0, 0, 0, 0, 0, 0};
396 7 : bool bGeoTransOk = false;
397 :
398 7 : double adfDemShift[3] = {0.0, 0.0, 0.0};
399 7 : bool bDemShiftOk = false;
400 7 : const int nDemMDCount = 7;
401 7 : bool abDemMetadataOk[nDemMDCount] = {false, false, false, false,
402 : false, false, false};
403 7 : double adfDemMetadata[nDemMDCount] = {0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0};
404 : static const char *const apszDemKeys[nDemMDCount] = {
405 : "XR_0", "XR_1", "YR_0", "YR_1", "ZR_0", "ZR_1", "BadZ"};
406 7 : if (eFormat == ph_megatiff)
407 : {
408 4 : osPartsExt = ".tif";
409 : }
410 3 : else if (eFormat == ph_xdem)
411 : {
412 3 : osPartsExt = ".demtif";
413 : }
414 :
415 187 : for (const CPLXMLNode *psElt = psPhIni->psChild; psElt != nullptr;
416 180 : psElt = psElt->psNext)
417 : {
418 180 : if (!EQUAL(psElt->pszValue, "s") || psElt->eType != CXT_Element)
419 : {
420 176 : continue;
421 : }
422 :
423 8 : CPLString osName;
424 8 : CPLString osValue;
425 :
426 4 : GetXmlNameValuePair(psElt, osName, osValue);
427 :
428 4 : if (EQUAL(osName, "parts_ext"))
429 : {
430 4 : osPartsExt = "." + osValue;
431 : }
432 : }
433 :
434 187 : for (const CPLXMLNode *psElt = psPhIni->psChild; psElt != nullptr;
435 180 : psElt = psElt->psNext)
436 : {
437 360 : CPLString osName;
438 360 : CPLString osValue;
439 :
440 180 : GetXmlNameValuePair(psElt, osName, osValue);
441 :
442 180 : if (EQUAL(osName, "ChannelsInfo"))
443 : {
444 7 : eResultDatatype = ParseChannelsInfo(psElt);
445 : }
446 173 : else if (EQUAL(osName, "Width"))
447 : {
448 7 : nSizeX = atoi(osValue);
449 : }
450 166 : else if (EQUAL(osName, "Height"))
451 : {
452 7 : nSizeY = atoi(osValue);
453 : }
454 159 : else if (EQUAL(osName, "QChans"))
455 : {
456 7 : nBandCount = atoi(osValue);
457 : }
458 152 : else if (EQUAL(osName, "GeoRef"))
459 : {
460 4 : bGeoTransOk = ParseGeoref(psElt, gt);
461 : }
462 148 : else if (EQUAL(osName, "DemShift"))
463 : {
464 3 : bDemShiftOk = ParseDemShift(psElt, adfDemShift);
465 : }
466 : else
467 : {
468 1160 : for (int n = 0; n != nDemMDCount; ++n)
469 : {
470 1015 : if (EQUAL(osName, apszDemKeys[n]))
471 : {
472 21 : adfDemMetadata[n] = CPLAtof(osValue);
473 21 : abDemMetadataOk[n] = true;
474 : }
475 : }
476 : }
477 : }
478 :
479 7 : if (eResultDatatype == GDT_Unknown)
480 : {
481 0 : CPLError(CE_Failure, CPLE_OpenFailed,
482 : "GDAL Dataset datatype not found");
483 0 : return nullptr;
484 : }
485 :
486 7 : if (nSizeX <= 0 || nSizeY <= 0 || nBandCount <= 0)
487 : {
488 0 : return nullptr;
489 : }
490 :
491 : PhPrfDataset *poDataset =
492 : new PhPrfDataset(GA_ReadOnly, nSizeX, nSizeY, nBandCount,
493 7 : eResultDatatype, poOpenInfo->pszFilename);
494 :
495 7 : if (!GDALCheckDatasetDimensions(poDataset->GetRasterXSize(),
496 : poDataset->GetRasterYSize()))
497 : {
498 0 : delete poDataset;
499 0 : return nullptr;
500 : }
501 :
502 187 : for (const CPLXMLNode *psElt = psPhIni->psChild; psElt != nullptr;
503 180 : psElt = psElt->psNext)
504 : {
505 180 : int nWidth = 0;
506 180 : int nHeight = 0;
507 180 : int nOffsetX = 0;
508 180 : int nOffsetY = 0;
509 180 : int nScale = 0;
510 :
511 680 : for (const CPLXMLNode *psItem = psElt->psChild; psItem != nullptr;
512 500 : psItem = psItem->psNext)
513 : {
514 1000 : CPLString osName;
515 1000 : CPLString osValue;
516 :
517 500 : GetXmlNameValuePair(psItem, osName, osValue);
518 :
519 500 : if (EQUAL(osName, "Width"))
520 : {
521 39 : nWidth = atoi(osValue);
522 : }
523 461 : else if (EQUAL(osName, "Height"))
524 : {
525 39 : nHeight = atoi(osValue);
526 : }
527 422 : else if (EQUAL(osName, "DispX"))
528 : {
529 36 : nOffsetX = atoi(osValue);
530 : }
531 386 : else if (EQUAL(osName, "DispY"))
532 : {
533 36 : nOffsetY = atoi(osValue);
534 : }
535 350 : else if (EQUAL(osName, "Scale"))
536 : {
537 3 : nScale = atoi(osValue);
538 : }
539 : }
540 :
541 180 : if (nWidth == 0 || nHeight == 0)
542 : {
543 141 : continue;
544 : }
545 :
546 78 : CPLString osPartName(osPartsPath + "/" + GetXmlAttribute(psElt, "n") +
547 39 : osPartsExt);
548 :
549 39 : if (!poDataset->AddTile(osPartName, GA_ReadOnly, nWidth, nHeight,
550 : nOffsetX, nOffsetY, nScale))
551 : {
552 0 : delete poDataset;
553 0 : return nullptr;
554 : }
555 : }
556 :
557 7 : if (eFormat == ph_megatiff && bGeoTransOk)
558 : {
559 4 : poDataset->SetGeoTransform(gt);
560 : }
561 :
562 7 : if (eFormat == ph_xdem)
563 : {
564 3 : GDALRasterBand *poFirstBand = poDataset->GetRasterBand(1);
565 :
566 3 : if (poFirstBand != nullptr)
567 : {
568 3 : poFirstBand->SetUnitType("m"); // Always meters.
569 : }
570 :
571 3 : if (abDemMetadataOk[0] && abDemMetadataOk[1] && abDemMetadataOk[2] &&
572 3 : abDemMetadataOk[3] && nSizeX > 1 && nSizeY > 1)
573 : {
574 3 : gt[0] = adfDemMetadata[0];
575 3 : gt[1] = (adfDemMetadata[1] - adfDemMetadata[0]) / (nSizeX - 1);
576 3 : gt[2] = 0;
577 3 : gt[3] = adfDemMetadata[3];
578 3 : gt[4] = 0;
579 3 : gt[5] = (adfDemMetadata[2] - adfDemMetadata[3]) / (nSizeY - 1);
580 :
581 3 : gt[0] -= 0.5 * gt[1];
582 3 : gt[3] -= 0.5 * gt[5];
583 :
584 3 : if (bDemShiftOk)
585 : {
586 3 : gt[0] += adfDemShift[0];
587 3 : gt[3] += adfDemShift[1];
588 : }
589 :
590 3 : poDataset->SetGeoTransform(gt);
591 : }
592 :
593 3 : if (abDemMetadataOk[4] && abDemMetadataOk[5])
594 : {
595 3 : poFirstBand->SetMetadataItem("STATISTICS_MINIMUM",
596 3 : CPLSPrintf("%f", adfDemMetadata[4]));
597 3 : poFirstBand->SetMetadataItem("STATISTICS_MAXIMUM",
598 3 : CPLSPrintf("%f", adfDemMetadata[5]));
599 : }
600 :
601 3 : if (abDemMetadataOk[6])
602 : {
603 3 : poFirstBand->SetNoDataValue(adfDemMetadata[6]);
604 : }
605 :
606 3 : if (bDemShiftOk)
607 : {
608 3 : poFirstBand->SetOffset(adfDemShift[2]);
609 : }
610 : }
611 :
612 : const std::string osPrj =
613 7 : CPLResetExtensionSafe(poOpenInfo->pszFilename, "prj");
614 7 : VSILFILE *const fp = VSIFOpenL(osPrj.c_str(), "rt");
615 7 : if (fp != nullptr)
616 : {
617 3 : const size_t nBufMax = 100000;
618 3 : char *const pszWKT = static_cast<char *>(CPLMalloc(nBufMax));
619 3 : const size_t nBytes = VSIFReadL(pszWKT, 1, nBufMax - 1, fp);
620 3 : VSIFCloseL(fp);
621 3 : if (nBytes > 0 && nBytes < nBufMax - 1)
622 : {
623 3 : auto poSRS = new OGRSpatialReference();
624 3 : poSRS->SetAxisMappingStrategy(OAMS_TRADITIONAL_GIS_ORDER);
625 3 : pszWKT[nBytes] = '\0';
626 3 : if (poSRS->importFromWkt(pszWKT) == OGRERR_NONE)
627 : {
628 3 : poDataset->SetSpatialRef(poSRS);
629 : }
630 3 : delete poSRS;
631 : }
632 3 : CPLFree(pszWKT);
633 : }
634 :
635 7 : return poDataset;
636 : }
637 :
638 2038 : void GDALRegister_PRF()
639 : {
640 2038 : if (GDALGetDriverByName(PH_PRF_DRIVER) != nullptr)
641 283 : return;
642 :
643 1755 : GDALDriver *poDriver = new GDALDriver;
644 :
645 1755 : poDriver->SetDescription(PH_PRF_DRIVER);
646 1755 : poDriver->SetMetadataItem(GDAL_DMD_LONGNAME, "Racurs PHOTOMOD PRF");
647 1755 : poDriver->SetMetadataItem(GDAL_DCAP_VIRTUALIO, "YES");
648 1755 : poDriver->SetMetadataItem(GDAL_DCAP_RASTER, "YES");
649 1755 : poDriver->SetMetadataItem(GDAL_DMD_EXTENSION, "prf");
650 1755 : poDriver->SetMetadataItem(GDAL_DMD_HELPTOPIC, "drivers/raster/prf.html");
651 1755 : poDriver->pfnIdentify = PhPrfDataset::Identify;
652 1755 : poDriver->pfnOpen = PhPrfDataset::Open;
653 1755 : GetGDALDriverManager()->RegisterDriver(poDriver);
654 : }
|