QGIS API Documentation 4.3.0-Master (90cb4ecf9ef)
Loading...
Searching...
No Matches
qgsannotationlayerchunkloader_p.cpp
Go to the documentation of this file.
1/***************************************************************************
2 qgsannotationlayerchunkloader_p.cpp
3 --------------------------------------
4 Date : September 2025
5 Copyright : (C) 2025 by Nyall Dawson
6 Email : nyall dot dawson at gmail dot com
7 ***************************************************************************
8 * *
9 * This program is free software; you can redistribute it and/or modify *
10 * it under the terms of the GNU General Public License as published by *
11 * the Free Software Foundation; either version 2 of the License, or *
12 * (at your option) any later version. *
13 * *
14 ***************************************************************************/
15
17
18#include "qgs3dutils.h"
19#include "qgsabstract3dsymbol.h"
21#include "qgsannotationitem.h"
22#include "qgsannotationlayer.h"
28#include "qgsapplication.h"
30#include "qgschunknode.h"
31#include "qgseventtracing.h"
34#include "qgsgeos.h"
35#include "qgsgeotransform.h"
36#include "qgsimagecache.h"
37#include "qgslinematerial_p.h"
38#include "qgslinevertexdata_p.h"
39#include "qgslogger.h"
40#include "qgsmarkersymbol.h"
41#include "qgspainting.h"
43#include "qgssvgcache.h"
45#include "qgstextdocument.h"
47
48#include <QString>
49#include <QTimer>
50#include <Qt3DCore/QTransform>
51#include <Qt3DRender/QGeometryRenderer>
52#include <QtConcurrentRun>
53
54#include "moc_qgsannotationlayerchunkloader_p.cpp"
55
56using namespace Qt::StringLiterals;
57
59
60
61QgsAnnotationLayerChunkLoader::QgsAnnotationLayerChunkLoader( const QgsAnnotationLayerChunkLoaderFactory *factory, QgsChunkNode *node )
62 : QgsChunkLoader( node )
63 , mFactory( factory )
64 , mRenderContext( factory->mRenderContext )
65{}
66
67namespace
68{
69 struct Billboard
70 {
71 QVector3D position;
72 int textureId = -1;
73 const QgsMarkerSymbol *markerSymbol = nullptr;
74 };
75
76 struct TextBillboard
77 {
78 QVector3D position;
79 QString text;
80 };
81
85 struct PictureBillboardGroup
86 {
87 QString path;
89 Qgis::PictureFormat pictureFormat;
90
91 bool operator<( const PictureBillboardGroup &other ) const
92 {
93 if ( path != other.path )
94 return path < other.path;
95 return scaleMode < other.scaleMode;
96 }
97 };
98
99 struct PictureBillboard
100 {
101 QVector3D position;
102 QSizeF size;
103 };
104} //namespace
105
106void QgsAnnotationLayerChunkLoader::start()
107{
108 QgsChunkNode *node = chunk();
109 if ( node->level() < mFactory->mLeafLevel )
110 {
111 QTimer::singleShot( 0, this, &QgsAnnotationLayerChunkLoader::finished );
112 return;
113 }
114
115 QgsAnnotationLayer *layer = mFactory->mLayer;
116 mLayerName = mFactory->mLayer->name();
117
118 // only a subset of data to be queried
119 const QgsRectangle rect = node->box3D().toRectangle();
120 // origin for coordinates of the chunk - it is kind of arbitrary, but it should be
121 // picked so that the coordinates are relatively small to avoid numerical precision issues
122 mChunkOrigin = QgsVector3D( rect.center().x(), rect.center().y(), 0 );
123
124 QgsExpressionContext exprContext;
126 mRenderContext.setExpressionContext( exprContext );
127
128 QgsCoordinateTransform layerToMapTransform( layer->crs(), mRenderContext.crs(), mRenderContext.transformContext() );
129
130 QgsRectangle layerExtent;
131 try
132 {
133 layerExtent = layerToMapTransform.transformBoundingBox( rect, Qgis::TransformDirection::Reverse );
134 }
135 catch ( QgsCsException &e )
136 {
137 QgsDebugError( u"Error transforming annotation layer extent to 3d map extent: %1"_s.arg( e.what() ) );
138 return;
139 }
140
141 const double zOffset = mFactory->mZOffset;
142 const Qgis::AltitudeClamping altitudeClamping = mFactory->mClamping;
143 bool showCallouts = mFactory->mShowCallouts;
144 const QgsTextFormat textFormat = mFactory->mTextFormat;
145
146 // see logic from QgsAnnotationLayerRenderer
147 const QStringList itemsList = layer->queryIndex( layerExtent );
148 QSet< QString > itemIds( itemsList.begin(), itemsList.end() );
149
150 // we also have NO choice but to clone ALL non-indexed items (i.e. those with a scale-dependent bounding box)
151 // since these won't be in the layer's spatial index, and it's too expensive to determine their actual bounding box
152 // upfront (we are blocking the main thread right now!)
153
154 // TODO -- come up with some brilliant way to avoid this and also index scale-dependent items ;)
155 itemIds.unite( layer->mNonIndexedItems );
156
157 mItemsToRender.reserve( itemIds.size() );
158 std::transform( itemIds.begin(), itemIds.end(), std::back_inserter( mItemsToRender ), [layer]( const QString &id ) -> std::unique_ptr< QgsAnnotationItem > {
159 return std::unique_ptr< QgsAnnotationItem >( layer->item( id )->clone() );
160 } );
161
162 //
163 // this will be run in a background thread
164 //
165 mFutureWatcher = new QFutureWatcher<void>( this );
166 connect( mFutureWatcher, &QFutureWatcher<void>::finished, this, &QgsChunkQueueJob::finished );
167
168 const QFuture<void> future = QtConcurrent::run( [this, rect, layerToMapTransform, zOffset, altitudeClamping, showCallouts, textFormat] {
169 const QgsScopedEvent e( u"3D"_s, u"Annotation layer chunk load"_s );
170
171 std::vector< Billboard > billboards;
172 billboards.reserve( mItemsToRender.size() );
173 QVector< QImage > textures;
174 textures.reserve( static_cast< qsizetype >( mItemsToRender.size() ) );
175
176 std::vector< TextBillboard > textBillboards;
177 textBillboards.reserve( mItemsToRender.size() );
178 QStringList textBillboardTexts;
179 textBillboardTexts.reserve( static_cast< qsizetype >( mItemsToRender.size() ) );
180
181 QMap< PictureBillboardGroup, QVector< PictureBillboard > > groupedPictures;
182 mPictureBillboards.reserve( static_cast< qsizetype >( mItemsToRender.size() ) );
183
184 auto addTextBillboard = [layerToMapTransform,
185 showCallouts,
186 rect,
187 zOffset,
188 altitudeClamping,
189 this,
190 &textBillboards,
191 &textBillboardTexts]( const QgsPointXY &p, const QString &annotationText, const QgsTextFormat &annotationTextFormat ) {
192 QString text = annotationText;
193 if ( annotationTextFormat.allowHtmlFormatting() )
194 {
195 // strip HTML characters, we don't support those in 3D
196 const QgsTextDocument document = QgsTextDocument::fromTextAndFormat( { text }, annotationTextFormat );
197 text = document.toPlainText().join( ' ' );
198 }
199 if ( !text.isEmpty() )
200 {
201 try
202 {
203 const QgsPointXY mapPoint = layerToMapTransform.transform( p );
204 if ( !rect.contains( mapPoint ) )
205 return;
206
207 double z = 0;
208 const float terrainZ = ( altitudeClamping == Qgis::AltitudeClamping::Absolute && !showCallouts ) ? 0
209 : mRenderContext.terrainRenderingEnabled() && mRenderContext.terrainGenerator()
210 ? static_cast<float>( mRenderContext.terrainGenerator()->heightAt( mapPoint.x(), mapPoint.y(), mRenderContext ) * mRenderContext.terrainSettings()->verticalScale() )
211 : 0.f;
212
213 switch ( altitudeClamping )
214 {
216 z = zOffset;
217 break;
219 z = terrainZ;
220 break;
222 z = terrainZ + zOffset;
223 break;
224 }
225
226 TextBillboard billboard;
227 billboard.position = ( QgsVector3D( mapPoint.x(), mapPoint.y(), z ) - mChunkOrigin ).toVector3D();
228 billboard.text = text;
229 textBillboards.emplace_back( std::move( billboard ) );
230 textBillboardTexts.append( text );
231
232 if ( showCallouts )
233 {
234 mCalloutLines << QgsLineString( { mapPoint.x(), mapPoint.x() }, { mapPoint.y(), mapPoint.y() }, { terrainZ, z } );
235 }
236
237 mZMax = std::max( mZMax, showCallouts ? std::max( 0.0, z ) : z );
238 mZMin = std::min( mZMin, showCallouts ? std::min( 0.0, z ) : z );
239 }
240 catch ( QgsCsException &e )
241 {
242 QgsDebugError( e.what() );
243 }
244 }
245 };
246
247 for ( const std::unique_ptr< QgsAnnotationItem > &item : std::as_const( mItemsToRender ) )
248 {
249 if ( mCanceled )
250 break;
251
252 QgsAnnotationItem *annotation = item.get();
253
254 if ( !annotation->enabled() )
255 continue;
256
257 if ( QgsAnnotationMarkerItem *marker = dynamic_cast< QgsAnnotationMarkerItem * >( annotation ) )
258 {
259 if ( marker->symbol() )
260 {
261 QgsPointXY p = marker->geometry();
262 try
263 {
264 const QgsPointXY mapPoint = layerToMapTransform.transform( p );
265 if ( !rect.contains( mapPoint ) )
266 continue;
267
268 double z = 0;
269 const float terrainZ = ( altitudeClamping == Qgis::AltitudeClamping::Absolute && !showCallouts ) ? 0
270 : mRenderContext.terrainRenderingEnabled() && mRenderContext.terrainGenerator()
271 ? static_cast<float>( mRenderContext.terrainGenerator()->heightAt( mapPoint.x(), mapPoint.y(), mRenderContext ) * mRenderContext.terrainSettings()->verticalScale() )
272 : 0.f;
273
274 switch ( altitudeClamping )
275 {
277 z = zOffset;
278 break;
280 z = terrainZ;
281 break;
283 z = terrainZ + zOffset;
284 break;
285 }
286
287 Billboard billboard;
288 billboard.position = ( QgsVector3D( mapPoint.x(), mapPoint.y(), z ) - mChunkOrigin ).toVector3D();
289 billboard.textureId = -1;
290
291 for ( const Billboard &existingBillboard : billboards )
292 {
293 if ( existingBillboard.markerSymbol && marker->symbol()->rendersIdenticallyTo( existingBillboard.markerSymbol ) )
294 {
295 // marker symbol has been reused => reuse existing texture to minimize size of texture atlas
296 billboard.textureId = existingBillboard.textureId;
297 break;
298 }
299 }
300
301 if ( billboard.textureId < 0 )
302 {
303 // could not match to previously considered marker, have to render and add to texture atlas
304 billboard.markerSymbol = marker->symbol();
305 billboard.textureId = textures.size();
306 textures.append( QgsPoint3DBillboardMaterial::renderSymbolToImage( marker->symbol(), mRenderContext ) );
307 }
308 billboards.emplace_back( std::move( billboard ) );
309
310 if ( showCallouts )
311 {
312 mCalloutLines << QgsLineString( { mapPoint.x(), mapPoint.x() }, { mapPoint.y(), mapPoint.y() }, { terrainZ, z } );
313 }
314
315 mZMax = std::max( mZMax, showCallouts ? std::max( 0.0, z ) : z );
316 mZMin = std::min( mZMin, showCallouts ? std::min( 0.0, z ) : z );
317 }
318 catch ( QgsCsException &e )
319 {
320 QgsDebugError( e.what() );
321 }
322 }
323 }
324 else if ( QgsAnnotationPointTextItem *pointText = dynamic_cast< QgsAnnotationPointTextItem * >( annotation ) )
325 {
326 addTextBillboard( pointText->point(), pointText->text(), pointText->format() );
327 }
328 else if ( QgsAnnotationLineTextItem *lineText = dynamic_cast< QgsAnnotationLineTextItem * >( annotation ) )
329 {
330 QgsGeos geos( lineText->geometry() );
331 std::unique_ptr< QgsPoint > point( geos.pointOnSurface() );
332 if ( point )
333 {
334 addTextBillboard( *point, lineText->text(), lineText->format() );
335 }
336 }
337 else if ( QgsAnnotationRectangleTextItem *rectText = dynamic_cast< QgsAnnotationRectangleTextItem * >( annotation ) )
338 {
339 switch ( rectText->placementMode() )
340 {
343 {
344 addTextBillboard( rectText->bounds().center(), rectText->text(), rectText->format() );
345 break;
346 }
348 // ignore these annotations, they don't have a fix map position
349 break;
350 }
351 }
352 else if ( auto pictureItem = dynamic_cast< QgsAnnotationPictureItem * >( annotation ) )
353 {
354 if ( pictureItem->path().isEmpty() )
355 continue;
356
357 if ( pictureItem->placementMode() == Qgis::AnnotationPlacementMode::RelativeToMapFrame )
358 {
359 // annotations relative to the map frame have no geographic position => ignore
360 continue;
361 }
362
363 const QgsPointXY p = pictureItem->bounds().center();
364 QgsPointXY mapPoint;
365 try
366 {
367 mapPoint = layerToMapTransform.transform( p );
368 }
369 catch ( QgsCsException &e )
370 {
371 QgsDebugError( e.what() );
372 continue;
373 }
374
375 if ( !rect.contains( mapPoint ) )
376 continue;
377
378 double z = 0;
379 const float terrainZ = ( altitudeClamping == Qgis::AltitudeClamping::Absolute && !showCallouts ) ? 0
380 : mRenderContext.terrainRenderingEnabled() && mRenderContext.terrainGenerator()
381 ? static_cast<float>( mRenderContext.terrainGenerator()->heightAt( mapPoint.x(), mapPoint.y(), mRenderContext ) * mRenderContext.terrainSettings()->verticalScale() )
382 : 0.f;
383
384 switch ( altitudeClamping )
385 {
387 z = zOffset;
388 break;
390 z = terrainZ;
391 break;
393 z = terrainZ + zOffset;
394 break;
395 }
396
397 PictureBillboardGroup billboardGroup;
398 billboardGroup.path = pictureItem->path();
399 billboardGroup.pictureFormat = pictureItem->format();
400 billboardGroup.scaleMode = pictureItem->billboard3DScaleMode();
401
402 PictureBillboard billboardItem;
403 billboardItem.position = ( QgsVector3D( mapPoint.x(), mapPoint.y(), z ) - mChunkOrigin ).toVector3D();
404 billboardItem.size = pictureItem->billboard3DSize();
405 if ( billboardItem.size.isEmpty() )
406 {
407 constexpr QSizeF DEFAULT_FIXED_SIZE = QSizeF( 128, 128 );
408 constexpr QSizeF DEFAULT_WORLD_SIZE = QSizeF( 20, 20 );
409 billboardItem.size = billboardGroup.scaleMode == Qgis::BillboardScaleMode::ViewIndependent ? DEFAULT_FIXED_SIZE : DEFAULT_WORLD_SIZE;
410 }
411
412 groupedPictures[billboardGroup].append( billboardItem );
413
414 if ( showCallouts )
415 {
416 mCalloutLines << QgsLineString( { mapPoint.x(), mapPoint.x() }, { mapPoint.y(), mapPoint.y() }, { terrainZ, z } );
417 }
418
419 mZMax = std::max( mZMax, showCallouts ? std::max( 0.0, z ) : z );
420 mZMin = std::min( mZMin, showCallouts ? std::min( 0.0, z ) : z );
421 }
422 }
423 // free memory
424 mItemsToRender.clear();
425
426 if ( !textures.isEmpty() )
427 {
428 const QgsTextureAtlas atlas = QgsTextureAtlasGenerator::createFromImages( textures, 2048 );
429 if ( atlas.isValid() )
430 {
431 mBillboardAtlas = atlas.renderAtlasTexture();
432 mBillboardPositions.reserve( static_cast< int >( billboards.size() ) );
433 for ( Billboard &billboard : billboards )
434 {
435 const QRect textureRect = atlas.rect( billboard.textureId );
437 geometry.position = billboard.position;
438 geometry.textureAtlasOffset = QVector2D(
439 static_cast< float >( textureRect.left() ) / static_cast< float>( mBillboardAtlas.width() ),
440 1 - ( static_cast< float >( textureRect.bottom() ) / static_cast< float>( mBillboardAtlas.height() ) )
441 );
442 geometry.textureAtlasSize = QVector2D(
443 static_cast< float >( textureRect.width() ) / static_cast< float>( mBillboardAtlas.width() ), static_cast< float>( textureRect.height() ) / static_cast< float>( mBillboardAtlas.height() )
444 );
445 geometry.pixelOffset = QPoint( 0, textureRect.height() / 2 );
446 mBillboardPositions.append( geometry );
447 }
448 }
449 else
450 {
451 QgsDebugError( u"Error encountered building texture atlas"_s );
452 mBillboardAtlas = QImage();
453 }
454 }
455 else
456 {
457 mBillboardAtlas = QImage();
458 mBillboardPositions.clear();
459 }
460
461 if ( !textBillboardTexts.isEmpty() )
462 {
463 const QgsFontTextureAtlas atlas = QgsFontTextureAtlasGenerator::create( textFormat, textBillboardTexts );
464 if ( atlas.isValid() )
465 {
466 mTextBillboardAtlas = atlas.renderAtlasTexture();
467 mTextBillboardPositions.reserve( static_cast< int >( textBillboards.size() ) );
468 for ( TextBillboard &billboard : textBillboards )
469 {
470 int graphemeIndex = 0;
471 const int graphemeCount = atlas.graphemeCount( billboard.text );
472 // horizontally center text over point
473 const double xOffset = atlas.totalWidth( billboard.text ) / 2.0;
474 for ( ; graphemeIndex < graphemeCount; ++graphemeIndex )
475 {
476 const QRect textureRect = atlas.textureRectForGrapheme( billboard.text, graphemeIndex );
478 geometry.position = billboard.position;
479 geometry.textureAtlasOffset = QVector2D(
480 static_cast< float >( textureRect.left() ) / static_cast< float>( mTextBillboardAtlas.width() ),
481 1 - ( static_cast< float >( textureRect.bottom() ) / static_cast< float>( mTextBillboardAtlas.height() ) )
482 );
483 geometry.textureAtlasSize = QVector2D(
484 static_cast< float >( textureRect.width() ) / static_cast< float>( mTextBillboardAtlas.width() ),
485 static_cast< float>( textureRect.height() ) / static_cast< float>( mTextBillboardAtlas.height() )
486 );
487 const QPointF pixelOffset = atlas.pixelOffsetForGrapheme( billboard.text, graphemeIndex );
488 geometry.pixelOffset
489 = QPoint( static_cast< int >( std::round( -xOffset + pixelOffset.x() + 0.5 * textureRect.width() ) ), static_cast< int >( std::round( pixelOffset.y() + 0.5 * textureRect.height() ) ) );
490 mTextBillboardPositions.append( geometry );
491 }
492 }
493 }
494 else
495 {
496 QgsDebugError( u"Error encountered building font texture atlas"_s );
497 mTextBillboardAtlas = QImage();
498 }
499 }
500 else
501 {
502 mTextBillboardAtlas = QImage();
503 mTextBillboardPositions.clear();
504 }
505
506 // picture item billboards, grouped by picture source
507 for ( auto it = groupedPictures.constBegin(); it != groupedPictures.constEnd(); ++it )
508 {
509 QSizeF maxGroupSize( 0, 0 );
510 for ( auto picIt = it.value().constBegin(); picIt != it.value().constEnd(); ++picIt )
511 {
512 if ( picIt->size.width() > maxGroupSize.width() )
513 {
514 maxGroupSize.setWidth( picIt->size.width() );
515 }
516 if ( picIt->size.height() > maxGroupSize.height() )
517 {
518 maxGroupSize.setHeight( picIt->size.height() );
519 }
520 }
521
522 QImage image;
523 bool fitsInCache = false;
524
525 // can't zoom into these billboards, so we can use a fairly conservative texture size
526 constexpr int MAXIMUM_PICTURE_TEXTURE_SIZE_FIXED_SIZE = 256;
527 // can zoom into these, so we need a larger texture
528 constexpr int MAXIMUM_PICTURE_TEXTURE_SIZE_PERSPECTIVE = 1024;
529 const int textureSize = it.key().scaleMode == Qgis::BillboardScaleMode::Perspective ? MAXIMUM_PICTURE_TEXTURE_SIZE_PERSPECTIVE : MAXIMUM_PICTURE_TEXTURE_SIZE_FIXED_SIZE;
530 switch ( it.key().pictureFormat )
531 {
533 {
534 const QSize originalSize = QgsApplication::imageCache()->originalSize( it.key().path, true );
535 QSize imageSize = originalSize;
536 if ( imageSize.isEmpty() )
537 {
538 imageSize = maxGroupSize.toSize();
539 }
540 if ( imageSize.width() >= imageSize.height() && imageSize.width() > textureSize )
541 {
542 imageSize = QSize( textureSize, static_cast< int >( std::round( imageSize.height() * textureSize / imageSize.width() ) ) );
543 }
544 else if ( imageSize.height() > textureSize )
545 {
546 imageSize = QSize( static_cast< int >( std::round( imageSize.width() * textureSize / imageSize.height() ) ), textureSize );
547 }
548 image = QgsApplication::imageCache()->pathAsImage( it.key().path, imageSize, false, 1.0, fitsInCache, true );
549 break;
550 }
551
553 {
554 const QPicture picture = QgsApplication::svgCache()->svgAsPicture( it.key().path, textureSize, QColor(), QColor(), 1.0, 1.0, false, 0, true );
555 if ( !picture.isNull() && picture.boundingRect().width() > 0 && picture.boundingRect().height() > 0 )
556 {
557 const QRectF picRect = picture.boundingRect();
558 QSize imageSize = picRect.size().toSize();
559 if ( imageSize.width() >= imageSize.height() )
560 {
561 imageSize = QSize( textureSize, static_cast< int >( std::round( picRect.height() * static_cast< double >( textureSize ) / picRect.width() ) ) );
562 }
563 else
564 {
565 imageSize = QSize( static_cast< int >( std::round( picRect.width() * static_cast< double >( textureSize ) / picRect.height() ) ), textureSize );
566 }
567
568 image = QImage( imageSize, QImage::Format_ARGB32_Premultiplied );
569 image.fill( Qt::transparent );
570
571 const double scale = static_cast< double >( imageSize.width() ) / picRect.width();
572
573 QPainter painter( &image );
574 painter.setRenderHint( QPainter::Antialiasing );
575 painter.scale( scale, scale );
576
577 QgsPainting::drawPicture( &painter, QPointF( picRect.width() / 2.0, picRect.height() / 2.0 ), picture );
578 painter.end();
579 }
580 break;
581 }
582
584 continue;
585 }
586
587 PictureBillboards billboard;
588 billboard.scaleMode = it.key().scaleMode;
589 billboard.image = image;
590 billboard.positions.reserve( it.value().size() );
591 billboard.sizes.reserve( it.value().size() );
592 for ( const PictureBillboard &item : it.value() )
593 {
594 billboard.positions.append( item.position );
595 billboard.sizes.append( item.size );
596 }
597
598 mPictureBillboards.append( billboard );
599 }
600 } );
601
602 // emit finished() as soon as the handler is populated with features
603 mFutureWatcher->setFuture( future );
604}
605
606QgsAnnotationLayerChunkLoader::~QgsAnnotationLayerChunkLoader()
607{
608 if ( mFutureWatcher && !mFutureWatcher->isFinished() )
609 {
610 disconnect( mFutureWatcher, &QFutureWatcher<void>::finished, this, &QgsChunkQueueJob::finished );
611 mFutureWatcher->waitForFinished();
612 }
613}
614
615void QgsAnnotationLayerChunkLoader::cancel()
616{
617 mCanceled = true;
618}
619
620Qt3DCore::QEntity *QgsAnnotationLayerChunkLoader::createEntity( Qt3DCore::QEntity *parent )
621{
622 if ( mNode->level() < mFactory->mLeafLevel )
623 {
624 Qt3DCore::QEntity *entity = new Qt3DCore::QEntity( parent ); // dummy entity
625 entity->setObjectName( mLayerName + "_CONTAINER_" + mNode->tileId().text() );
626 return entity;
627 }
628
629 if ( mBillboardPositions.empty() && mTextBillboardPositions.empty() && mPictureBillboards.empty() )
630 {
631 // an empty node, so we return no entity. This tags the node as having no data and effectively removes it.
632 // we just make sure first that its initial estimated vertical range does not affect its parents' bboxes calculation
633 mNode->setExactBox3D( QgsBox3D() );
634 mNode->updateParentBoundingBoxesRecursively();
635 return nullptr;
636 }
637
638 Qt3DCore::QEntity *entity = new Qt3DCore::QEntity( parent );
639 entity->setObjectName( mLayerName + "_" + mNode->tileId().text() );
640
641 QgsGeoTransform *billboardTransform = new QgsGeoTransform;
642 billboardTransform->setGeoTranslation( mChunkOrigin );
643 entity->addComponent( billboardTransform );
644
645 if ( !mBillboardPositions.empty() )
646 {
647 QgsBillboardGeometry *billboardGeometry = new QgsBillboardGeometry();
648 billboardGeometry->setBillboardData( mBillboardPositions, true );
649
650 Qt3DRender::QGeometryRenderer *billboardGeometryRenderer = new Qt3DRender::QGeometryRenderer;
651 billboardGeometryRenderer->setPrimitiveType( Qt3DRender::QGeometryRenderer::TriangleStrip );
652 billboardGeometryRenderer->setGeometry( billboardGeometry );
653 billboardGeometryRenderer->setVertexCount( 4 );
654 billboardGeometryRenderer->setInstanceCount( mBillboardPositions.count() );
655
657 billboardMaterial->setTexture2DFromImage( mBillboardAtlas );
658
659 Qt3DCore::QEntity *billboardEntity = new Qt3DCore::QEntity;
660 billboardEntity->addComponent( billboardMaterial );
661 billboardEntity->addComponent( billboardGeometryRenderer );
662 billboardEntity->setParent( entity );
663 }
664
665 if ( !mTextBillboardPositions.empty() )
666 {
667 QgsBillboardGeometry *textBillboardGeometry = new QgsBillboardGeometry();
668 textBillboardGeometry->setBillboardData( mTextBillboardPositions, true );
669
670 Qt3DRender::QGeometryRenderer *billboardGeometryRenderer = new Qt3DRender::QGeometryRenderer;
671 billboardGeometryRenderer->setPrimitiveType( Qt3DRender::QGeometryRenderer::TriangleStrip );
672 billboardGeometryRenderer->setGeometry( textBillboardGeometry );
673 billboardGeometryRenderer->setVertexCount( 4 );
674 billboardGeometryRenderer->setInstanceCount( mTextBillboardPositions.count() );
675
677 billboardMaterial->setTexture2DFromImage( mTextBillboardAtlas );
678
679 Qt3DCore::QEntity *billboardEntity = new Qt3DCore::QEntity;
680 billboardEntity->addComponent( billboardMaterial );
681 billboardEntity->addComponent( billboardGeometryRenderer );
682 billboardEntity->setParent( entity );
683 }
684
685 for ( const PictureBillboards &pictureBillboard : mPictureBillboards )
686 {
687 QgsBillboardGeometry *pictureGeometry = new QgsBillboardGeometry();
688 pictureGeometry->setPositionsAndSizes( pictureBillboard.positions, pictureBillboard.sizes );
689
690 Qt3DRender::QGeometryRenderer *pictureGeometryRenderer = new Qt3DRender::QGeometryRenderer;
691 pictureGeometryRenderer->setPrimitiveType( Qt3DRender::QGeometryRenderer::TriangleStrip );
692 pictureGeometryRenderer->setGeometry( pictureGeometry );
693 pictureGeometryRenderer->setVertexCount( 4 );
694 pictureGeometryRenderer->setInstanceCount( static_cast< int >( pictureBillboard.positions.size() ) );
695
696 QgsPoint3DBillboardMaterial *pictureMaterial
698 pictureMaterial->setTexture2DFromImage( pictureBillboard.image );
699 // picture billboards should be vertically anchored to the bottom of the picture
700 pictureMaterial->setVerticalOffset( 0.5 );
701
702 Qt3DCore::QEntity *pictureEntity = new Qt3DCore::QEntity;
703 pictureEntity->addComponent( pictureMaterial );
704 pictureEntity->addComponent( pictureGeometryRenderer );
705 pictureEntity->setParent( entity );
706 }
707
708 if ( mFactory->mShowCallouts )
709 {
710 QgsLineVertexData lineData;
711 lineData.withAdjacency = true;
712 lineData.geocentricCoordinates = false; // mMapSettings->sceneMode() == Qgis::SceneMode::Globe;
713 lineData.init( Qgis::AltitudeClamping::Absolute, Qgis::AltitudeBinding::Vertex, 0, mRenderContext, mChunkOrigin );
714
715 for ( const QgsLineString &line : mCalloutLines )
716 {
717 lineData.addLineString( line, 0, false );
718 }
719
720 QgsLineMaterial *mat = new QgsLineMaterial;
721 mat->setLineColor( mFactory->mCalloutLineColor );
722 mat->setLineWidth( mFactory->mCalloutLineWidth );
723
724 Qt3DCore::QEntity *calloutEntity = new Qt3DCore::QEntity;
725 calloutEntity->setObjectName( parent->objectName() + "_CALLOUTS" );
726
727 // geometry renderer
728 Qt3DRender::QGeometryRenderer *calloutRenderer = new Qt3DRender::QGeometryRenderer;
729 calloutRenderer->setPrimitiveType( Qt3DRender::QGeometryRenderer::LineStripAdjacency );
730 calloutRenderer->setGeometry( lineData.createGeometry( calloutEntity ) );
731 calloutRenderer->setVertexCount( lineData.indexes.count() );
732 calloutRenderer->setPrimitiveRestartEnabled( true );
733 calloutRenderer->setRestartIndexValue( 0 );
734
735 // make entity
736 calloutEntity->addComponent( calloutRenderer );
737 calloutEntity->addComponent( mat );
738
739 calloutEntity->setParent( entity );
740 }
741
742 // fix the vertical range of the node from the estimated vertical range to the true range
743 if ( mZMin != std::numeric_limits<float>::max() && mZMax != std::numeric_limits<float>::lowest() )
744 {
745 QgsBox3D box = mNode->box3D();
746 box.setZMinimum( mZMin );
747 box.setZMaximum( mZMax );
748 mNode->setExactBox3D( box );
749 mNode->updateParentBoundingBoxesRecursively();
750 }
751 return entity;
752}
753
754
756
757
758QgsAnnotationLayerChunkLoaderFactory::QgsAnnotationLayerChunkLoaderFactory(
759 const Qgs3DRenderContext &context,
760 QgsAnnotationLayer *layer,
761 int leafLevel,
762 Qgis::AltitudeClamping clamping,
763 double zOffset,
764 bool showCallouts,
765 const QColor &calloutLineColor,
766 double calloutLineWidth,
767 const QgsTextFormat &textFormat,
768 double zMin,
769 double zMax
770)
771 : mRenderContext( context )
772 , mLayer( layer )
773 , mLeafLevel( leafLevel )
774 , mClamping( clamping )
775 , mZOffset( zOffset )
776 , mShowCallouts( showCallouts )
777 , mCalloutLineColor( calloutLineColor )
778 , mCalloutLineWidth( calloutLineWidth )
779 , mTextFormat( textFormat )
780{
781 if ( context.crs().type() == Qgis::CrsType::Geocentric )
782 {
783 // TODO: add support for handling of annotation layers
784 // (we're using dummy quadtree here to make sure the empty extent does not break the scene completely)
785 QgsDebugError( u"Annotation layers in globe scenes are not supported yet!"_s );
786 setupQuadtree( QgsBox3D( -1e7, -1e7, -1e7, 1e7, 1e7, 1e7 ), -1, leafLevel );
787 return;
788 }
789
790 // choose the smaller root extent between context and mLayer ones:
791 QgsRectangle extent = context.extent();
792 const QgsRectangle layerExtentInMapCrs = Qgs3DUtils::tryReprojectExtent2D( mLayer->extent(), mLayer->crs(), context.crs(), context.transformContext() );
793 if ( layerExtentInMapCrs.isValid() )
794 {
795 extent = context.extent().intersect( layerExtentInMapCrs );
796 }
797 if ( extent.isValid() )
798 {
799 QgsBox3D rootBox3D( extent, zMin, zMax );
800
801 // add small padding to avoid clipping of point features located at the edge of the bounding box
802 rootBox3D.grow( 1.0 );
803 setupQuadtree( rootBox3D, -1, leafLevel ); // negative root error means that the node does not contain anything
804 }
805}
806
807QgsChunkLoader *QgsAnnotationLayerChunkLoaderFactory::createChunkLoader( QgsChunkNode *node ) const
808{
809 return new QgsAnnotationLayerChunkLoader( this, node );
810}
811
812
814
815
816QgsAnnotationLayerChunkedEntity::QgsAnnotationLayerChunkedEntity(
817 Qgs3DMapSettings *map,
818 QgsAnnotationLayer *layer,
819 Qgis::AltitudeClamping clamping,
820 double zOffset,
821 bool showCallouts,
822 const QColor &calloutLineColor,
823 double calloutLineWidth,
824 const QgsTextFormat &textFormat,
825 double zMin,
826 double zMax
827)
828 : QgsAbstractFeatureBasedChunkedEntity(
829 map,
830 -1, // max. allowed screen error (negative tau means that we need to go until leaves are reached)
831 new QgsAnnotationLayerChunkLoaderFactory( Qgs3DRenderContext::fromMapSettings( map ), layer, 3, clamping, zOffset, showCallouts, calloutLineColor, calloutLineWidth, textFormat, zMin, zMax ),
832 true
833 )
834{
835 onTerrainElevationOffsetChanged();
836}
837
838QgsAnnotationLayerChunkedEntity::~QgsAnnotationLayerChunkedEntity()
839{
840 // cancel / wait for jobs
841 cancelActiveJobs();
842}
843
844// if the AltitudeClamping is `Absolute`, do not apply the offset
845bool QgsAnnotationLayerChunkedEntity::applyTerrainOffset() const
846{
847 if ( auto loaderFactory = static_cast<QgsAnnotationLayerChunkLoaderFactory *>( mChunkLoaderFactory ) )
848 {
849 return loaderFactory->mClamping != Qgis::AltitudeClamping::Absolute;
850 }
851 return true;
852}
853
854QList<QgsRayCastHit> QgsAnnotationLayerChunkedEntity::rayIntersection( const QgsRay3D &ray, const QgsRayCastContext &context ) const
855{
856 Q_UNUSED( ray )
857 Q_UNUSED( context )
858 return {};
859}
860
861
AltitudeClamping
Altitude clamping.
Definition qgis.h:4201
@ Relative
Elevation is relative to terrain height (final elevation = terrain elevation + feature elevation).
Definition qgis.h:4203
@ Terrain
Elevation is clamped to terrain (final elevation = terrain elevation).
Definition qgis.h:4204
@ Absolute
Elevation is taken directly from feature and is independent of terrain height (final elevation = feat...
Definition qgis.h:4202
@ Geocentric
Geocentric CRS.
Definition qgis.h:2496
PictureFormat
Picture formats.
Definition qgis.h:5776
@ Raster
Raster image.
Definition qgis.h:5778
@ Unknown
Invalid or unknown image type.
Definition qgis.h:5779
@ SVG
SVG image.
Definition qgis.h:5777
@ Vertex
Clamp every vertex of feature.
Definition qgis.h:4215
BillboardScaleMode
3D billboard scaling modes.
Definition qgis.h:4441
@ Perspective
Billboard size is scaled with perspective distance from camera, using world units.
Definition qgis.h:4443
@ SpatialBounds
Item is rendered inside fixed spatial bounds, and size will depend on map scale.
Definition qgis.h:2668
@ FixedSize
Item is rendered at a fixed size, regardless of map scale. Item's location is georeferenced to a spat...
Definition qgis.h:2669
@ RelativeToMapFrame
Items size and placement is relative to the map's frame, and the item will always be rendered in the ...
Definition qgis.h:2670
@ Reverse
Reverse/inverse transform (from destination to source).
Definition qgis.h:2864
Definition of the world.
Rendering context for preparation of 3D entities.
QgsCoordinateReferenceSystem crs() const
Returns the coordinate reference system used in the 3D scene.
QgsRectangle extent() const
Returns the 3D scene's 2D extent in the 3D scene's CRS.
QgsCoordinateTransformContext transformContext() const
Returns the coordinate transform context, which stores various information regarding which datum tran...
static QgsRectangle tryReprojectExtent2D(const QgsRectangle &extent, const QgsCoordinateReferenceSystem &crs1, const QgsCoordinateReferenceSystem &crs2, const QgsCoordinateTransformContext &context)
Reprojects extent from crs1 to crs2 coordinate reference system with context context.
Abstract base class for annotation items which are drawn with QgsAnnotationLayers.
bool enabled() const
Returns true if the item is enabled and will be rendered in the layer.
Represents a map layer containing a set of georeferenced annotations, e.g.
An annotation item which renders text along a line geometry.
An annotation item which renders a marker symbol at a point location.
An annotation item which renders a picture.
An annotation item which renders a text string at a point location.
An annotation item which renders paragraphs of text within a rectangle.
static QgsImageCache * imageCache()
Returns the application's image cache, used for caching resampled versions of raster images.
static QgsSvgCache * svgCache()
Returns the application's SVG cache, used for caching SVG images and handling parameter replacement w...
Geometry of the billboard rendering for points in 3D map view.
void setBillboardData(const QVector< QgsBillboardGeometry::BillboardAtlasData > &billboards, bool includePixelOffsets=false)
Set the position and texture data for the billboard.
void setPositionsAndSizes(const QVector< QVector3D > &positions, const QVector< QSizeF > &sizes)
Sets per-instance positions and sizes for billboards sharing a single texture.
A 3-dimensional box composed of x, y, z coordinates.
Definition qgsbox3d.h:45
void setZMinimum(double z)
Sets the minimum z value.
Definition qgsbox3d.cpp:94
void setZMaximum(double z)
Sets the maximum z value.
Definition qgsbox3d.cpp:99
Qgis::CrsType type() const
Returns the type of the CRS.
Handles coordinate transforms between two coordinate systems.
Custom exception class for Coordinate Reference System related exceptions.
QString what() const
static QList< QgsExpressionContextScope * > globalProjectLayerScopes(const QgsMapLayer *layer)
Creates a list of three scopes: global, layer's project and layer.
Expression contexts are used to encapsulate the parameters around which a QgsExpression should be eva...
void appendScopes(const QList< QgsExpressionContextScope * > &scopes)
Appends a list of scopes to the end of the context.
static QgsFontTextureAtlas create(const QgsTextFormat &format, const QStringList &strings)
Creates the texture atlas for a set of strings, using the specified text format.
Encapsulates a font texture atlas.
int graphemeCount(const QString &string) const
Returns the number of graphemes to render for a given string.
bool isValid() const
Returns true if the atlas is valid.
QImage renderAtlasTexture() const
Renders the combined texture atlas, containing all required characters.
int totalWidth(const QString &string) const
Returns the total width (in pixels) required for a given string.
QRect textureRectForGrapheme(const QString &string, int graphemeIndex) const
Returns the packed rectangle for the texture for the matching grapheme.
QPoint pixelOffsetForGrapheme(const QString &string, int graphemeIndex) const
Returns the pixel offset at which the texture for the matching grapheme should be placed.
Does vector analysis using the GEOS library and handles import, export, and exception handling.
Definition qgsgeos.h:175
QSize originalSize(const QString &path, bool blocking=false) const
Returns the original size (in pixels) of the image at the specified path.
QImage pathAsImage(const QString &path, const QSize size, const bool keepAspectRatio, const double opacity, bool &fitsInCache, bool blocking=false, double targetDpi=96, int frameNumber=-1, bool *isMissing=nullptr)
Returns the specified path rendered as an image.
Line string geometry type, with support for z-dimension and m-values.
QString name
Definition qgsmaplayer.h:87
QgsCoordinateReferenceSystem crs
Definition qgsmaplayer.h:90
static void drawPicture(QPainter *painter, const QPointF &point, const QPicture &picture)
Draws a picture onto a painter, correctly applying workarounds to avoid issues with incorrect scaling...
Material of the billboard rendering for points in 3D map view.
void setVerticalOffset(float offset)
Set the vertical offset.
void setTexture2DFromImage(const QImage &image)
Set the texture2D of the billboard from an image.
Represents a 2D point.
Definition qgspointxy.h:62
double y
Definition qgspointxy.h:66
double x
Definition qgspointxy.h:65
A representation of a ray in 3D.
Definition qgsray3d.h:31
Responsible for defining parameters of the ray casting operations in 3D map canvases.
A rectangle specified with double values.
QgsPointXY center
QPicture svgAsPicture(const QString &path, double size, const QColor &fill, const QColor &stroke, double strokeWidth, double widthScaleFactor, bool forceVectorOutput=false, double fixedAspectRatio=0, bool blocking=false, const QMap< QString, QString > &parameters=QMap< QString, QString >())
Returns an SVG drawing as a QPicture.
Represents a document consisting of one or more QgsTextBlock objects.
QStringList toPlainText() const
Returns a list of plain text lines of text representing the document.
static QgsTextDocument fromTextAndFormat(const QStringList &lines, const QgsTextFormat &format)
Constructor for QgsTextDocument consisting of a set of lines, respecting settings from a text format.
Container for all settings relating to text rendering.
static QgsTextureAtlas createFromImages(const QVector< QImage > &images, int maxSide=1000)
Creates a texture atlas for a set of images.
Encapsulates a texture atlas.
bool isValid() const
Returns true if the atlas is valid.
QRect rect(int index) const
Returns the packed rectangle for the texture with the specified index.
QImage renderAtlasTexture() const
Renders the combined texture atlas, containing all source images.
A 3D vector (similar to QVector3D) with the difference that it uses double precision instead of singl...
Definition qgsvector3d.h:33
Contains geos related utilities and functions.
Definition qgsgeos.h:112
bool operator<(const QVariant &v1, const QVariant &v2)
Compares two QVariant values and returns whether the first is less than the second.
Definition qgis.h:8047
#define QgsDebugError(str)
Definition qgslogger.h:71
Contains the billboard positions and texture information.
QPoint pixelOffset
Optional pixel offset for billboard.
QVector3D position
Vertex position for billboard placement.
QVector2D textureAtlasOffset
Texture atlas offset for associated billboard texture.
QVector2D textureAtlasSize
Texture atlas size for associated billboard texture.