1 /***************************************************************************
2                          qgspointcloudrenderer.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 "qgspointcloudrenderer.h"
19 #include "qgspointcloudrendererregistry.h"
20 #include "qgsapplication.h"
21 #include "qgssymbollayerutils.h"
22 #include "qgspointcloudlayer.h"
23 #include "qgspointcloudindex.h"
24 #include "qgspointcloudlayerelevationproperties.h"
25 #include "qgslogger.h"
26 #include "qgscircle.h"
27 #include <QThread>
28 #include <QPointer>
29 
QgsPointCloudRenderContext(QgsRenderContext & context,const QgsVector3D & scale,const QgsVector3D & offset,double zValueScale,double zValueFixedOffset,QgsFeedback * feedback)30 QgsPointCloudRenderContext::QgsPointCloudRenderContext( QgsRenderContext &context, const QgsVector3D &scale, const QgsVector3D &offset, double zValueScale, double zValueFixedOffset, QgsFeedback *feedback )
31   : mRenderContext( context )
32   , mScale( scale )
33   , mOffset( offset )
34   , mZValueScale( zValueScale )
35   , mZValueFixedOffset( zValueFixedOffset )
36   , mFeedback( feedback )
37 {
38 
39 }
40 
pointsRendered() const41 long QgsPointCloudRenderContext::pointsRendered() const
42 {
43   return mPointsRendered;
44 }
45 
incrementPointsRendered(long count)46 void QgsPointCloudRenderContext::incrementPointsRendered( long count )
47 {
48   mPointsRendered += count;
49 }
50 
setAttributes(const QgsPointCloudAttributeCollection & attributes)51 void QgsPointCloudRenderContext::setAttributes( const QgsPointCloudAttributeCollection &attributes )
52 {
53   mAttributes = attributes;
54   mPointRecordSize = mAttributes.pointRecordSize();
55 
56   // fetch offset for x/y/z attributes
57   attributes.find( QStringLiteral( "X" ), mXOffset );
58   attributes.find( QStringLiteral( "Y" ), mYOffset );
59   attributes.find( QStringLiteral( "Z" ), mZOffset );
60 }
61 
load(QDomElement & element,const QgsReadWriteContext & context)62 QgsPointCloudRenderer *QgsPointCloudRenderer::load( QDomElement &element, const QgsReadWriteContext &context )
63 {
64   if ( element.isNull() )
65     return nullptr;
66 
67   // load renderer
68   const QString rendererType = element.attribute( QStringLiteral( "type" ) );
69 
70   QgsPointCloudRendererAbstractMetadata *m = QgsApplication::pointCloudRendererRegistry()->rendererMetadata( rendererType );
71   if ( !m )
72     return nullptr;
73 
74   std::unique_ptr< QgsPointCloudRenderer > r( m->createRenderer( element, context ) );
75   return r.release();
76 }
77 
usedAttributes(const QgsPointCloudRenderContext &) const78 QSet<QString> QgsPointCloudRenderer::usedAttributes( const QgsPointCloudRenderContext & ) const
79 {
80   return QSet< QString >();
81 }
82 
startRender(QgsPointCloudRenderContext & context)83 void QgsPointCloudRenderer::startRender( QgsPointCloudRenderContext &context )
84 {
85 #ifdef QGISDEBUG
86   if ( !mThread )
87   {
88     mThread = QThread::currentThread();
89   }
90   else
91   {
92     Q_ASSERT_X( mThread == QThread::currentThread(), "QgsPointCloudRenderer::startRender", "startRender called in a different thread - use a cloned renderer instead" );
93   }
94 #endif
95 
96   mPainterPenWidth = context.renderContext().convertToPainterUnits( pointSize(), pointSizeUnit(), pointSizeMapUnitScale() );
97 
98   switch ( mPointSymbol )
99   {
100     case Square:
101       // for square point we always disable antialiasing -- it's not critical here and we benefit from the performance boost disabling it gives
102       context.renderContext().painter()->setRenderHint( QPainter::Antialiasing, false );
103       break;
104 
105     case Circle:
106       break;
107   }
108 }
109 
stopRender(QgsPointCloudRenderContext &)110 void QgsPointCloudRenderer::stopRender( QgsPointCloudRenderContext & )
111 {
112 #ifdef QGISDEBUG
113   Q_ASSERT_X( mThread == QThread::currentThread(), "QgsPointCloudRenderer::stopRender", "stopRender called in a different thread - use a cloned renderer instead" );
114 #endif
115 }
116 
legendItemChecked(const QString &)117 bool QgsPointCloudRenderer::legendItemChecked( const QString & )
118 {
119   return false;
120 }
121 
checkLegendItem(const QString &,bool)122 void QgsPointCloudRenderer::checkLegendItem( const QString &, bool )
123 {
124 
125 }
126 
maximumScreenError() const127 double QgsPointCloudRenderer::maximumScreenError() const
128 {
129   return mMaximumScreenError;
130 }
131 
setMaximumScreenError(double error)132 void QgsPointCloudRenderer::setMaximumScreenError( double error )
133 {
134   mMaximumScreenError = error;
135 }
136 
maximumScreenErrorUnit() const137 QgsUnitTypes::RenderUnit QgsPointCloudRenderer::maximumScreenErrorUnit() const
138 {
139   return mMaximumScreenErrorUnit;
140 }
141 
setMaximumScreenErrorUnit(QgsUnitTypes::RenderUnit unit)142 void QgsPointCloudRenderer::setMaximumScreenErrorUnit( QgsUnitTypes::RenderUnit unit )
143 {
144   mMaximumScreenErrorUnit = unit;
145 }
146 
createLegendNodes(QgsLayerTreeLayer *)147 QList<QgsLayerTreeModelLegendNode *> QgsPointCloudRenderer::createLegendNodes( QgsLayerTreeLayer * )
148 {
149   return QList<QgsLayerTreeModelLegendNode *>();
150 }
151 
legendRuleKeys() const152 QStringList QgsPointCloudRenderer::legendRuleKeys() const
153 {
154   return QStringList();
155 }
156 
copyCommonProperties(QgsPointCloudRenderer * destination) const157 void QgsPointCloudRenderer::copyCommonProperties( QgsPointCloudRenderer *destination ) const
158 {
159   destination->setPointSize( mPointSize );
160   destination->setPointSizeUnit( mPointSizeUnit );
161   destination->setPointSizeMapUnitScale( mPointSizeMapUnitScale );
162   destination->setMaximumScreenError( mMaximumScreenError );
163   destination->setMaximumScreenErrorUnit( mMaximumScreenErrorUnit );
164   destination->setPointSymbol( mPointSymbol );
165 }
166 
restoreCommonProperties(const QDomElement & element,const QgsReadWriteContext &)167 void QgsPointCloudRenderer::restoreCommonProperties( const QDomElement &element, const QgsReadWriteContext & )
168 {
169   mPointSize = element.attribute( QStringLiteral( "pointSize" ), QStringLiteral( "1" ) ).toDouble();
170   mPointSizeUnit = QgsUnitTypes::decodeRenderUnit( element.attribute( QStringLiteral( "pointSizeUnit" ), QStringLiteral( "MM" ) ) );
171   mPointSizeMapUnitScale = QgsSymbolLayerUtils::decodeMapUnitScale( element.attribute( QStringLiteral( "pointSizeMapUnitScale" ), QString() ) );
172 
173   mMaximumScreenError = element.attribute( QStringLiteral( "maximumScreenError" ), QStringLiteral( "0.3" ) ).toDouble();
174   mMaximumScreenErrorUnit = QgsUnitTypes::decodeRenderUnit( element.attribute( QStringLiteral( "maximumScreenErrorUnit" ), QStringLiteral( "MM" ) ) );
175   mPointSymbol = static_cast< PointSymbol >( element.attribute( QStringLiteral( "pointSymbol" ), QStringLiteral( "0" ) ).toInt() );
176 }
177 
saveCommonProperties(QDomElement & element,const QgsReadWriteContext &) const178 void QgsPointCloudRenderer::saveCommonProperties( QDomElement &element, const QgsReadWriteContext & ) const
179 {
180   element.setAttribute( QStringLiteral( "pointSize" ), qgsDoubleToString( mPointSize ) );
181   element.setAttribute( QStringLiteral( "pointSizeUnit" ), QgsUnitTypes::encodeUnit( mPointSizeUnit ) );
182   element.setAttribute( QStringLiteral( "pointSizeMapUnitScale" ), QgsSymbolLayerUtils::encodeMapUnitScale( mPointSizeMapUnitScale ) );
183 
184   element.setAttribute( QStringLiteral( "maximumScreenError" ), qgsDoubleToString( mMaximumScreenError ) );
185   element.setAttribute( QStringLiteral( "maximumScreenErrorUnit" ), QgsUnitTypes::encodeUnit( mMaximumScreenErrorUnit ) );
186   element.setAttribute( QStringLiteral( "pointSymbol" ), QString::number( mPointSymbol ) );
187 }
188 
pointSymbol() const189 QgsPointCloudRenderer::PointSymbol QgsPointCloudRenderer::pointSymbol() const
190 {
191   return mPointSymbol;
192 }
193 
setPointSymbol(PointSymbol symbol)194 void QgsPointCloudRenderer::setPointSymbol( PointSymbol symbol )
195 {
196   mPointSymbol = symbol;
197 }
198 
identify(QgsPointCloudLayer * layer,const QgsRenderContext & renderContext,const QgsGeometry & geometry,double toleranceForPointIdentification)199 QVector<QVariantMap> QgsPointCloudRenderer::identify( QgsPointCloudLayer *layer, const QgsRenderContext &renderContext, const QgsGeometry &geometry, double toleranceForPointIdentification )
200 {
201   QVector<QVariantMap> selectedPoints;
202 
203   QgsPointCloudIndex *index = layer->dataProvider()->index();
204   const IndexedPointCloudNode root = index->root();
205 
206   const double maxErrorPixels = renderContext.convertToPainterUnits( maximumScreenError(), maximumScreenErrorUnit() );// in pixels
207 
208   const QgsRectangle rootNodeExtentLayerCoords = index->nodeMapExtent( root );
209   QgsRectangle rootNodeExtentMapCoords;
210   try
211   {
212     rootNodeExtentMapCoords = renderContext.coordinateTransform().transformBoundingBox( rootNodeExtentLayerCoords );
213   }
214   catch ( QgsCsException & )
215   {
216     QgsDebugMsg( QStringLiteral( "Could not transform node extent to map CRS" ) );
217     rootNodeExtentMapCoords = rootNodeExtentLayerCoords;
218   }
219 
220   const double rootErrorInMapCoordinates = rootNodeExtentMapCoords.width() / index->span();
221   const double rootErrorInLayerCoordinates = rootNodeExtentLayerCoords.width() / index->span();
222 
223   const double mapUnitsPerPixel = renderContext.mapToPixel().mapUnitsPerPixel();
224   if ( ( rootErrorInMapCoordinates < 0.0 ) || ( mapUnitsPerPixel < 0.0 ) || ( maxErrorPixels < 0.0 ) )
225   {
226     QgsDebugMsg( QStringLiteral( "invalid screen error" ) );
227     return selectedPoints;
228   }
229 
230   const double maxErrorInMapCoordinates = maxErrorPixels * mapUnitsPerPixel;
231   const double maxErrorInLayerCoordinates = maxErrorInMapCoordinates * rootErrorInLayerCoordinates / rootErrorInMapCoordinates;
232 
233   QgsGeometry selectionGeometry = geometry;
234   if ( geometry.type() == QgsWkbTypes::PointGeometry )
235   {
236     const double x = geometry.asPoint().x();
237     const double y = geometry.asPoint().y();
238     const double toleranceInPixels = toleranceForPointIdentification / renderContext.mapToPixel().mapUnitsPerPixel();
239     const double pointSizePixels = renderContext.convertToPainterUnits( mPointSize, mPointSizeUnit, mPointSizeMapUnitScale );
240     switch ( pointSymbol() )
241     {
242       case QgsPointCloudRenderer::PointSymbol::Square:
243       {
244         const QgsPointXY deviceCoords = renderContext.mapToPixel().transform( QgsPointXY( x, y ) );
245         const QgsPointXY point1( deviceCoords.x() - std::max( toleranceInPixels, pointSizePixels / 2.0 ), deviceCoords.y() - std::max( toleranceInPixels, pointSizePixels / 2.0 ) );
246         const QgsPointXY point2( deviceCoords.x() + std::max( toleranceInPixels, pointSizePixels / 2.0 ), deviceCoords.y() + std::max( toleranceInPixels, pointSizePixels / 2.0 ) );
247         const QgsPointXY point1MapCoords = renderContext.mapToPixel().toMapCoordinates( point1.x(), point1.y() );
248         const QgsPointXY point2MapCoords = renderContext.mapToPixel().toMapCoordinates( point2.x(), point2.y() );
249         const QgsRectangle pointRect( point1MapCoords, point2MapCoords );
250         selectionGeometry = QgsGeometry::fromRect( pointRect );
251         break;
252       }
253       case QgsPointCloudRenderer::PointSymbol::Circle:
254       {
255         const QgsPoint centerMapCoords( x, y );
256         const QgsPointXY deviceCoords = renderContext.mapToPixel().transform( centerMapCoords );
257         const QgsPoint point1( deviceCoords.x(), deviceCoords.y() - std::max( toleranceInPixels, pointSizePixels / 2.0 ) );
258         const QgsPoint point2( deviceCoords.x(), deviceCoords.y() + std::max( toleranceInPixels, pointSizePixels / 2.0 ) );
259         const QgsPointXY point1MapCoords = renderContext.mapToPixel().toMapCoordinates( point1.x(), point1.y() );
260         const QgsPointXY point2MapCoords = renderContext.mapToPixel().toMapCoordinates( point2.x(), point2.y() );
261         const QgsCircle circle = QgsCircle::from2Points( QgsPoint( point1MapCoords ), QgsPoint( point2MapCoords ) );
262         std::unique_ptr<QgsPolygon> polygon( circle.toPolygon( 6 ) );
263         const QgsGeometry circleGeometry( std::move( polygon ) );
264         selectionGeometry = circleGeometry;
265         break;
266       }
267     }
268   }
269 
270   // selection geometry must be in layer CRS for QgsPointCloudDataProvider::identify
271   try
272   {
273     selectionGeometry.transform( renderContext.coordinateTransform(), Qgis::TransformDirection::Reverse );
274   }
275   catch ( QgsCsException & )
276   {
277     QgsDebugMsg( QStringLiteral( "Could not transform geometry to layer CRS" ) );
278     return selectedPoints;
279   }
280 
281   selectedPoints = layer->dataProvider()->identify( maxErrorInLayerCoordinates, selectionGeometry, renderContext.zRange() );
282 
283   selectedPoints.erase( std::remove_if( selectedPoints.begin(), selectedPoints.end(), [this]( const QMap<QString, QVariant> &point ) { return !this->willRenderPoint( point ); } ), selectedPoints.end() );
284 
285   return selectedPoints;
286 }
287