QGIS API Documentation 3.99.0-Master (09f76ad7019)
Loading...
Searching...
No Matches
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 "lazperf/vlr.hpp"
22#include "qgslogger.h"
24
25#include <QString>
26
27using namespace Qt::StringLiterals;
28
29// QgsLazInfo
30
32
34{
35 if ( mVersion.first == 1 && mVersion.second == 4 )
36 return 375;
37 if ( mVersion.first == 1 && mVersion.second == 3 )
38 return 235;
39 if ( mVersion.first == 1 && mVersion.second <= 2 )
40 return 227;
41 return 0;
42}
43
44void QgsLazInfo::parseRawHeader( char *data, uint64_t length )
45{
46 mIsValid = true;
47 if ( std::string( data, 4 ) != "LASF" )
48 {
49 mError = u"Supplied header is not from a LAZ file"_s;
50 mIsValid = false;
51 return;
52 }
53 std::istringstream file( std::string( data, length ) );
54 lazperf::header14 header = lazperf::header14::create( file );
55 parseHeader( header );
56}
57
58void QgsLazInfo::parseRawVlrEntries( char *data, uint64_t length )
59{
60 if ( !mIsValid )
61 return;
62 uint64_t currentOffset = 0;
63 for ( uint64_t i = 0; i < ( uint64_t )mVlrCount && currentOffset < length; ++i )
64 {
65 lazperf::vlr_header vlrHeader;
66 vlrHeader.fill( data + currentOffset, 54 );
67
68 LazVlr vlr;
69 vlr.userId = QString::fromStdString( vlrHeader.user_id );
70 vlr.recordId = vlrHeader.record_id;
71 vlr.data = QByteArray( data + currentOffset + 54, vlrHeader.data_length );
72 mVlrVector.push_back( vlr );
73 currentOffset += 54 + vlrHeader.data_length;
74 }
75
76 parseCrs();
77 parseExtrabyteAttributes();
78}
79
80
81void QgsLazInfo::parseHeader( lazperf::header14 &header )
82{
83 mHeader = header;
84
85 mScale = QgsVector3D( header.scale.x, header.scale.y, header.scale.z );
86 mOffset = QgsVector3D( header.offset.x, header.offset.y, header.offset.z );
87 mCreationYearDay = QPair<uint16_t, uint16_t>( header.creation.year, header.creation.day );
88 mVersion = QPair<uint8_t, uint8_t>( header.version.major, header.version.minor );
89 mPointFormat = header.pointFormat();
90
91 mProjectId = QString( QByteArray( header.guid, 16 ).toHex() );
92 mSystemId = QString::fromLocal8Bit( header.system_identifier, 32 );
93 while ( !mSystemId.isEmpty() && mSystemId.back() == '\0' )
94 {
95 mSystemId.remove( mSystemId.size() - 1, 1 );
96 }
97 mSoftwareId = QString::fromLocal8Bit( header.generating_software, 32 ).trimmed();
98 while ( !mSoftwareId.isEmpty() && mSoftwareId.back() == '\0' )
99 {
100 mSoftwareId.remove( mSoftwareId.size() - 1, 1 );
101 }
102
103 mMinCoords = QgsVector3D( header.minx, header.miny, header.minz );
104 mMaxCoords = QgsVector3D( header.maxx, header.maxy, header.maxz );
105
106 mVlrCount = header.vlr_count;
107
108 parseLazAttributes();
109}
110
111
112void QgsLazInfo::parseCrs()
113{
114 // TODO: handle other kind of CRS in the laz spec
115 for ( LazVlr &vlr : mVlrVector )
116 {
117 if ( vlr.userId.trimmed() == "LASF_Projection"_L1 && vlr.recordId == 2112 )
118 {
119 mCrs = QgsCoordinateReferenceSystem::fromWkt( QString::fromStdString( vlr.data.toStdString() ) );
120 break;
121 }
122 }
123}
124
125QVariantMap QgsLazInfo::toMetadata() const
126{
127 QVariantMap metadata;
128 metadata[ u"creation_year"_s ] = mHeader.creation.year;
129 metadata[ u"creation_day"_s ] = mHeader.creation.day;
130 metadata[ u"major_version"_s ] = mHeader.version.major;
131 metadata[ u"minor_version"_s ] = mHeader.version.minor;
132 metadata[ u"dataformat_id"_s ] = mHeader.pointFormat();
133 metadata[ u"scale_x"_s ] = mScale.x();
134 metadata[ u"scale_y"_s ] = mScale.y();
135 metadata[ u"scale_z"_s ] = mScale.z();
136 metadata[ u"offset_x"_s ] = mOffset.x();
137 metadata[ u"offset_y"_s ] = mOffset.y();
138 metadata[ u"offset_z"_s ] = mOffset.z();
139 metadata[ u"project_id"_s ] = QString( QByteArray( mHeader.guid, 16 ).toHex() );
140 metadata[ u"system_id"_s ] = QString::fromLocal8Bit( mHeader.system_identifier, 32 );
141 metadata[ u"software_id"_s ] = QString::fromLocal8Bit( mHeader.generating_software, 32 );
142 return metadata;
143}
144
145QByteArray QgsLazInfo::vlrData( QString userId, int recordId )
146{
147 for ( LazVlr vlr : mVlrVector )
148 {
149 if ( vlr.userId == userId && vlr.recordId == recordId )
150 {
151 return vlr.data;
152 }
153 }
154 return QByteArray();
155}
156
157void QgsLazInfo::parseLazAttributes()
158{
159 if ( mPointFormat < 0 || mPointFormat > 10 )
160 {
161 QgsDebugMsgLevel( u"Invalid point record format %1"_s.arg( mPointFormat ), 2 );
162 return;
163 }
164 mAttributes.push_back( QgsPointCloudAttribute( "X", QgsPointCloudAttribute::Int32 ) );
165 mAttributes.push_back( QgsPointCloudAttribute( "Y", QgsPointCloudAttribute::Int32 ) );
166 mAttributes.push_back( QgsPointCloudAttribute( "Z", QgsPointCloudAttribute::Int32 ) );
167 mAttributes.push_back( QgsPointCloudAttribute( "Intensity", QgsPointCloudAttribute::UShort ) );
168 mAttributes.push_back( QgsPointCloudAttribute( "ReturnNumber", QgsPointCloudAttribute::Char ) );
169 mAttributes.push_back( QgsPointCloudAttribute( "NumberOfReturns", QgsPointCloudAttribute::Char ) );
170 mAttributes.push_back( QgsPointCloudAttribute( "ScanDirectionFlag", QgsPointCloudAttribute::Char ) );
171 mAttributes.push_back( QgsPointCloudAttribute( "EdgeOfFlightLine", QgsPointCloudAttribute::Char ) );
172 mAttributes.push_back( QgsPointCloudAttribute( "Classification", QgsPointCloudAttribute::UChar ) );
173 mAttributes.push_back( QgsPointCloudAttribute( "ScanAngleRank", QgsPointCloudAttribute::Float ) );
174 mAttributes.push_back( QgsPointCloudAttribute( "UserData", QgsPointCloudAttribute::UChar ) );
175 mAttributes.push_back( QgsPointCloudAttribute( "PointSourceId", QgsPointCloudAttribute::UShort ) );
176 mAttributes.push_back( QgsPointCloudAttribute( "Synthetic", QgsPointCloudAttribute::UChar ) );
177 mAttributes.push_back( QgsPointCloudAttribute( "KeyPoint", QgsPointCloudAttribute::UChar ) );
178 mAttributes.push_back( QgsPointCloudAttribute( "Withheld", QgsPointCloudAttribute::UChar ) );
179 mAttributes.push_back( QgsPointCloudAttribute( "Overlap", QgsPointCloudAttribute::UChar ) );
180
181 if ( mPointFormat == 6 || mPointFormat == 7 || mPointFormat == 8 || mPointFormat == 9 || mPointFormat == 10 )
182 {
183 mAttributes.push_back( QgsPointCloudAttribute( "ScannerChannel", QgsPointCloudAttribute::Char ) );
184 }
185 if ( mPointFormat != 0 && mPointFormat != 2 )
186 {
187 mAttributes.push_back( QgsPointCloudAttribute( "GpsTime", QgsPointCloudAttribute::Double ) );
188 }
189 if ( mPointFormat == 2 || mPointFormat == 3 || mPointFormat == 5 || mPointFormat == 7 || mPointFormat == 8 || mPointFormat == 10 )
190 {
191 mAttributes.push_back( QgsPointCloudAttribute( u"Red"_s, QgsPointCloudAttribute::UShort ) );
192 mAttributes.push_back( QgsPointCloudAttribute( u"Green"_s, QgsPointCloudAttribute::UShort ) );
193 mAttributes.push_back( QgsPointCloudAttribute( u"Blue"_s, QgsPointCloudAttribute::UShort ) );
194 }
195 if ( mPointFormat == 8 || mPointFormat == 10 )
196 {
197 mAttributes.push_back( QgsPointCloudAttribute( u"Infrared"_s, QgsPointCloudAttribute::UShort ) );
198 }
199 // Note: wave packet attributes are not handled and are unreadable
200}
201
202void QgsLazInfo::parseExtrabyteAttributes()
203{
204 QByteArray ebVlrRaw = vlrData( "LASF_Spec", 4 );
205 mExtrabyteAttributes = QgsLazInfo::parseExtrabytes( ebVlrRaw.data(), ebVlrRaw.size(), mHeader.point_record_length );
206
207 for ( QgsLazInfo::ExtraBytesAttributeDetails attr : mExtrabyteAttributes )
208 {
209 mAttributes.push_back( QgsPointCloudAttribute( attr.attribute, attr.type ) );
210 }
211}
212
213QVector<QgsLazInfo::ExtraBytesAttributeDetails> QgsLazInfo::parseExtrabytes( char *rawData, int length, int pointRecordLength )
214{
215 QVector<QgsLazInfo::ExtraBytesAttributeDetails> extrabyteAttributes;
216 lazperf::eb_vlr ebVlr;
217 ebVlr.fill( rawData, length );
218 for ( std::vector<lazperf::eb_vlr::ebfield>::reverse_iterator it = ebVlr.items.rbegin(); it != ebVlr.items.rend(); ++it )
219 {
220 lazperf::eb_vlr::ebfield &field = *it;
222 ebAtrr.attribute = QString::fromStdString( field.name );
223 switch ( field.data_type )
224 {
225 case 0:
227 ebAtrr.size = field.options;
228 break;
229 case 1:
231 ebAtrr.size = 1;
232 break;
233 case 2:
235 ebAtrr.size = 1;
236 break;
237 case 3:
239 ebAtrr.size = 2;
240 break;
241 case 4:
243 ebAtrr.size = 2;
244 break;
245 case 5:
247 ebAtrr.size = 4;
248 break;
249 case 6:
251 ebAtrr.size = 4;
252 break;
253 case 7:
255 ebAtrr.size = 8;
256 break;
257 case 8:
259 ebAtrr.size = 8;
260 break;
261 case 9:
263 ebAtrr.size = 4;
264 break;
265 case 10:
267 ebAtrr.size = 8;
268 break;
269 default:
271 ebAtrr.size = field.options;
272 break;
273 }
274 int accOffset = ( extrabyteAttributes.empty() ? pointRecordLength : extrabyteAttributes.back().offset ) - ebAtrr.size;
275 ebAtrr.offset = accOffset;
276 extrabyteAttributes.push_back( ebAtrr );
277 }
278 return extrabyteAttributes;
279}
280
281QgsLazInfo QgsLazInfo::fromFile( std::ifstream &file )
282{
283 QgsLazInfo lazInfo;
284
285 char headerRawData[ 375 ];
286 file.seekg( 0 );
287 file.read( headerRawData, 375 );
288 lazInfo.parseRawHeader( headerRawData, 375 );
289
290 int vlrDataSize = lazInfo.firstPointRecordOffset() - lazInfo.firstVariableLengthRecord();
291 std::unique_ptr<char[]> vlrEntriesRawData( new char[ vlrDataSize ] );
292 file.seekg( lazInfo.firstVariableLengthRecord() );
293 file.read( vlrEntriesRawData.get(), vlrDataSize );
294 lazInfo.parseRawVlrEntries( vlrEntriesRawData.get(), vlrDataSize );
295
296 return lazInfo;
297}
298
299QgsLazInfo QgsLazInfo::fromUrl( QUrl &url, const QString &authcfg )
300{
301 QgsLazInfo lazInfo;
302
303 // Fetch header data
304 {
305 QNetworkRequest nr( url );
306 QgsSetRequestInitiatorClass( nr, u"QgsLazInfo"_s );
307 nr.setAttribute( QNetworkRequest::CacheLoadControlAttribute, QNetworkRequest::AlwaysNetwork );
308 nr.setAttribute( QNetworkRequest::CacheSaveControlAttribute, false );
309 nr.setRawHeader( "Range", "bytes=0-374" );
311 req.setAuthCfg( authcfg );
312 QgsBlockingNetworkRequest::ErrorCode errCode = req.get( nr );
313 if ( errCode != QgsBlockingNetworkRequest::NoError )
314 {
315 QgsDebugError( u"Request failed: "_s + url.toString() );
316
317 if ( req.reply().attribute( QNetworkRequest::HttpStatusCodeAttribute ).toInt() == 200 )
318 {
319 lazInfo.mError = req.errorMessage();
320 }
321 else
322 {
323 lazInfo.mError = u"Range query 0-374 to \"%1\" failed: \"%2\""_s.arg( url.toString() ).arg( req.errorMessage() );
324 }
325 return lazInfo;
326 }
327
328 const QgsNetworkReplyContent reply = req.reply();
329
330 QByteArray lazHeaderData = reply.content();
331
332 lazInfo.parseRawHeader( lazHeaderData.data(), lazHeaderData.size() );
333
334 // If request was redirected, let's update our url for all next calls
335 const QUrl requestedUrl = reply.request().url();
336 if ( requestedUrl != url && authcfg.isEmpty() )
337 {
338 url.setUrl( requestedUrl.toString() );
339 }
340 }
341
342 // Fetch VLR data
343 {
344 QNetworkRequest nr( url );
345 QgsSetRequestInitiatorClass( nr, u"QgsLazInfo"_s );
346 nr.setAttribute( QNetworkRequest::CacheLoadControlAttribute, QNetworkRequest::AlwaysNetwork );
347 nr.setAttribute( QNetworkRequest::CacheSaveControlAttribute, false );
348 uint32_t firstVlrOffset = lazInfo.firstVariableLengthRecord();
349 QByteArray vlrRequestRange = u"bytes=%1-%2"_s.arg( firstVlrOffset ).arg( lazInfo.firstPointRecordOffset() - 1 ).toLocal8Bit();
350 nr.setRawHeader( "Range", vlrRequestRange );
352 req.setAuthCfg( authcfg );
353 QgsBlockingNetworkRequest::ErrorCode errCode = req.get( nr );
354 if ( errCode != QgsBlockingNetworkRequest::NoError )
355 {
356 QgsDebugError( u"Request failed: "_s + url.toString() );
357
358 lazInfo.mError = u"Range query %1-%2 to \"%3\" failed: \"%4\""_s.arg( firstVlrOffset ).arg( lazInfo.firstPointRecordOffset() - 1 )
359 .arg( url.toString() ).arg( req.errorMessage() );
360 return lazInfo;
361 }
362 QByteArray vlrDataRaw = req.reply().content();
363
364 lazInfo.parseRawVlrEntries( vlrDataRaw.data(), vlrDataRaw.size() );
365 }
366
367 return lazInfo;
368}
A thread safe class for performing blocking (sync) network requests, with full support for QGIS proxy...
void setAuthCfg(const QString &authCfg)
Sets the authentication config id which should be used during the request.
QString errorMessage() const
Returns the error message string, after a get(), post(), head() or put() request has been made.
ErrorCode get(QNetworkRequest &request, bool forceRefresh=false, QgsFeedback *feedback=nullptr, RequestFlags requestFlags=QgsBlockingNetworkRequest::RequestFlags())
Performs a "get" operation on the specified request.
@ 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.
uint32_t firstVariableLengthRecord() const
Returns the absolute offset to the first variable length record in the LAZ file.
QByteArray vlrData(QString userId, int recordId)
Returns the binary data of the variable length record with the user identifier userId and record iden...
uint32_t firstPointRecordOffset() const
Returns the absolute offset to the first point record in the LAZ file.
Definition qgslazinfo.h:96
lazperf::header14 header() const
Returns the LAZPERF header object.
Definition qgslazinfo.h:126
void parseRawVlrEntries(char *data, uint64_t length)
Parses the variable length records found in the array data of length length.
QVariantMap toMetadata() const
Returns a map containing various metadata extracted from the LAZ file.
void parseRawHeader(char *data, uint64_t length)
Parses the raw header data loaded from a LAZ file.
static QgsLazInfo fromFile(std::ifstream &file)
Static function to create a QgsLazInfo class from a file.
QgsLazInfo()
Constructor for an empty laz info parser.
int pointRecordLength() const
Returns the length of each point record in bytes.
Definition qgslazinfo.h:100
static QgsLazInfo fromUrl(QUrl &url, const QString &authcfg=QString())
Static function to create a QgsLazInfo class from a file over network.
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.
Encapsulates a network reply within a container which is inexpensive to copy and safe to pass between...
QVariant attribute(QNetworkRequest::Attribute code) const
Returns the attribute associated with the code.
QByteArray content() const
Returns the reply content.
QNetworkRequest request() const
Returns the original network request.
@ UShort
Unsigned short int 2 bytes.
@ UChar
Unsigned char 1 byte.
@ UInt32
Unsigned int32 4 bytes.
@ UInt64
Unsigned int64 8 bytes.
A 3D vector (similar to QVector3D) with the difference that it uses double precision instead of singl...
Definition qgsvector3d.h:33
#define QgsDebugMsgLevel(str, level)
Definition qgslogger.h:63
#define QgsDebugError(str)
Definition qgslogger.h:59
#define QgsSetRequestInitiatorClass(request, _class)
QgsPointCloudAttribute::DataType type
Definition qgslazinfo.h:50
QByteArray data
Definition qgslazinfo.h:44