QGIS API Documentation 3.27.0-Master (95e00c50d2)
qgslazinfo.cpp
Go to the documentation of this file.
1/***************************************************************************
2 qgslazinfo.cpp
3 --------------------
4 begin : April 2022
5 copyright : (C) 2022 by Belgacem Nedjima
6 email : belgacem dot nedjima at gmail dot com
7 ***************************************************************************/
8
9/***************************************************************************
10 * *
11 * This program is free software; you can redistribute it and/or modify *
12 * it under the terms of the GNU General Public License as published by *
13 * the Free Software Foundation; either version 2 of the License, or *
14 * (at your option) any later version. *
15 * *
16 ***************************************************************************/
17
18#include "qgslazinfo.h"
19
20#include "qgslogger.h"
22
23#include "lazperf/readers.hpp"
24
25// QgsLazInfo
26
28
30{
31 if ( mVersion.first == 1 && mVersion.second == 4 )
32 return 375;
33 if ( mVersion.first == 1 && mVersion.second == 3 )
34 return 235;
35 if ( mVersion.first == 1 && mVersion.second <= 2 )
36 return 227;
37 return 0;
38}
39
40void QgsLazInfo::parseRawHeader( char *data, uint64_t length )
41{
42 mIsValid = true;
43 if ( std::string( data, 4 ) != "LASF" )
44 {
45 mError = QStringLiteral( "Supplied header is not from a LAZ file" );
46 mIsValid = false;
47 return;
48 }
49 std::istringstream file( std::string( data, length ) );
50 lazperf::header14 header = lazperf::header14::create( file );
51 parseHeader( header );
52}
53
54void QgsLazInfo::parseRawVlrEntries( char *data, uint64_t length )
55{
56 if ( !mIsValid )
57 return;
58 uint64_t currentOffset = 0;
59 for ( uint64_t i = 0; i < ( uint64_t )mVlrCount && currentOffset < length; ++i )
60 {
61 lazperf::vlr_header vlrHeader;
62 vlrHeader.fill( data + currentOffset, 54 );
63
64 LazVlr vlr;
65 vlr.userId = QString::fromStdString( vlrHeader.user_id );
66 vlr.recordId = vlrHeader.record_id;
67 vlr.data = QByteArray( data + currentOffset + 54, vlrHeader.data_length );
68 mVlrVector.push_back( vlr );
69 currentOffset += 54 + vlrHeader.data_length;
70 }
71
72 parseCrs();
73 parseExtrabyteAttributes();
74}
75
76
77void QgsLazInfo::parseHeader( lazperf::header14 &header )
78{
79 mHeader = header;
80
81 mScale = QgsVector3D( header.scale.x, header.scale.y, header.scale.z );
82 mOffset = QgsVector3D( header.offset.x, header.offset.y, header.offset.z );
83 mCreationYearDay = QPair<uint16_t, uint16_t>( header.creation.year, header.creation.day );
84 mVersion = QPair<uint8_t, uint8_t>( header.version.major, header.version.minor );
85 mPointFormat = header.pointFormat();
86
87 mProjectId = QString( QByteArray( header.guid, 16 ).toHex() );
88 mSystemId = QString::fromLocal8Bit( header.system_identifier, 32 );
89 while ( !mSystemId.isEmpty() && mSystemId.back() == '\0' )
90 {
91 mSystemId.remove( mSystemId.size() - 1, 1 );
92 }
93 mSoftwareId = QString::fromLocal8Bit( header.generating_software, 32 ).trimmed();
94 while ( !mSoftwareId.isEmpty() && mSoftwareId.back() == '\0' )
95 {
96 mSoftwareId.remove( mSoftwareId.size() - 1, 1 );
97 }
98
99 mMinCoords = QgsVector3D( header.minx, header.miny, header.minz );
100 mMaxCoords = QgsVector3D( header.maxx, header.maxy, header.maxz );
101
102 mVlrCount = header.vlr_count;
103
104 parseLazAttributes();
105}
106
107
108void QgsLazInfo::parseCrs()
109{
110 // TODO: handle other kind of CRS in the laz spec
111 for ( LazVlr &vlr : mVlrVector )
112 {
113 if ( vlr.userId.trimmed() == QLatin1String( "LASF_Projection" ) && vlr.recordId == 2112 )
114 {
115 mCrs = QgsCoordinateReferenceSystem::fromWkt( QString::fromStdString( vlr.data.toStdString() ) );
116 break;
117 }
118 }
119}
120
121QVariantMap QgsLazInfo::toMetadata() const
122{
123 QVariantMap metadata;
124 metadata[ QStringLiteral( "creation_year" ) ] = mHeader.creation.year;
125 metadata[ QStringLiteral( "creation_day" ) ] = mHeader.creation.day;
126 metadata[ QStringLiteral( "major_version" ) ] = mHeader.version.major;
127 metadata[ QStringLiteral( "minor_version" ) ] = mHeader.version.minor;
128 metadata[ QStringLiteral( "dataformat_id" ) ] = mHeader.pointFormat();
129 metadata[ QStringLiteral( "scale_x" ) ] = mScale.x();
130 metadata[ QStringLiteral( "scale_y" ) ] = mScale.y();
131 metadata[ QStringLiteral( "scale_z" ) ] = mScale.z();
132 metadata[ QStringLiteral( "offset_x" ) ] = mOffset.x();
133 metadata[ QStringLiteral( "offset_y" ) ] = mOffset.y();
134 metadata[ QStringLiteral( "offset_z" ) ] = mOffset.z();
135 metadata[ QStringLiteral( "project_id" ) ] = QString( QByteArray( mHeader.guid, 16 ).toHex() );
136 metadata[ QStringLiteral( "system_id" ) ] = QString::fromLocal8Bit( mHeader.system_identifier, 32 );
137 metadata[ QStringLiteral( "software_id" ) ] = QString::fromLocal8Bit( mHeader.generating_software, 32 );
138 return metadata;
139}
140
141QByteArray QgsLazInfo::vlrData( QString userId, int recordId )
142{
143 for ( LazVlr vlr : mVlrVector )
144 {
145 if ( vlr.userId == userId && vlr.recordId == recordId )
146 {
147 return vlr.data;
148 }
149 }
150 return QByteArray();
151}
152
153void QgsLazInfo::parseLazAttributes()
154{
155 if ( mPointFormat < 0 || mPointFormat > 10 )
156 {
157 QgsDebugMsgLevel( QStringLiteral( "Invalid point record format %1" ).arg( mPointFormat ), 2 );
158 return;
159 }
164 mAttributes.push_back( QgsPointCloudAttribute( "ReturnNumber", QgsPointCloudAttribute::Char ) );
165 mAttributes.push_back( QgsPointCloudAttribute( "NumberOfReturns", QgsPointCloudAttribute::Char ) );
166 mAttributes.push_back( QgsPointCloudAttribute( "ScanDirectionFlag", QgsPointCloudAttribute::Char ) );
167 mAttributes.push_back( QgsPointCloudAttribute( "EdgeOfFlightLine", QgsPointCloudAttribute::Char ) );
168 mAttributes.push_back( QgsPointCloudAttribute( "Classification", QgsPointCloudAttribute::UChar ) );
169 mAttributes.push_back( QgsPointCloudAttribute( "ScanAngleRank", QgsPointCloudAttribute::Short ) );
171 mAttributes.push_back( QgsPointCloudAttribute( "PointSourceId", QgsPointCloudAttribute::UShort ) );
172
173 if ( mPointFormat == 6 || mPointFormat == 7 || mPointFormat == 8 || mPointFormat == 9 || mPointFormat == 10 )
174 {
175 mAttributes.push_back( QgsPointCloudAttribute( "ScannerChannel", QgsPointCloudAttribute::Char ) );
176 mAttributes.push_back( QgsPointCloudAttribute( "ClassificationFlags", QgsPointCloudAttribute::Char ) );
177 }
178 if ( mPointFormat != 0 && mPointFormat != 2 )
179 {
181 }
182 if ( mPointFormat == 2 || mPointFormat == 3 || mPointFormat == 5 || mPointFormat == 7 || mPointFormat == 8 || mPointFormat == 10 )
183 {
184 mAttributes.push_back( QgsPointCloudAttribute( QStringLiteral( "Red" ), QgsPointCloudAttribute::UShort ) );
185 mAttributes.push_back( QgsPointCloudAttribute( QStringLiteral( "Green" ), QgsPointCloudAttribute::UShort ) );
186 mAttributes.push_back( QgsPointCloudAttribute( QStringLiteral( "Blue" ), QgsPointCloudAttribute::UShort ) );
187 }
188 if ( mPointFormat == 8 || mPointFormat == 10 )
189 {
190 mAttributes.push_back( QgsPointCloudAttribute( QStringLiteral( "Infrared" ), QgsPointCloudAttribute::UShort ) );
191 }
192 // Note: wave packet attributes are not handled and are unreadable
193}
194
195void QgsLazInfo::parseExtrabyteAttributes()
196{
197 QByteArray ebVlrRaw = vlrData( "LASF_Spec", 4 );
198 mExtrabyteAttributes = QgsLazInfo::parseExtrabytes( ebVlrRaw.data(), ebVlrRaw.size(), mHeader.point_record_length );
199
200 for ( QgsLazInfo::ExtraBytesAttributeDetails attr : mExtrabyteAttributes )
201 {
202 mAttributes.push_back( QgsPointCloudAttribute( attr.attribute, attr.type ) );
203 }
204}
205
206QVector<QgsLazInfo::ExtraBytesAttributeDetails> QgsLazInfo::parseExtrabytes( char *rawData, int length, int pointRecordLength )
207{
208 QVector<QgsLazInfo::ExtraBytesAttributeDetails> extrabyteAttributes;
209 lazperf::eb_vlr ebVlr;
210 ebVlr.fill( rawData, length );
211 for ( std::vector<lazperf::eb_vlr::ebfield>::reverse_iterator it = ebVlr.items.rbegin(); it != ebVlr.items.rend(); ++it )
212 {
213 lazperf::eb_vlr::ebfield &field = *it;
215 ebAtrr.attribute = QString::fromStdString( field.name );
216 switch ( field.data_type )
217 {
218 case 0:
220 ebAtrr.size = field.options;
221 break;
222 case 1:
224 ebAtrr.size = 1;
225 break;
226 case 2:
228 ebAtrr.size = 1;
229 break;
230 case 3:
232 ebAtrr.size = 2;
233 break;
234 case 4:
236 ebAtrr.size = 2;
237 break;
238 case 5:
240 ebAtrr.size = 4;
241 break;
242 case 6:
244 ebAtrr.size = 4;
245 break;
246 case 7:
248 ebAtrr.size = 8;
249 break;
250 case 8:
252 ebAtrr.size = 8;
253 break;
254 case 9:
256 ebAtrr.size = 4;
257 break;
258 case 10:
260 ebAtrr.size = 8;
261 break;
262 default:
264 ebAtrr.size = field.options;
265 break;
266 }
267 int accOffset = ( extrabyteAttributes.empty() ? pointRecordLength : extrabyteAttributes.back().offset ) - ebAtrr.size;
268 ebAtrr.offset = accOffset;
269 extrabyteAttributes.push_back( ebAtrr );
270 }
271 return extrabyteAttributes;
272}
273
274QgsLazInfo QgsLazInfo::fromFile( std::ifstream &file )
275{
276 QgsLazInfo lazInfo;
277
278 char headerRawData[ 375 ];
279 file.seekg( 0 );
280 file.read( headerRawData, 375 );
281 lazInfo.parseRawHeader( headerRawData, 375 );
282
283 int vlrDataSize = lazInfo.firstPointRecordOffset() - lazInfo.firstVariableLengthRecord();
284 std::unique_ptr<char[]> vlrEntriesRawData( new char[ vlrDataSize ] );
285 file.seekg( lazInfo.firstVariableLengthRecord() );
286 file.read( vlrEntriesRawData.get(), vlrDataSize );
287 lazInfo.parseRawVlrEntries( vlrEntriesRawData.get(), vlrDataSize );
288
289 return lazInfo;
290}
291
293{
294 QgsLazInfo lazInfo;
295
296 if ( !supportsRangeQueries( url ) )
297 {
298 lazInfo.mError = QStringLiteral( "The server of submitted URL doesn't support range queries" );
299 return lazInfo;
300 }
301
302 // Fetch header data
303 {
304 QNetworkRequest nr( url );
305 nr.setAttribute( QNetworkRequest::CacheLoadControlAttribute, QNetworkRequest::AlwaysNetwork );
306 nr.setAttribute( QNetworkRequest::CacheSaveControlAttribute, false );
307 nr.setRawHeader( "Range", "bytes=0-374" );
309 QgsBlockingNetworkRequest::ErrorCode errCode = req.get( nr );
310 if ( errCode != QgsBlockingNetworkRequest::NoError )
311 {
312 QgsDebugMsg( QStringLiteral( "Request failed: " ) + url.toString() );
313 lazInfo.mError = QStringLiteral( "Range query 0-374 to \"%1\" failed: \"%2\"" ).arg( url.toString() ).arg( req.errorMessage() );
314 return lazInfo;
315 }
316
317 const QgsNetworkReplyContent reply = req.reply();
318 QByteArray lazHeaderData = reply.content();
319
320 lazInfo.parseRawHeader( lazHeaderData.data(), lazHeaderData.size() );
321 }
322
323 // Fetch VLR data
324 {
325 QNetworkRequest nr( url );
326 nr.setAttribute( QNetworkRequest::CacheLoadControlAttribute, QNetworkRequest::AlwaysNetwork );
327 nr.setAttribute( QNetworkRequest::CacheSaveControlAttribute, false );
328 uint32_t firstVlrOffset = lazInfo.firstVariableLengthRecord();
329 QByteArray vlrRequestRange = QStringLiteral( "bytes=%1-%2" ).arg( firstVlrOffset ).arg( lazInfo.firstPointRecordOffset() - 1 ).toLocal8Bit();
330 nr.setRawHeader( "Range", vlrRequestRange );
332 QgsBlockingNetworkRequest::ErrorCode errCode = req.get( nr );
333 if ( errCode != QgsBlockingNetworkRequest::NoError )
334 {
335 QgsDebugMsg( QStringLiteral( "Request failed: " ) + url.toString() );
336
337 lazInfo.mError = QStringLiteral( "Range query %1-%2 to \"%3\" failed: \"%4\"" ).arg( firstVlrOffset ).arg( lazInfo.firstPointRecordOffset() - 1 )
338 .arg( url.toString() ).arg( req.errorMessage() );
339 return lazInfo;
340 }
341 QByteArray vlrDataRaw = req.reply().content();
342
343 lazInfo.parseRawVlrEntries( vlrDataRaw.data(), vlrDataRaw.size() );
344 }
345
346 return lazInfo;
347}
348
350{
351 QNetworkRequest nr( url );
352 nr.setAttribute( QNetworkRequest::CacheLoadControlAttribute, QNetworkRequest::AlwaysNetwork );
353 nr.setAttribute( QNetworkRequest::CacheSaveControlAttribute, false );
354 nr.setRawHeader( "Range", "bytes=0-0" );
356 // ignore the reply's status, we only care if accept-ranges is in the headers
357 req.head( nr );
358 QgsNetworkReplyContent reply = req.reply();
359
360 const QString acceptRangesHeader = reply.rawHeader( QStringLiteral( "Accept-Ranges" ).toLocal8Bit() );
361 return acceptRangesHeader.compare( QStringLiteral( "bytes" ), Qt::CaseSensitivity::CaseInsensitive ) == 0;
362}
A thread safe class for performing blocking (sync) network requests, with full support for QGIS proxy...
ErrorCode get(QNetworkRequest &request, bool forceRefresh=false, QgsFeedback *feedback=nullptr)
Performs a "get" operation on the specified request.
ErrorCode head(QNetworkRequest &request, bool forceRefresh=false, QgsFeedback *feedback=nullptr)
Performs a "head" operation on the specified request.
QString errorMessage() const
Returns the error message string, after a get(), post(), head() or put() request has been made.
@ NoError
No error was encountered.
QgsNetworkReplyContent reply() const
Returns the content of the network reply, after a get(), post(), head() or put() request has been mad...
static QgsCoordinateReferenceSystem fromWkt(const QString &wkt)
Creates a CRS from a WKT spatial ref sys definition string.
QString name
Definition: qgsfield.h:60
Class for extracting information contained in LAZ file such as the public header block and variable l...
Definition: qgslazinfo.h:39
uint32_t firstVariableLengthRecord() const
Returns the absolute offset to the first variable length record in the LAZ file.
Definition: qgslazinfo.cpp:29
QByteArray vlrData(QString userId, int recordId)
Returns the binary data of the variable length record with the user identifier userId and record iden...
Definition: qgslazinfo.cpp:141
uint32_t firstPointRecordOffset() const
Returns the absolute offset to the first point record in the LAZ file.
Definition: qgslazinfo.h:97
static QgsLazInfo fromUrl(QUrl &url)
Static function to create a QgsLazInfo class from a file over network.
Definition: qgslazinfo.cpp:292
lazperf::header14 header() const
Returns the LAZPERF header object.
Definition: qgslazinfo.h:127
void parseRawVlrEntries(char *data, uint64_t length)
Parses the variable length records found in the array data of length length.
Definition: qgslazinfo.cpp:54
QVariantMap toMetadata() const
Returns a map containing various metadata extracted from the LAZ file.
Definition: qgslazinfo.cpp:121
void parseRawHeader(char *data, uint64_t length)
Parses the raw header data loaded from a LAZ file.
Definition: qgslazinfo.cpp:40
static QgsLazInfo fromFile(std::ifstream &file)
Static function to create a QgsLazInfo class from a file.
Definition: qgslazinfo.cpp:274
static bool supportsRangeQueries(QUrl &url)
Static function to check whether the server of URL url supports range queries.
Definition: qgslazinfo.cpp:349
QgsLazInfo()
Constructor for an empty laz info parser.
Definition: qgslazinfo.cpp:27
int pointRecordLength() const
Returns the length of each point record in bytes.
Definition: qgslazinfo.h:101
static QVector< ExtraBytesAttributeDetails > parseExtrabytes(char *rawData, int length, int pointRecordLength)
Static function to parse the raw extrabytes VLR into a list of recognizable extrabyte attributes.
Definition: qgslazinfo.cpp:206
Encapsulates a network reply within a container which is inexpensive to copy and safe to pass between...
QByteArray content() const
Returns the reply content.
QByteArray rawHeader(const QByteArray &headerName) const
Returns the content of the header with the specified headerName, or an empty QByteArray if the specif...
void push_back(const QgsPointCloudAttribute &attribute)
Adds extra attribute.
Attribute for point cloud data pair of name and size in bytes.
@ UShort
Unsigned short int 2 bytes.
@ UChar
Unsigned char 1 byte.
@ UInt32
Unsigned int32 4 bytes.
@ UInt64
Unsigned int64 8 bytes.
double y() const
Returns Y coordinate.
Definition: qgsvector3d.h:51
double z() const
Returns Z coordinate.
Definition: qgsvector3d.h:53
double x() const
Returns X coordinate.
Definition: qgsvector3d.h:49
const QgsField & field
Definition: qgsfield.h:463
#define QgsDebugMsgLevel(str, level)
Definition: qgslogger.h:39
#define QgsDebugMsg(str)
Definition: qgslogger.h:38
QgsPointCloudAttribute::DataType type
Definition: qgslazinfo.h:51
QByteArray data
Definition: qgslazinfo.h:45