Branch data Line data Source code
1 : : /***************************************************************************
2 : : qgspointcloudindex.cpp
3 : : --------------------
4 : : begin : October 2020
5 : : copyright : (C) 2020 by Peter Petrik
6 : : email : zilolv 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 "qgseptpointcloudindex.h"
19 : : #include <QFile>
20 : : #include <QFileInfo>
21 : : #include <QDir>
22 : : #include <QJsonArray>
23 : : #include <QJsonDocument>
24 : : #include <QJsonObject>
25 : : #include <QTime>
26 : : #include <QtDebug>
27 : : #include <QQueue>
28 : :
29 : : #include "qgseptdecoder.h"
30 : : #include "qgscoordinatereferencesystem.h"
31 : : #include "qgspointcloudrequest.h"
32 : : #include "qgspointcloudattribute.h"
33 : : #include "qgslogger.h"
34 : : #include "qgsfeedback.h"
35 : : #include "qgsmessagelog.h"
36 : :
37 : : ///@cond PRIVATE
38 : :
39 : : #define PROVIDER_KEY QStringLiteral( "ept" )
40 : : #define PROVIDER_DESCRIPTION QStringLiteral( "EPT point cloud provider" )
41 : :
42 : 0 : QgsEptPointCloudIndex::QgsEptPointCloudIndex() = default;
43 : :
44 : 0 : QgsEptPointCloudIndex::~QgsEptPointCloudIndex() = default;
45 : :
46 : 0 : void QgsEptPointCloudIndex::load( const QString &fileName )
47 : : {
48 : 0 : QFile f( fileName );
49 : 0 : if ( !f.open( QIODevice::ReadOnly ) )
50 : : {
51 : 0 : QgsMessageLog::logMessage( tr( "Unable to open %1 for reading" ).arg( fileName ) );
52 : 0 : mIsValid = false;
53 : 0 : return;
54 : : }
55 : :
56 : 0 : const QDir directory = QFileInfo( fileName ).absoluteDir();
57 : 0 : mDirectory = directory.absolutePath();
58 : 0 : bool success = loadSchema( f );
59 : 0 : if ( success )
60 : : {
61 : 0 : success = loadHierarchy();
62 : 0 : }
63 : :
64 : 0 : mIsValid = success;
65 : 0 : }
66 : :
67 : 0 : bool QgsEptPointCloudIndex::loadSchema( QFile &f )
68 : : {
69 : 0 : const QByteArray dataJson = f.readAll();
70 : : QJsonParseError err;
71 : 0 : const QJsonDocument doc = QJsonDocument::fromJson( dataJson, &err );
72 : 0 : if ( err.error != QJsonParseError::NoError )
73 : 0 : return false;
74 : 0 : const QJsonObject result = doc.object();
75 : 0 : mDataType = result.value( QLatin1String( "dataType" ) ).toString(); // "binary" or "laszip"
76 : 0 : if ( mDataType != QLatin1String( "laszip" ) && mDataType != QLatin1String( "binary" ) && mDataType != QLatin1String( "zstandard" ) )
77 : 0 : return false;
78 : :
79 : 0 : const QString hierarchyType = result.value( QLatin1String( "hierarchyType" ) ).toString(); // "json" or "gzip"
80 : 0 : if ( hierarchyType != QLatin1String( "json" ) )
81 : 0 : return false;
82 : :
83 : 0 : mSpan = result.value( QLatin1String( "span" ) ).toInt();
84 : 0 : mPointCount = result.value( QLatin1String( "points" ) ).toInt();
85 : :
86 : : // WKT
87 : 0 : const QJsonObject srs = result.value( QLatin1String( "srs" ) ).toObject();
88 : 0 : mWkt = srs.value( QLatin1String( "wkt" ) ).toString();
89 : :
90 : : // rectangular
91 : 0 : const QJsonArray bounds = result.value( QLatin1String( "bounds" ) ).toArray();
92 : 0 : if ( bounds.size() != 6 )
93 : 0 : return false;
94 : :
95 : 0 : const QJsonArray boundsConforming = result.value( QLatin1String( "boundsConforming" ) ).toArray();
96 : 0 : if ( boundsConforming.size() != 6 )
97 : 0 : return false;
98 : 0 : mExtent.set( boundsConforming[0].toDouble(), boundsConforming[1].toDouble(),
99 : 0 : boundsConforming[3].toDouble(), boundsConforming[4].toDouble() );
100 : 0 : mZMin = boundsConforming[2].toDouble();
101 : 0 : mZMax = boundsConforming[5].toDouble();
102 : :
103 : 0 : const QJsonArray schemaArray = result.value( QLatin1String( "schema" ) ).toArray();
104 : 0 : QgsPointCloudAttributeCollection attributes;
105 : :
106 : 0 : for ( const QJsonValue &schemaItem : schemaArray )
107 : : {
108 : 0 : const QJsonObject schemaObj = schemaItem.toObject();
109 : 0 : const QString name = schemaObj.value( QLatin1String( "name" ) ).toString();
110 : 0 : const QString type = schemaObj.value( QLatin1String( "type" ) ).toString();
111 : :
112 : 0 : int size = schemaObj.value( QLatin1String( "size" ) ).toInt();
113 : :
114 : 0 : if ( type == QLatin1String( "float" ) && ( size == 4 ) )
115 : : {
116 : 0 : attributes.push_back( QgsPointCloudAttribute( name, QgsPointCloudAttribute::Float ) );
117 : 0 : }
118 : 0 : else if ( type == QLatin1String( "float" ) && ( size == 8 ) )
119 : : {
120 : 0 : attributes.push_back( QgsPointCloudAttribute( name, QgsPointCloudAttribute::Double ) );
121 : 0 : }
122 : 0 : else if ( size == 1 )
123 : : {
124 : 0 : attributes.push_back( QgsPointCloudAttribute( name, QgsPointCloudAttribute::Char ) );
125 : 0 : }
126 : 0 : else if ( type == QLatin1String( "unsigned" ) && size == 2 )
127 : : {
128 : 0 : attributes.push_back( QgsPointCloudAttribute( name, QgsPointCloudAttribute::UShort ) );
129 : 0 : }
130 : 0 : else if ( size == 2 )
131 : : {
132 : 0 : attributes.push_back( QgsPointCloudAttribute( name, QgsPointCloudAttribute::Short ) );
133 : 0 : }
134 : 0 : else if ( size == 4 )
135 : : {
136 : 0 : attributes.push_back( QgsPointCloudAttribute( name, QgsPointCloudAttribute::Int32 ) );
137 : 0 : }
138 : : else
139 : : {
140 : : // unknown attribute type
141 : 0 : return false;
142 : : }
143 : :
144 : 0 : double scale = 1.f;
145 : 0 : if ( schemaObj.contains( QLatin1String( "scale" ) ) )
146 : 0 : scale = schemaObj.value( QLatin1String( "scale" ) ).toDouble();
147 : :
148 : 0 : double offset = 0.f;
149 : 0 : if ( schemaObj.contains( QLatin1String( "offset" ) ) )
150 : 0 : offset = schemaObj.value( QLatin1String( "offset" ) ).toDouble();
151 : :
152 : 0 : if ( name == QLatin1String( "X" ) )
153 : : {
154 : 0 : mOffset.set( offset, mOffset.y(), mOffset.z() );
155 : 0 : mScale.set( scale, mScale.y(), mScale.z() );
156 : 0 : }
157 : 0 : else if ( name == QLatin1String( "Y" ) )
158 : : {
159 : 0 : mOffset.set( mOffset.x(), offset, mOffset.z() );
160 : 0 : mScale.set( mScale.x(), scale, mScale.z() );
161 : 0 : }
162 : 0 : else if ( name == QLatin1String( "Z" ) )
163 : : {
164 : 0 : mOffset.set( mOffset.x(), mOffset.y(), offset );
165 : 0 : mScale.set( mScale.x(), mScale.y(), scale );
166 : 0 : }
167 : :
168 : : // store any metadata stats which are present for the attribute
169 : 0 : AttributeStatistics stats;
170 : 0 : if ( schemaObj.contains( QLatin1String( "count" ) ) )
171 : 0 : stats.count = schemaObj.value( QLatin1String( "count" ) ).toInt();
172 : 0 : if ( schemaObj.contains( QLatin1String( "minimum" ) ) )
173 : 0 : stats.minimum = schemaObj.value( QLatin1String( "minimum" ) ).toDouble();
174 : 0 : if ( schemaObj.contains( QLatin1String( "maximum" ) ) )
175 : 0 : stats.maximum = schemaObj.value( QLatin1String( "maximum" ) ).toDouble();
176 : 0 : if ( schemaObj.contains( QLatin1String( "count" ) ) )
177 : 0 : stats.mean = schemaObj.value( QLatin1String( "mean" ) ).toDouble();
178 : 0 : if ( schemaObj.contains( QLatin1String( "stddev" ) ) )
179 : 0 : stats.stDev = schemaObj.value( QLatin1String( "stddev" ) ).toDouble();
180 : 0 : if ( schemaObj.contains( QLatin1String( "variance" ) ) )
181 : 0 : stats.variance = schemaObj.value( QLatin1String( "variance" ) ).toDouble();
182 : 0 : mMetadataStats.insert( name, stats );
183 : :
184 : 0 : if ( schemaObj.contains( QLatin1String( "counts" ) ) )
185 : : {
186 : 0 : QMap< int, int > classCounts;
187 : 0 : const QJsonArray counts = schemaObj.value( QLatin1String( "counts" ) ).toArray();
188 : 0 : for ( const QJsonValue &count : counts )
189 : : {
190 : 0 : const QJsonObject countObj = count.toObject();
191 : 0 : classCounts.insert( countObj.value( QLatin1String( "value" ) ).toInt(), countObj.value( QLatin1String( "count" ) ).toInt() );
192 : 0 : }
193 : 0 : mAttributeClasses.insert( name, classCounts );
194 : 0 : }
195 : 0 : }
196 : 0 : setAttributes( attributes );
197 : :
198 : : // try to import the metadata too!
199 : :
200 : 0 : QFile manifestFile( mDirectory + QStringLiteral( "/ept-sources/manifest.json" ) );
201 : 0 : if ( manifestFile.open( QIODevice::ReadOnly ) )
202 : : {
203 : 0 : const QByteArray manifestJson = manifestFile.readAll();
204 : 0 : const QJsonDocument manifestDoc = QJsonDocument::fromJson( manifestJson, &err );
205 : 0 : if ( err.error == QJsonParseError::NoError )
206 : : {
207 : 0 : const QJsonArray manifestArray = manifestDoc.array();
208 : : // TODO how to handle multiple?
209 : 0 : if ( ! manifestArray.empty() )
210 : : {
211 : 0 : const QJsonObject sourceObject = manifestArray.at( 0 ).toObject();
212 : 0 : const QString metadataPath = sourceObject.value( QStringLiteral( "metadataPath" ) ).toString();
213 : 0 : QFile metadataFile( mDirectory + QStringLiteral( "/ept-sources/" ) + metadataPath );
214 : 0 : if ( metadataFile.open( QIODevice::ReadOnly ) )
215 : : {
216 : 0 : const QByteArray metadataJson = metadataFile.readAll();
217 : 0 : const QJsonDocument metadataDoc = QJsonDocument::fromJson( metadataJson, &err );
218 : 0 : if ( err.error == QJsonParseError::NoError )
219 : : {
220 : 0 : const QJsonObject metadataObject = metadataDoc.object().value( QStringLiteral( "metadata" ) ).toObject();
221 : 0 : if ( !metadataObject.empty() )
222 : : {
223 : 0 : const QJsonObject sourceMetadata = metadataObject.constBegin().value().toObject();
224 : 0 : mOriginalMetadata = sourceMetadata.toVariantMap();
225 : 0 : }
226 : 0 : }
227 : 0 : }
228 : 0 : }
229 : 0 : }
230 : 0 : }
231 : :
232 : : // save mRootBounds
233 : :
234 : : // bounds (cube - octree volume)
235 : 0 : double xmin = bounds[0].toDouble();
236 : 0 : double ymin = bounds[1].toDouble();
237 : 0 : double zmin = bounds[2].toDouble();
238 : 0 : double xmax = bounds[3].toDouble();
239 : 0 : double ymax = bounds[4].toDouble();
240 : 0 : double zmax = bounds[5].toDouble();
241 : :
242 : 0 : mRootBounds = QgsPointCloudDataBounds(
243 : 0 : ( xmin - mOffset.x() ) / mScale.x(),
244 : 0 : ( ymin - mOffset.y() ) / mScale.y(),
245 : 0 : ( zmin - mOffset.z() ) / mScale.z(),
246 : 0 : ( xmax - mOffset.x() ) / mScale.x(),
247 : 0 : ( ymax - mOffset.y() ) / mScale.y(),
248 : 0 : ( zmax - mOffset.z() ) / mScale.z()
249 : : );
250 : :
251 : :
252 : : #ifdef QGIS_DEBUG
253 : : double dx = xmax - xmin, dy = ymax - ymin, dz = zmax - zmin;
254 : : QgsDebugMsgLevel( QStringLiteral( "lvl0 node size in CRS units: %1 %2 %3" ).arg( dx ).arg( dy ).arg( dz ), 2 ); // all dims should be the same
255 : : QgsDebugMsgLevel( QStringLiteral( "res at lvl0 %1" ).arg( dx / mSpan ), 2 );
256 : : QgsDebugMsgLevel( QStringLiteral( "res at lvl1 %1" ).arg( dx / mSpan / 2 ), 2 );
257 : : QgsDebugMsgLevel( QStringLiteral( "res at lvl2 %1 with node size %2" ).arg( dx / mSpan / 4 ).arg( dx / 4 ), 2 );
258 : : #endif
259 : :
260 : 0 : return true;
261 : 0 : }
262 : :
263 : 0 : QgsPointCloudBlock *QgsEptPointCloudIndex::nodeData( const IndexedPointCloudNode &n, const QgsPointCloudRequest &request )
264 : : {
265 : 0 : if ( !mHierarchy.contains( n ) )
266 : 0 : return nullptr;
267 : :
268 : 0 : if ( mDataType == QLatin1String( "binary" ) )
269 : : {
270 : 0 : QString filename = QStringLiteral( "%1/ept-data/%2.bin" ).arg( mDirectory, n.toString() );
271 : 0 : return QgsEptDecoder::decompressBinary( filename, attributes(), request.attributes() );
272 : 0 : }
273 : 0 : else if ( mDataType == QLatin1String( "zstandard" ) )
274 : : {
275 : 0 : QString filename = QStringLiteral( "%1/ept-data/%2.zst" ).arg( mDirectory, n.toString() );
276 : 0 : return QgsEptDecoder::decompressZStandard( filename, attributes(), request.attributes() );
277 : 0 : }
278 : 0 : else if ( mDataType == QLatin1String( "laszip" ) )
279 : : {
280 : 0 : QString filename = QStringLiteral( "%1/ept-data/%2.laz" ).arg( mDirectory, n.toString() );
281 : 0 : return QgsEptDecoder::decompressLaz( filename, attributes(), request.attributes() );
282 : 0 : }
283 : : else
284 : : {
285 : 0 : return nullptr; // unsupported
286 : : }
287 : 0 : }
288 : :
289 : 0 : QgsCoordinateReferenceSystem QgsEptPointCloudIndex::crs() const
290 : : {
291 : 0 : return QgsCoordinateReferenceSystem::fromWkt( mWkt );
292 : : }
293 : :
294 : 0 : int QgsEptPointCloudIndex::pointCount() const
295 : : {
296 : 0 : return mPointCount;
297 : : }
298 : :
299 : 0 : QVariant QgsEptPointCloudIndex::metadataStatistic( const QString &attribute, QgsStatisticalSummary::Statistic statistic ) const
300 : : {
301 : 0 : if ( !mMetadataStats.contains( attribute ) )
302 : 0 : return QVariant();
303 : :
304 : 0 : const AttributeStatistics &stats = mMetadataStats[ attribute ];
305 : 0 : switch ( statistic )
306 : : {
307 : : case QgsStatisticalSummary::Count:
308 : 0 : return stats.count >= 0 ? QVariant( stats.count ) : QVariant();
309 : :
310 : : case QgsStatisticalSummary::Mean:
311 : 0 : return std::isnan( stats.mean ) ? QVariant() : QVariant( stats.mean );
312 : :
313 : : case QgsStatisticalSummary::StDev:
314 : 0 : return std::isnan( stats.stDev ) ? QVariant() : QVariant( stats.stDev );
315 : :
316 : : case QgsStatisticalSummary::Min:
317 : 0 : return stats.minimum;
318 : :
319 : : case QgsStatisticalSummary::Max:
320 : 0 : return stats.maximum;
321 : :
322 : : case QgsStatisticalSummary::Range:
323 : 0 : return stats.minimum.isValid() && stats.maximum.isValid() ? QVariant( stats.maximum.toDouble() - stats.minimum.toDouble() ) : QVariant();
324 : :
325 : : case QgsStatisticalSummary::CountMissing:
326 : : case QgsStatisticalSummary::Sum:
327 : : case QgsStatisticalSummary::Median:
328 : : case QgsStatisticalSummary::StDevSample:
329 : : case QgsStatisticalSummary::Minority:
330 : : case QgsStatisticalSummary::Majority:
331 : : case QgsStatisticalSummary::Variety:
332 : : case QgsStatisticalSummary::FirstQuartile:
333 : : case QgsStatisticalSummary::ThirdQuartile:
334 : : case QgsStatisticalSummary::InterQuartileRange:
335 : : case QgsStatisticalSummary::First:
336 : : case QgsStatisticalSummary::Last:
337 : : case QgsStatisticalSummary::All:
338 : 0 : return QVariant();
339 : : }
340 : 0 : return QVariant();
341 : 0 : }
342 : :
343 : 0 : QVariantList QgsEptPointCloudIndex::metadataClasses( const QString &attribute ) const
344 : : {
345 : 0 : QVariantList classes;
346 : 0 : const QMap< int, int > values = mAttributeClasses.value( attribute );
347 : 0 : for ( auto it = values.constBegin(); it != values.constEnd(); ++it )
348 : : {
349 : 0 : classes << it.key();
350 : 0 : }
351 : 0 : return classes;
352 : 0 : }
353 : :
354 : 0 : QVariant QgsEptPointCloudIndex::metadataClassStatistic( const QString &attribute, const QVariant &value, QgsStatisticalSummary::Statistic statistic ) const
355 : : {
356 : 0 : if ( statistic != QgsStatisticalSummary::Count )
357 : 0 : return QVariant();
358 : :
359 : 0 : const QMap< int, int > values = mAttributeClasses.value( attribute );
360 : 0 : if ( !values.contains( value.toInt() ) )
361 : 0 : return QVariant();
362 : 0 : return values.value( value.toInt() );
363 : 0 : }
364 : :
365 : 0 : bool QgsEptPointCloudIndex::loadHierarchy()
366 : : {
367 : 0 : QQueue<QString> queue;
368 : 0 : queue.enqueue( QStringLiteral( "0-0-0-0" ) );
369 : 0 : while ( !queue.isEmpty() )
370 : : {
371 : 0 : const QString filename = QStringLiteral( "%1/ept-hierarchy/%2.json" ).arg( mDirectory, queue.dequeue() );
372 : 0 : QFile fH( filename );
373 : 0 : if ( !fH.open( QIODevice::ReadOnly ) )
374 : : {
375 : 0 : QgsDebugMsgLevel( QStringLiteral( "unable to read hierarchy from file %1" ).arg( filename ), 2 );
376 : 0 : return false;
377 : : }
378 : :
379 : 0 : QByteArray dataJsonH = fH.readAll();
380 : : QJsonParseError errH;
381 : 0 : const QJsonDocument docH = QJsonDocument::fromJson( dataJsonH, &errH );
382 : 0 : if ( errH.error != QJsonParseError::NoError )
383 : : {
384 : 0 : QgsDebugMsgLevel( QStringLiteral( "QJsonParseError when reading hierarchy from file %1" ).arg( filename ), 2 );
385 : 0 : return false;
386 : : }
387 : :
388 : 0 : const QJsonObject rootHObj = docH.object();
389 : 0 : for ( auto it = rootHObj.constBegin(); it != rootHObj.constEnd(); ++it )
390 : : {
391 : 0 : QString nodeIdStr = it.key();
392 : 0 : int nodePointCount = it.value().toInt();
393 : 0 : if ( nodePointCount < 0 )
394 : : {
395 : 0 : queue.enqueue( nodeIdStr );
396 : 0 : }
397 : : else
398 : : {
399 : 0 : IndexedPointCloudNode nodeId = IndexedPointCloudNode::fromString( nodeIdStr );
400 : 0 : mHierarchy[nodeId] = nodePointCount;
401 : : }
402 : 0 : }
403 : 0 : }
404 : 0 : return true;
405 : 0 : }
406 : :
407 : 0 : bool QgsEptPointCloudIndex::isValid() const
408 : : {
409 : 0 : return mIsValid;
410 : : }
411 : :
412 : : ///@endcond
|