QGIS API Documentation 4.3.0-Master (52aa6503bed)
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 <functional>
19
20#include "qgs3dutils.h"
21#include "qgsabstract3dsymbol.h"
23#include "qgsannotationitem.h"
24#include "qgsannotationlayer.h"
30#include "qgsapplication.h"
32#include "qgschunkloader.h"
33#include "qgschunknode.h"
34#include "qgseventtracing.h"
37#include "qgsgeos.h"
38#include "qgsgeotransform.h"
39#include "qgsimagecache.h"
40#include "qgslinematerial_p.h"
41#include "qgslinevertexdata_p.h"
42#include "qgslogger.h"
43#include "qgsmarkersymbol.h"
44#include "qgspainting.h"
46#include "qgssvgcache.h"
48#include "qgstextdocument.h"
50#include "qgsthreadingutils.h"
51
52#include <QString>
53#include <QTimer>
54#include <Qt3DCore/QTransform>
55#include <Qt3DRender/QGeometryRenderer>
56#include <QtConcurrentRun>
57
58#include "moc_qgsannotationlayerchunkloader_p.cpp"
59
60using namespace Qt::StringLiterals;
61
63
64namespace
65{
66 struct Billboard
67 {
68 QVector3D position;
69 int textureId = -1;
70 const QgsMarkerSymbol *markerSymbol = nullptr;
71 };
72
73 struct TextBillboard
74 {
75 QVector3D position;
76 QString text;
77 };
78
82 struct PictureBillboardGroup
83 {
84 QString path;
86 Qgis::PictureFormat pictureFormat;
87
88 bool operator<( const PictureBillboardGroup &other ) const
89 {
90 if ( path != other.path )
91 return path < other.path;
92 return scaleMode < other.scaleMode;
93 }
94 };
95
96 struct PictureBillboard
97 {
98 QVector3D position;
99 QSizeF size;
100 };
101
102} //namespace
103
104QgsAnnotationLayerChunkLoader::QgsAnnotationLayerChunkLoader(
105 const Qgs3DRenderContext &context,
106 QgsAnnotationLayer *layer,
107 int leafLevel,
108 Qgis::AltitudeClamping clamping,
109 double zOffset,
110 bool showCallouts,
111 const QColor &calloutLineColor,
112 double calloutLineWidth,
113 const QgsTextFormat &textFormat,
114 double zMin,
115 double zMax
116)
117 : mRenderContext( context )
118 , mLayer( layer )
119 , mLeafLevel( leafLevel )
120 , mData {
121 .mClamping = clamping,
122 .mZOffset = zOffset,
123 .mShowCallouts = showCallouts,
124 .mCalloutLineColor = calloutLineColor,
125 .mCalloutLineWidth = calloutLineWidth,
126 .mTextFormat = textFormat,
127 }
128{
129 if ( context.crs().type() == Qgis::CrsType::Geocentric )
130 {
131 // TODO: add support for handling of annotation layers
132 // (we're using dummy quadtree here to make sure the empty extent does not break the scene completely)
133 QgsDebugError( u"Annotation layers in globe scenes are not supported yet!"_s );
134 setupQuadtree( QgsBox3D( -1e7, -1e7, -1e7, 1e7, 1e7, 1e7 ), -1, leafLevel );
135 return;
136 }
137
138 // choose the smaller root extent between context and mLayer ones:
139 QgsRectangle extent = context.extent();
140 const QgsRectangle layerExtentInMapCrs = Qgs3DUtils::tryReprojectExtent2D( mLayer->extent(), mLayer->crs(), context.crs(), context.transformContext() );
141 if ( layerExtentInMapCrs.isValid() )
142 {
143 extent = context.extent().intersect( layerExtentInMapCrs );
144 }
145 if ( extent.isValid() )
146 {
147 QgsBox3D rootBox3D( extent, zMin, zMax );
148
149 // add small padding to avoid clipping of point features located at the edge of the bounding box
150 rootBox3D.grow( 1.0 );
151 setupQuadtree( rootBox3D, -1, leafLevel ); // negative root error means that the node does not contain anything
152 }
153}
154
155QFuture<QgsChunkLoaderResult> QgsAnnotationLayerChunkLoader::loadChunk( QgsChunkNode *node )
156{
157 if ( node->level() < mLeafLevel )
158 {
159 return QtFuture::makeReadyValueFuture<QgsChunkLoaderResult>( { [this, node]( Qt3DCore::QEntity *parent ) {
161 Qt3DCore::QEntity *entity = new Qt3DCore::QEntity( parent ); // dummy entity
162 entity->setObjectName( mLayer->name() + "_CONTAINER_" + node->tileId().text() );
163 return entity;
164 } } );
165 }
166
167 auto renderCtx = mRenderContext; // Copy, since we mutate it locally
168
169 // only a subset of data to be queried
170 const QgsRectangle rect = node->box3D().toRectangle();
171 // origin for coordinates of the chunk - it is kind of arbitrary, but it should be
172 // picked so that the coordinates are relatively small to avoid numerical precision issues
173 QgsVector3D chunkOrigin = QgsVector3D( rect.center().x(), rect.center().y(), 0 );
174
175 QgsExpressionContext exprContext;
177 renderCtx.setExpressionContext( exprContext );
178
179 QgsCoordinateTransform layerToMapTransform( mLayer->crs(), renderCtx.crs(), renderCtx.transformContext() );
180
181 QgsRectangle layerExtent;
182 try
183 {
184 layerExtent = layerToMapTransform.transformBoundingBox( rect, Qgis::TransformDirection::Reverse );
185 }
186 catch ( QgsCsException &e )
187 {
188 QgsDebugError( u"Error transforming annotation layer extent to 3d map extent: %1"_s.arg( e.what() ) );
189 return QtFuture::makeReadyValueFuture( QgsChunkLoaderResult::sEmpty );
190 }
191
192 // see logic from QgsAnnotationLayerRenderer
193 const QStringList itemsList = mLayer->queryIndex( layerExtent );
194 QSet< QString > itemIds( itemsList.begin(), itemsList.end() );
195
196 // we also have NO choice but to clone ALL non-indexed items (i.e. those with a scale-dependent bounding box)
197 // since these won't be in the layer's spatial index, and it's too expensive to determine their actual bounding box
198 // upfront (we are blocking the main thread right now!)
199
200 // TODO -- come up with some brilliant way to avoid this and also index scale-dependent items ;)
201 itemIds.unite( mLayer->mNonIndexedItems );
202
203 std::vector< std::unique_ptr< QgsAnnotationItem > > itemsToRender;
204 itemsToRender.reserve( itemIds.size() );
205 std::transform( itemIds.begin(), itemIds.end(), std::back_inserter( itemsToRender ), [this]( const QString &id ) -> std::unique_ptr< QgsAnnotationItem > {
206 return std::unique_ptr< QgsAnnotationItem >( mLayer->item( id )->clone() );
207 } );
208
209 QString layerName = mLayer->name();
210
211 //
212 // this will be run in a background thread
213 //
214 return QtConcurrent::run( &QgsAnnotationLayerChunkLoader::loadChunkInWorker, node, rect, layerToMapTransform, layerName, std::move( itemsToRender ), chunkOrigin, renderCtx, mData )
215 .then( this, [this]( ChunkData chunkData ) {
217 return QgsChunkLoaderResult { std::bind_front( &QgsAnnotationLayerChunkLoader::createEntity, this, std::move( chunkData ) ) };
218 } );
219}
220
221void QgsAnnotationLayerChunkLoader::loadChunkInWorker(
222 QPromise<ChunkData> &promise,
223 QgsChunkNode *node,
224 const QgsRectangle &rect,
225 const QgsCoordinateTransform &layerToMapTransform,
226 const QString &layerName,
227 const std::vector< std::unique_ptr< QgsAnnotationItem > > &itemsToRender,
228 const QgsVector3D &chunkOrigin,
229 const Qgs3DRenderContext &renderCtx,
230 const QgsAnnotationLayerChunkLoader::LoaderData &data
231)
232{
233 const QgsScopedEvent e( u"3D"_s, u"Annotation layer chunk load"_s );
234
235 QVector< QgsLineString > calloutLines;
236 double zMin = std::numeric_limits< double >::max();
237 double zMax = std::numeric_limits< double >::lowest();
238
239 std::vector< Billboard > billboards;
240 billboards.reserve( itemsToRender.size() );
241 QVector< QImage > textures;
242 textures.reserve( static_cast< qsizetype >( itemsToRender.size() ) );
243
244 std::vector< TextBillboard > textBillboards;
245 textBillboards.reserve( itemsToRender.size() );
246 QStringList textBillboardTexts;
247 textBillboardTexts.reserve( static_cast< qsizetype >( itemsToRender.size() ) );
248
249 QMap< PictureBillboardGroup, QVector< PictureBillboard > > groupedPictures;
250
251 QVector< PictureBillboards > pictureBillboards;
252 pictureBillboards.reserve( static_cast< qsizetype >( itemsToRender.size() ) );
253
254 auto addTextBillboard = [layerToMapTransform,
255 rect,
256 &data,
257 &textBillboards,
258 &textBillboardTexts,
259 &chunkOrigin,
260 &calloutLines,
261 &zMax,
262 &zMin,
263 &renderCtx]( const QgsPointXY &p, const QString &annotationText, const QgsTextFormat &annotationTextFormat ) {
264 QString text = annotationText;
265 if ( annotationTextFormat.allowHtmlFormatting() )
266 {
267 // strip HTML characters, we don't support those in 3D
268 const QgsTextDocument document = QgsTextDocument::fromTextAndFormat( { text }, annotationTextFormat );
269 text = document.toPlainText().join( ' ' );
270 }
271 if ( !text.isEmpty() )
272 {
273 try
274 {
275 const QgsPointXY mapPoint = layerToMapTransform.transform( p );
276 if ( !rect.contains( mapPoint ) )
277 return;
278
279 double z = 0;
280 const float terrainZ = ( data.mClamping == Qgis::AltitudeClamping::Absolute && !data.mShowCallouts ) ? 0
281 : renderCtx.terrainRenderingEnabled() && renderCtx.terrainGenerator()
282 ? static_cast<float>( renderCtx.terrainGenerator()->heightAt( mapPoint.x(), mapPoint.y(), renderCtx ) * renderCtx.terrainSettings()->verticalScale() )
283 : 0.f;
284
285 switch ( data.mClamping )
286 {
287 case Qgis::AltitudeClamping::Absolute:
288 z = data.mZOffset;
289 break;
290 case Qgis::AltitudeClamping::Terrain:
291 z = terrainZ;
292 break;
293 case Qgis::AltitudeClamping::Relative:
294 z = terrainZ + data.mZOffset;
295 break;
296 }
297
298 TextBillboard billboard;
299 billboard.position = ( QgsVector3D( mapPoint.x(), mapPoint.y(), z ) - chunkOrigin ).toVector3D();
300 billboard.text = text;
301 textBillboards.emplace_back( std::move( billboard ) );
302 textBillboardTexts.append( text );
303
304 if ( data.mShowCallouts )
305 {
306 calloutLines << QgsLineString( { mapPoint.x(), mapPoint.x() }, { mapPoint.y(), mapPoint.y() }, { terrainZ, z } );
307 }
308
309 zMax = std::max( zMax, data.mShowCallouts ? std::max( 0.0, z ) : z );
310 zMin = std::min( zMin, data.mShowCallouts ? std::min( 0.0, z ) : z );
311 }
312 catch ( QgsCsException &e )
313 {
314 QgsDebugError( e.what() );
315 }
316 }
317 };
318
319 for ( const std::unique_ptr< QgsAnnotationItem > &item : std::as_const( itemsToRender ) )
320 {
321 if ( promise.isCanceled() )
322 break;
323
324 QgsAnnotationItem *annotation = item.get();
325
326 if ( !annotation->enabled() )
327 continue;
328
329 if ( QgsAnnotationMarkerItem *marker = dynamic_cast< QgsAnnotationMarkerItem * >( annotation ) )
330 {
331 if ( marker->symbol() )
332 {
333 QgsPointXY p = marker->geometry();
334 try
335 {
336 const QgsPointXY mapPoint = layerToMapTransform.transform( p );
337 if ( !rect.contains( mapPoint ) )
338 continue;
339
340 double z = 0;
341 const float terrainZ = ( data.mClamping == Qgis::AltitudeClamping::Absolute && !data.mShowCallouts ) ? 0
342 : renderCtx.terrainRenderingEnabled() && renderCtx.terrainGenerator()
343 ? static_cast<float>( renderCtx.terrainGenerator()->heightAt( mapPoint.x(), mapPoint.y(), renderCtx ) * renderCtx.terrainSettings()->verticalScale() )
344 : 0.f;
345
346 switch ( data.mClamping )
347 {
348 case Qgis::AltitudeClamping::Absolute:
349 z = data.mZOffset;
350 break;
351 case Qgis::AltitudeClamping::Terrain:
352 z = terrainZ;
353 break;
354 case Qgis::AltitudeClamping::Relative:
355 z = terrainZ + data.mZOffset;
356 break;
357 }
358
359 Billboard billboard;
360 billboard.position = ( QgsVector3D( mapPoint.x(), mapPoint.y(), z ) - chunkOrigin ).toVector3D();
361 billboard.textureId = -1;
362
363 for ( const Billboard &existingBillboard : billboards )
364 {
365 if ( existingBillboard.markerSymbol && marker->symbol()->rendersIdenticallyTo( existingBillboard.markerSymbol ) )
366 {
367 // marker symbol has been reused => reuse existing texture to minimize size of texture atlas
368 billboard.textureId = existingBillboard.textureId;
369 break;
370 }
371 }
372
373 if ( billboard.textureId < 0 )
374 {
375 // could not match to previously considered marker, have to render and add to texture atlas
376 billboard.markerSymbol = marker->symbol();
377 billboard.textureId = textures.size();
378 textures.append( QgsPoint3DBillboardMaterial::renderSymbolToImage( marker->symbol(), renderCtx ) );
379 }
380 billboards.emplace_back( std::move( billboard ) );
381
382 if ( data.mShowCallouts )
383 {
384 calloutLines << QgsLineString( { mapPoint.x(), mapPoint.x() }, { mapPoint.y(), mapPoint.y() }, { terrainZ, z } );
385 }
386
387 zMax = std::max( zMax, data.mShowCallouts ? std::max( 0.0, z ) : z );
388 zMin = std::min( zMin, data.mShowCallouts ? std::min( 0.0, z ) : z );
389 }
390 catch ( QgsCsException &e )
391 {
392 QgsDebugError( e.what() );
393 }
394 }
395 }
396 else if ( QgsAnnotationPointTextItem *pointText = dynamic_cast< QgsAnnotationPointTextItem * >( annotation ) )
397 {
398 addTextBillboard( pointText->point(), pointText->text(), pointText->format() );
399 }
400 else if ( QgsAnnotationLineTextItem *lineText = dynamic_cast< QgsAnnotationLineTextItem * >( annotation ) )
401 {
402 QgsGeos geos( lineText->geometry() );
403 std::unique_ptr< QgsPoint > point( geos.pointOnSurface() );
404 if ( point )
405 {
406 addTextBillboard( *point, lineText->text(), lineText->format() );
407 }
408 }
409 else if ( QgsAnnotationRectangleTextItem *rectText = dynamic_cast< QgsAnnotationRectangleTextItem * >( annotation ) )
410 {
411 switch ( rectText->placementMode() )
412 {
415 {
416 addTextBillboard( rectText->bounds().center(), rectText->text(), rectText->format() );
417 break;
418 }
420 // ignore these annotations, they don't have a fix map position
421 break;
422 }
423 }
424 else if ( auto pictureItem = dynamic_cast< QgsAnnotationPictureItem * >( annotation ) )
425 {
426 if ( pictureItem->path().isEmpty() )
427 continue;
428
429 if ( pictureItem->placementMode() == Qgis::AnnotationPlacementMode::RelativeToMapFrame )
430 {
431 // annotations relative to the map frame have no geographic position => ignore
432 continue;
433 }
434
435 const QgsPointXY p = pictureItem->bounds().center();
436 QgsPointXY mapPoint;
437 try
438 {
439 mapPoint = layerToMapTransform.transform( p );
440 }
441 catch ( QgsCsException &e )
442 {
443 QgsDebugError( e.what() );
444 continue;
445 }
446
447 if ( !rect.contains( mapPoint ) )
448 continue;
449
450 double z = 0;
451 const float terrainZ = ( data.mClamping == Qgis::AltitudeClamping::Absolute && !data.mShowCallouts ) ? 0
452 : renderCtx.terrainRenderingEnabled() && renderCtx.terrainGenerator()
453 ? static_cast<float>( renderCtx.terrainGenerator()->heightAt( mapPoint.x(), mapPoint.y(), renderCtx ) * renderCtx.terrainSettings()->verticalScale() )
454 : 0.f;
455
456 switch ( data.mClamping )
457 {
458 case Qgis::AltitudeClamping::Absolute:
459 z = data.mZOffset;
460 break;
461 case Qgis::AltitudeClamping::Terrain:
462 z = terrainZ;
463 break;
464 case Qgis::AltitudeClamping::Relative:
465 z = terrainZ + data.mZOffset;
466 break;
467 }
468
469 PictureBillboardGroup billboardGroup;
470 billboardGroup.path = pictureItem->path();
471 billboardGroup.pictureFormat = pictureItem->format();
472 billboardGroup.scaleMode = pictureItem->billboard3DScaleMode();
473
474 PictureBillboard billboardItem;
475 billboardItem.position = ( QgsVector3D( mapPoint.x(), mapPoint.y(), z ) - chunkOrigin ).toVector3D();
476 billboardItem.size = pictureItem->billboard3DSize();
477 if ( billboardItem.size.isEmpty() )
478 {
479 constexpr QSizeF DEFAULT_FIXED_SIZE = QSizeF( 128, 128 );
480 constexpr QSizeF DEFAULT_WORLD_SIZE = QSizeF( 20, 20 );
481 billboardItem.size = billboardGroup.scaleMode == Qgis::BillboardScaleMode::ViewIndependent ? DEFAULT_FIXED_SIZE : DEFAULT_WORLD_SIZE;
482 }
483
484 groupedPictures[billboardGroup].append( billboardItem );
485
486 if ( data.mShowCallouts )
487 {
488 calloutLines << QgsLineString( { mapPoint.x(), mapPoint.x() }, { mapPoint.y(), mapPoint.y() }, { terrainZ, z } );
489 }
490
491 zMax = std::max( zMax, data.mShowCallouts ? std::max( 0.0, z ) : z );
492 zMin = std::min( zMin, data.mShowCallouts ? std::min( 0.0, z ) : z );
493 }
494 }
495
496 QImage billboardAtlas;
497 QVector< QgsBillboardGeometry::BillboardAtlasData > billboardPositions;
498 if ( !textures.isEmpty() )
499 {
500 const QgsTextureAtlas atlas = QgsTextureAtlasGenerator::createFromImages( textures, 2048 );
501 if ( atlas.isValid() )
502 {
503 billboardAtlas = atlas.renderAtlasTexture();
504 billboardPositions.reserve( static_cast< int >( billboards.size() ) );
505 for ( Billboard &billboard : billboards )
506 {
507 const QRect textureRect = atlas.rect( billboard.textureId );
509 geometry.position = billboard.position;
510 geometry.textureAtlasOffset = QVector2D(
511 static_cast< float >( textureRect.left() ) / static_cast< float>( billboardAtlas.width() ), 1 - ( static_cast< float >( textureRect.bottom() ) / static_cast< float>( billboardAtlas.height() ) )
512 );
513 geometry.textureAtlasSize
514 = QVector2D( static_cast< float >( textureRect.width() ) / static_cast< float>( billboardAtlas.width() ), static_cast< float>( textureRect.height() ) / static_cast< float>( billboardAtlas.height() ) );
515 geometry.pixelOffset = QPoint( 0, textureRect.height() / 2 );
516 billboardPositions.append( geometry );
517 }
518 }
519 else
520 {
521 QgsDebugError( u"Error encountered building texture atlas"_s );
522 billboardAtlas = QImage();
523 }
524 }
525 else
526 {
527 billboardAtlas = QImage();
528 billboardPositions.clear();
529 }
530
531 QImage textBillboardAtlas;
532 QVector< QgsBillboardGeometry::BillboardAtlasData > textBillboardPositions;
533 if ( !textBillboardTexts.isEmpty() )
534 {
535 const QgsFontTextureAtlas atlas = QgsFontTextureAtlasGenerator::create( data.mTextFormat, textBillboardTexts );
536 if ( atlas.isValid() )
537 {
538 textBillboardAtlas = atlas.renderAtlasTexture();
539 textBillboardPositions.reserve( static_cast< int >( textBillboards.size() ) );
540 for ( TextBillboard &billboard : textBillboards )
541 {
542 int graphemeIndex = 0;
543 const int graphemeCount = atlas.graphemeCount( billboard.text );
544 // horizontally center text over point
545 const double xOffset = atlas.totalWidth( billboard.text ) / 2.0;
546 for ( ; graphemeIndex < graphemeCount; ++graphemeIndex )
547 {
548 const QRect textureRect = atlas.textureRectForGrapheme( billboard.text, graphemeIndex );
550 geometry.position = billboard.position;
551 geometry.textureAtlasOffset = QVector2D(
552 static_cast< float >( textureRect.left() ) / static_cast< float>( textBillboardAtlas.width() ),
553 1 - ( static_cast< float >( textureRect.bottom() ) / static_cast< float>( textBillboardAtlas.height() ) )
554 );
555 geometry.textureAtlasSize = QVector2D(
556 static_cast< float >( textureRect.width() ) / static_cast< float>( textBillboardAtlas.width() ),
557 static_cast< float>( textureRect.height() ) / static_cast< float>( textBillboardAtlas.height() )
558 );
559 const QPointF pixelOffset = atlas.pixelOffsetForGrapheme( billboard.text, graphemeIndex );
560 geometry.pixelOffset
561 = QPoint( static_cast< int >( std::round( -xOffset + pixelOffset.x() + 0.5 * textureRect.width() ) ), static_cast< int >( std::round( pixelOffset.y() + 0.5 * textureRect.height() ) ) );
562 textBillboardPositions.append( geometry );
563 }
564 }
565 }
566 else
567 {
568 QgsDebugError( u"Error encountered building font texture atlas"_s );
569 textBillboardAtlas = QImage();
570 }
571 }
572 else
573 {
574 textBillboardAtlas = QImage();
575 textBillboardPositions.clear();
576 }
577
578 // picture item billboards, grouped by picture source
579 for ( auto it = groupedPictures.constBegin(); it != groupedPictures.constEnd(); ++it )
580 {
581 QSizeF maxGroupSize( 0, 0 );
582 for ( auto picIt = it.value().constBegin(); picIt != it.value().constEnd(); ++picIt )
583 {
584 if ( picIt->size.width() > maxGroupSize.width() )
585 {
586 maxGroupSize.setWidth( picIt->size.width() );
587 }
588 if ( picIt->size.height() > maxGroupSize.height() )
589 {
590 maxGroupSize.setHeight( picIt->size.height() );
591 }
592 }
593
594 QImage image;
595 bool fitsInCache = false;
596
597 // can't zoom into these billboards, so we can use a fairly conservative texture size
598 constexpr int MAXIMUM_PICTURE_TEXTURE_SIZE_FIXED_SIZE = 256;
599 // can zoom into these, so we need a larger texture
600 constexpr int MAXIMUM_PICTURE_TEXTURE_SIZE_PERSPECTIVE = 1024;
601 const int textureSize = it.key().scaleMode == Qgis::BillboardScaleMode::Perspective ? MAXIMUM_PICTURE_TEXTURE_SIZE_PERSPECTIVE : MAXIMUM_PICTURE_TEXTURE_SIZE_FIXED_SIZE;
602 switch ( it.key().pictureFormat )
603 {
605 {
606 const QSize originalSize = QgsApplication::imageCache()->originalSize( it.key().path, true );
607 QSize imageSize = originalSize;
608 if ( imageSize.isEmpty() )
609 {
610 imageSize = maxGroupSize.toSize();
611 }
612 if ( imageSize.width() >= imageSize.height() && imageSize.width() > textureSize )
613 {
614 imageSize = QSize( textureSize, static_cast< int >( std::round( imageSize.height() * textureSize / imageSize.width() ) ) );
615 }
616 else if ( imageSize.height() > textureSize )
617 {
618 imageSize = QSize( static_cast< int >( std::round( imageSize.width() * textureSize / imageSize.height() ) ), textureSize );
619 }
620 image = QgsApplication::imageCache()->pathAsImage( it.key().path, imageSize, false, 1.0, fitsInCache, true );
621 break;
622 }
623
625 {
626 const QPicture picture = QgsApplication::svgCache()->svgAsPicture( it.key().path, textureSize, QColor(), QColor(), 1.0, 1.0, false, 0, true );
627 if ( !picture.isNull() && picture.boundingRect().width() > 0 && picture.boundingRect().height() > 0 )
628 {
629 const QRectF picRect = picture.boundingRect();
630 QSize imageSize = picRect.size().toSize();
631 if ( imageSize.width() >= imageSize.height() )
632 {
633 imageSize = QSize( textureSize, static_cast< int >( std::round( picRect.height() * static_cast< double >( textureSize ) / picRect.width() ) ) );
634 }
635 else
636 {
637 imageSize = QSize( static_cast< int >( std::round( picRect.width() * static_cast< double >( textureSize ) / picRect.height() ) ), textureSize );
638 }
639
640 image = QImage( imageSize, QImage::Format_ARGB32_Premultiplied );
641 image.fill( Qt::transparent );
642
643 const double scale = static_cast< double >( imageSize.width() ) / picRect.width();
644
645 QPainter painter( &image );
646 painter.setRenderHint( QPainter::Antialiasing );
647 painter.scale( scale, scale );
648
649 QgsPainting::drawPicture( &painter, QPointF( picRect.width() / 2.0, picRect.height() / 2.0 ), picture );
650 painter.end();
651 }
652 break;
653 }
654
656 continue;
657 }
658
659 PictureBillboards billboard;
660 billboard.scaleMode = it.key().scaleMode;
661 billboard.image = image;
662 billboard.positions.reserve( it.value().size() );
663 billboard.sizes.reserve( it.value().size() );
664 for ( const PictureBillboard &item : it.value() )
665 {
666 billboard.positions.append( item.position );
667 billboard.sizes.append( item.size );
668 }
669
670 pictureBillboards.append( billboard );
671 }
672
673 promise.addResult(
674 ChunkData {
675 .node = node,
676 .layerName = layerName,
677 .chunkOrigin = chunkOrigin,
678 .renderContext = renderCtx,
679 .billboardPositions = std::move( billboardPositions ),
680 .textBillboardPositions = std::move( textBillboardPositions ),
681 .billboardAtlas = std::move( billboardAtlas ),
682 .textBillboardAtlas = std::move( textBillboardAtlas ),
683 .pictureBillboards = std::move( pictureBillboards ),
684 .calloutLines = std::move( calloutLines ),
685 .zMin = zMin,
686 .zMax = zMax,
687 }
688 );
689}
690
691Qt3DCore::QEntity *QgsAnnotationLayerChunkLoader::createEntity( const QgsAnnotationLayerChunkLoader::ChunkData &chunkData, Qt3DCore::QEntity *parent )
692{
694 if ( chunkData.billboardPositions.empty() && chunkData.textBillboardPositions.empty() && chunkData.pictureBillboards.empty() )
695 {
696 // an empty node, so we return no entity. This tags the node as having no data and effectively removes it.
697 // we just make sure first that its initial estimated vertical range does not affect its parents' bboxes calculation
698 chunkData.node->setExactBox3D( QgsBox3D() );
699 chunkData.node->updateParentBoundingBoxesRecursively();
700 return nullptr;
701 }
702
703 Qt3DCore::QEntity *entity = new Qt3DCore::QEntity( parent );
704 entity->setObjectName( chunkData.layerName + "_" + chunkData.node->tileId().text() );
705
706 QgsGeoTransform *billboardTransform = new QgsGeoTransform;
707 billboardTransform->setGeoTranslation( chunkData.chunkOrigin );
708 entity->addComponent( billboardTransform );
709
710 if ( !chunkData.billboardPositions.empty() )
711 {
712 QgsBillboardGeometry *billboardGeometry = new QgsBillboardGeometry();
713 billboardGeometry->setBillboardData( chunkData.billboardPositions, true );
714
715 Qt3DRender::QGeometryRenderer *billboardGeometryRenderer = new Qt3DRender::QGeometryRenderer;
716 billboardGeometryRenderer->setPrimitiveType( Qt3DRender::QGeometryRenderer::TriangleStrip );
717 billboardGeometryRenderer->setGeometry( billboardGeometry );
718 billboardGeometryRenderer->setVertexCount( 4 );
719 billboardGeometryRenderer->setInstanceCount( billboardGeometry->count() );
720
722 billboardMaterial->setTexture2DFromImage( chunkData.billboardAtlas );
723
724
725 Qt3DCore::QEntity *billboardEntity = new Qt3DCore::QEntity;
726 billboardEntity->addComponent( billboardMaterial );
727 billboardEntity->addComponent( billboardGeometryRenderer );
728 billboardEntity->setParent( entity );
729 }
730
731 if ( !chunkData.textBillboardPositions.empty() )
732 {
733 QgsBillboardGeometry *textBillboardGeometry = new QgsBillboardGeometry();
734 textBillboardGeometry->setBillboardData( chunkData.textBillboardPositions, true );
735
736 Qt3DRender::QGeometryRenderer *billboardGeometryRenderer = new Qt3DRender::QGeometryRenderer;
737 billboardGeometryRenderer->setPrimitiveType( Qt3DRender::QGeometryRenderer::TriangleStrip );
738 billboardGeometryRenderer->setGeometry( textBillboardGeometry );
739 billboardGeometryRenderer->setVertexCount( 4 );
740 billboardGeometryRenderer->setInstanceCount( textBillboardGeometry->count() );
741
743 billboardMaterial->setTexture2DFromImage( chunkData.textBillboardAtlas );
744
745 Qt3DCore::QEntity *billboardEntity = new Qt3DCore::QEntity;
746 billboardEntity->addComponent( billboardMaterial );
747 billboardEntity->addComponent( billboardGeometryRenderer );
748 billboardEntity->setParent( entity );
749 }
750
751 for ( const PictureBillboards &pictureBillboard : chunkData.pictureBillboards )
752 {
753 QgsBillboardGeometry *pictureGeometry = new QgsBillboardGeometry();
754 pictureGeometry->setPositionsAndSizes( pictureBillboard.positions, pictureBillboard.sizes );
755 Qt3DRender::QGeometryRenderer *pictureGeometryRenderer = new Qt3DRender::QGeometryRenderer;
756 pictureGeometryRenderer->setPrimitiveType( Qt3DRender::QGeometryRenderer::TriangleStrip );
757 pictureGeometryRenderer->setGeometry( pictureGeometry );
758 pictureGeometryRenderer->setVertexCount( 4 );
759 pictureGeometryRenderer->setInstanceCount( static_cast< int >( pictureBillboard.positions.size() ) );
760 QgsPoint3DBillboardMaterial *pictureMaterial
762 pictureMaterial->setTexture2DFromImage( pictureBillboard.image );
763 // picture billboards should be vertically anchored to the bottom of the picture
764 pictureMaterial->setVerticalOffset( 0.5 );
765 Qt3DCore::QEntity *pictureEntity = new Qt3DCore::QEntity;
766 pictureEntity->addComponent( pictureMaterial );
767 pictureEntity->addComponent( pictureGeometryRenderer );
768 pictureEntity->setParent( entity );
769 }
770
771 if ( mData.mShowCallouts )
772 {
773 QgsLineVertexData lineData;
774 lineData.geocentricCoordinates = false; // mMapSettings->sceneMode() == Qgis::SceneMode::Globe;
775 lineData.init( Qgis::AltitudeClamping::Absolute, Qgis::AltitudeBinding::Vertex, 0, chunkData.renderContext, chunkData.chunkOrigin );
776
777 for ( const QgsLineString &line : chunkData.calloutLines )
778 {
779 lineData.addLineString( line, 0, false );
780 }
781
782 if ( !lineData.pointsA.isEmpty() )
783 {
784 QgsLineMaterial *mat = new QgsLineMaterial;
785 mat->setLineColor( mData.mCalloutLineColor );
786 mat->setLineWidth( mData.mCalloutLineWidth );
787
788 Qt3DCore::QEntity *calloutEntity = lineData.createSegmentEntity( mat );
789 calloutEntity->setObjectName( parent->objectName() + "_CALLOUTS" );
790 calloutEntity->setParent( entity );
791
792 if ( Qt3DCore::QEntity *calloutJoinEntity = lineData.createJoinEntity( mat ) )
793 {
794 calloutJoinEntity->setObjectName( parent->objectName() + "_CALLOUTS_JOINS" );
795 calloutJoinEntity->setParent( entity );
796 }
797 }
798 }
799
800 // fix the vertical range of the node from the estimated vertical range to the true range
801 if ( chunkData.zMin != std::numeric_limits<double>::max() && chunkData.zMax != std::numeric_limits<double>::lowest() )
802 {
803 QgsBox3D box = chunkData.node->box3D();
804 box.setZMinimum( chunkData.zMin );
805 box.setZMaximum( chunkData.zMax );
806 chunkData.node->setExactBox3D( box );
807 chunkData.node->updateParentBoundingBoxesRecursively();
808 }
809 return entity;
810}
811
812
814
815QgsAnnotationLayerChunkedEntity::QgsAnnotationLayerChunkedEntity(
816 Qgs3DMapSettings *map,
817 QgsAnnotationLayer *layer,
818 Qgis::AltitudeClamping clamping,
819 double zOffset,
820 bool showCallouts,
821 const QColor &calloutLineColor,
822 double calloutLineWidth,
823 const QgsTextFormat &textFormat,
824 double zMin,
825 double zMax
826)
827 : QgsAbstractFeatureBasedChunkedEntity(
828 map,
829 -1, // max. allowed screen error (negative tau means that we need to go until leaves are reached)
830 new QgsAnnotationLayerChunkLoader( Qgs3DRenderContext::fromMapSettings( map ), layer, 3, clamping, zOffset, showCallouts, calloutLineColor, calloutLineWidth, textFormat, zMin, zMax ),
831 true
832 )
833{
834 onTerrainElevationOffsetChanged();
835}
836
837QgsAnnotationLayerChunkedEntity::~QgsAnnotationLayerChunkedEntity()
838{
839 // cancel / wait for jobs
840 cancelActiveJobs();
841}
842
843// if the AltitudeClamping is `Absolute`, do not apply the offset
844bool QgsAnnotationLayerChunkedEntity::applyTerrainOffset() const
845{
846 if ( auto loader = static_cast<QgsAnnotationLayerChunkLoader *>( mChunkLoader ) )
847 {
848 return loader->mData.mClamping != Qgis::AltitudeClamping::Absolute;
849 }
850 return true;
851}
852
853QList<QgsRayCastHit> QgsAnnotationLayerChunkedEntity::rayIntersection( const QgsRay3D &ray, const QgsRayCastContext &context ) const
854{
855 Q_UNUSED( ray )
856 Q_UNUSED( context )
857 return {};
858}
859
AltitudeClamping
Altitude clamping.
Definition qgis.h:4245
@ Absolute
Elevation is taken directly from feature and is independent of terrain height (final elevation = feat...
Definition qgis.h:4246
@ Geocentric
Geocentric CRS.
Definition qgis.h:2516
PictureFormat
Picture formats.
Definition qgis.h:5852
@ Raster
Raster image.
Definition qgis.h:5854
@ Unknown
Invalid or unknown image type.
Definition qgis.h:5855
@ SVG
SVG image.
Definition qgis.h:5853
@ Vertex
Clamp every vertex of feature.
Definition qgis.h:4259
BillboardScaleMode
3D billboard scaling modes.
Definition qgis.h:4497
@ Perspective
Billboard size is scaled with perspective distance from camera, using world units.
Definition qgis.h:4499
@ SpatialBounds
Item is rendered inside fixed spatial bounds, and size will depend on map scale.
Definition qgis.h:2688
@ FixedSize
Item is rendered at a fixed size, regardless of map scale. Item's location is georeferenced to a spat...
Definition qgis.h:2689
@ RelativeToMapFrame
Items size and placement is relative to the map's frame, and the item will always be rendered in the ...
Definition qgis.h:2690
@ Reverse
Reverse/inverse transform (from destination to source).
Definition qgis.h:2884
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.
QgsPointXY transform(const QgsPointXY &point, Qgis::TransformDirection direction=Qgis::TransformDirection::Forward) const
Transform the point from the source CRS to the destination CRS.
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:192
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.
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:129
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:8299
#define QgsDebugError(str)
Definition qgslogger.h:71
#define QGIS_CHECK_MAIN_THREAD_ACCESS
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.