QGIS API Documentation 3.99.0-Master (d270888f95f)
Loading...
Searching...
No Matches
qgsmaptooleditblanksegments.cpp
Go to the documentation of this file.
1/***************************************************************************
2 qgsmaptooleditblanksegments.cpp
3 ---------------------
4 begin : 2025/08/19
5 copyright : (C) 2025 by Julien Cabieces
6 email : julien dot cabieces at oslandia 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 <line_p.h>
19
21#include "qgsfeatureid.h"
22#include "qgsguiutils.h"
23#include "qgslinesymbollayer.h"
24#include "qgsmapcanvas.h"
25#include "qgsmapmouseevent.h"
27#include "qgsrubberband.h"
31#include "qgssnappingutils.h"
33#include "qgssymbol.h"
34#include "qgsvectorlayer.h"
35
36#include "moc_qgsmaptooleditblanksegments.cpp"
37
38constexpr int TOLERANCE = 20;
39
40
42
44
45namespace
46{
47
51 class QgsSymbolLayerFinder : public QgsStyleEntityVisitorInterface
52 {
53 public:
58 QgsSymbolLayerFinder( const QString &symbolLayerId )
59 : mSymbolLayerId( symbolLayerId ) {}
60
61 bool visitEnter( const QgsStyleEntityVisitorInterface::Node &node ) override
62 {
64 return false;
65
66 return true;
67 }
68
72 bool visitSymbol( const QgsSymbol *symbol )
73 {
74 if ( symbol && !mSymbol )
75 mSymbol = symbol;
76
77 for ( int idx = 0; idx < symbol->symbolLayerCount(); idx++ )
78 {
79 const QgsSymbolLayer *sl = symbol->symbolLayer( idx );
80 if ( sl->id() == mSymbolLayerId )
81 {
82 mSymbolLayer = dynamic_cast<const QgsTemplatedLineSymbolLayerBase *>( sl );
83 mSymbolLayerIndex = mSymbolLayer ? idx : -1;
84 return false;
85 }
86
87 // recurse over sub symbols
88 if ( const QgsSymbol *subSymbol = const_cast<QgsSymbolLayer *>( sl )->subSymbol();
89 subSymbol && !visitSymbol( subSymbol ) )
90 {
91 return false;
92 }
93 }
94
95 return true;
96 }
97
98 bool visit( const QgsStyleEntityVisitorInterface::StyleLeaf &leaf ) override
99 {
100 if ( leaf.entity && leaf.entity->type() == QgsStyle::SymbolEntity )
101 {
102 auto symbolEntity = static_cast<const QgsStyleSymbolEntity *>( leaf.entity );
103 if ( symbolEntity->symbol() )
104 visitSymbol( symbolEntity->symbol() );
105 }
106 return true;
107 }
108
112 const QgsSymbol *symbol() const { return mSymbol; }
113
117 const QgsTemplatedLineSymbolLayerBase *symbolLayer() const { return mSymbolLayer; }
118
122 int symbolLayerIndex() const { return mSymbolLayerIndex; }
123
124 private:
125 const QgsSymbol *mSymbol = nullptr;
126 const QgsTemplatedLineSymbolLayerBase *mSymbolLayer = nullptr;
127 int mSymbolLayerIndex = -1;
128 const QString &mSymbolLayerId;
129 };
130
135 const QPointF &pointAt( const QList<QList<QPolygonF>> &points, int partIndex, int ringIndex, int pointIndex )
136 {
137 if ( partIndex < 0 || partIndex >= points.count() )
138 throw std::invalid_argument( "Blank segments internal error : Invalid part index" );
139
140 const QList<QPolygonF> &rings = points.at( partIndex );
141 if ( ringIndex < 0 || ringIndex >= rings.count() )
142 throw std::invalid_argument( "blank segments internal error : Invalid ring index" );
143
144 const QPolygonF &pts = rings.at( ringIndex );
145 if ( pointIndex < 0 || pointIndex >= pts.count() )
146 throw std::invalid_argument( "blank segments internal error : Invalid point index" );
147
148 return pts.at( pointIndex );
149 }
150
151 enum ProjectedPointStatus
152 {
153 OK, // ok, point is on the segment
154 LINE_EMPTY, // line is empty, cannot project point
155 NOT_ON_SEGMENT // point is on the line, but not on the segment
156 };
157
162 QPointF projectedPoint( const QPointF &lineStartPt, const QPointF &lineEndPt, const QPointF &point, double &distance, ProjectedPointStatus &status )
163 {
164 status = ProjectedPointStatus::OK;
165 distance = -1;
166
167 const double Ax = lineStartPt.x();
168 const double Ay = lineStartPt.y();
169 const double Bx = lineEndPt.x();
170 const double By = lineEndPt.y();
171 const double Cx = point.x();
172 const double Cy = point.y();
173
174 const double length = QgsGeometryUtilsBase::distance2D( Ax, Ay, Bx, By );
175 if ( length == 0 )
176 {
177 status = ProjectedPointStatus::LINE_EMPTY;
178 return QPointF();
179 }
180
181 const double r = ( ( Cx - Ax ) * ( Bx - Ax ) + ( Cy - Ay ) * ( By - Ay ) ) / std::pow( length, 2 );
182 if ( r < 0 or r > 1 )
183 {
184 status = ProjectedPointStatus::NOT_ON_SEGMENT;
185 }
186
187 // projected point
188 const double Px = Ax + r * ( Bx - Ax );
189 const double Py = Ay + r * ( By - Ay );
190
191 distance = QgsGeometryUtilsBase::distance2D( Cx, Cy, Px, Py );
192
193 return QPointF( Px, Py );
194 }
195
196} //namespace
198
199
206{
207 public:
219 QgsBlankSegmentRubberBand( int partIndex, int ringIndex, int startIndex, int endIndex, QPointF startPt, QPointF endPt, QgsMapCanvas *canvas, const FeaturePoints &points );
220
226
236 void setPoints( int partIndex, int ringIndex, int startIndex, int endIndex, QPointF startPt, QPointF endPt );
237
241 void copyFrom( const QgsBlankSegmentRubberBand &blankSegment );
242
246 void setHighlighted( bool highlighted );
247
251 const QPointF &getStartPoint() const;
252
256 const QPointF &getEndPoint() const;
257
261 int getStartIndex() const;
262
266 int getEndIndex() const;
267
271 int getPartIndex() const;
272
276 int getRingIndex() const;
277
281 QPair<double, double> getStartEndDistance( Qgis::RenderUnit unit ) const;
282
286 int pointsCount() const;
287
291 const QPointF &pointAt( int index ) const;
292
293 private:
297 void updatePoints();
298
299 int mPartIndex = -1;
300 int mRingIndex = -1;
301 int mStartIndex = -1;
302 int mEndIndex = -1;
303 QPointF mStartPt;
304 QPointF mEndPt;
305 bool mNeedSwap = false;
306 const FeaturePoints &mPoints;
307};
308
310 : QgsMapTool( canvas )
311 , mLayer( layer )
312 , mSymbolLayerId( symbolLayer->id() )
313 , mBlankSegmentsFieldIndex( blankSegmentFieldIndex )
314 , mEditedBlankSegment( new QgsBlankSegmentRubberBand( canvas, mPoints ) )
315 , mStartRubberBand( new QgsRubberBand( canvas, Qgis::GeometryType::Point ) )
316 , mEndRubberBand( new QgsRubberBand( canvas, Qgis::GeometryType::Point ) )
317{
318 auto initRubberBand = []( QgsRubberBand *rb ) {
319 rb->setWidth( QgsGuiUtils::scaleIconSize( 4 ) );
322 };
323
324 initRubberBand( mStartRubberBand );
325 initRubberBand( mEndRubberBand );
326
327 mEditedBlankSegment->setHighlighted( true );
328}
329
331
333{
334 if ( !mLayer || !mLayer->renderer() )
335 return;
336
337 if ( !mSymbol || !mSymbolLayer )
338 {
339 // search and symbol and symbol layer
340 QgsSymbolLayerFinder finder( mSymbolLayerId );
341 mLayer->renderer()->accept( &finder );
342 if ( finder.symbol() && finder.symbolLayer() && finder.symbolLayerIndex() > -1 )
343 {
344 mSymbol.reset( finder.symbol()->clone() );
345 if ( mSymbolLayer = createRenderedPointsSymbolLayer( finder.symbolLayer() ); mSymbolLayer )
346 {
347 // set our on symbol layer to later retrieve rendered points
348 mSymbol->changeSymbolLayer( finder.symbolLayerIndex(), mSymbolLayer );
349 }
350 else
351 {
352 mSymbol.reset();
353 QgsDebugError( "Fail to create fake templated line symbol layer" );
354 }
355 }
356 else
357 {
358 QgsDebugError( "Fail to retrieve symbol and templated line symbol layer" );
359 }
360 }
361
363}
364
366{
367 if ( !mSymbol || mPoints.isEmpty() )
368 return;
369
370 const QPoint &pos = e->pos();
371
372 if ( canvas()->extent() != mExtent )
373 {
374 loadFeaturePoints();
375
376 // If edition has been started, we cancel them because they are no longer in the same extent reference
377 switch ( mState )
378 {
379 case State::SelectFeature:
380 case State::BlankSegmentSelected:
381 case State::FeatureSelected:
382 break;
383
384 case State::BlankSegmentCreationStarted:
385 mState = State::FeatureSelected;
386 setCurrentBlankSegment( -1 );
387 break;
388
389 case State::BlankSegmentModificationStarted:
390 mState = State::BlankSegmentSelected;
391 // force original blank segment
392 setCurrentBlankSegment( mCurrentBlankSegmentIndex );
393 break;
394 }
395 }
396
397 switch ( mState )
398 {
399 case State::SelectFeature:
400 return;
401
402 case State::BlankSegmentSelected:
403 updateHoveredBlankSegment( pos );
404 break;
405
406 case State::FeatureSelected:
407 {
408 updateHoveredBlankSegment( pos );
409
410 // display current start point to create a new blank segment
411 mStartRubberBand->setVisible( mHoveredBlankSegmentIndex == -1 );
412 if ( mHoveredBlankSegmentIndex == -1 )
413 {
414 const QgsMapToPixel &m2p = *( canvas()->getCoordinateTransform() );
415 mStartRubberBand->reset( Qgis::GeometryType::Point );
416 double distance;
417 int partIndex = -1, ringIndex = -1, pointIndex = -1;
418 const QPointF closestPt = closestPoint( pos, distance, partIndex, ringIndex, pointIndex );
419
420 // for now end point is the same as start one
421 mStartRubberBand->addPoint( m2p.toMapCoordinates( closestPt.x(), closestPt.y() ) );
422 }
423
424 break;
425 }
426
427 case State::BlankSegmentModificationStarted:
428 case State::BlankSegmentCreationStarted:
429 {
430 double distance = -1;
431 int partIndex = -1;
432 int pointIndex = -1;
433 int ringIndex = -1;
434 QPointF P = closestPoint( pos, distance, partIndex, ringIndex, pointIndex );
435 if ( distance > -1 && pointIndex > -1 )
436 {
437 mEditedBlankSegment->setPoints( partIndex, ringIndex, mEditedBlankSegment->getStartIndex(), pointIndex, mEditedBlankSegment->getStartPoint(), P );
438 updateStartEndRubberBand();
439 }
440 }
441 break;
442 }
443}
444
446{
447 switch ( mState )
448 {
449 case State::SelectFeature:
450 {
451 //find the closest feature to the pressed position
452 const QgsPointLocator::Match m = mCanvas->snappingUtils()->snapToCurrentLayer( e->pos(), QgsPointLocator::Area );
453 if ( !m.isValid() )
454 {
455 emit messageEmitted( tr( "No feature was detected at the clicked position. Please click closer to the feature or enhance the search tolerance under Settings->Options->Digitizing->Search radius for vertex edits" ), Qgis::MessageLevel::Critical );
456 return;
457 }
458
459 mCurrentFeatureId = m.featureId();
460 loadFeaturePoints();
461 updateHoveredBlankSegment( e->pos() );
462
463 mState = State::FeatureSelected;
464 break;
465 }
466 case State::FeatureSelected:
467
468 // new blank segment selected
469 if ( mHoveredBlankSegmentIndex > -1 )
470 {
471 mState = State::BlankSegmentSelected;
472 setCurrentBlankSegment( mHoveredBlankSegmentIndex );
473 }
474 // init first point of new blank segment
475 else
476 {
477 mState = State::BlankSegmentCreationStarted;
478 double distance = -1;
479 int partIndex = -1;
480 int pointIndex = -1;
481 int ringIndex = -1;
482 QPointF P = closestPoint( e->pos(), distance, partIndex, ringIndex, pointIndex );
483 if ( distance > -1 && pointIndex > -1 )
484 {
485 mEditedBlankSegment->setPoints( partIndex, ringIndex, pointIndex, pointIndex, P, P );
486 updateStartEndRubberBand();
487 }
488 }
489
490 break;
491
492 case State::BlankSegmentSelected:
493 {
494 Q_ASSERT( mCurrentBlankSegmentIndex > -1 && mCurrentBlankSegmentIndex < static_cast<int>( mBlankSegments.size() ) );
495 const QObjectUniquePtr<QgsBlankSegmentRubberBand> &currentBlankSegment = mBlankSegments.at( mCurrentBlankSegmentIndex );
496
497 // selected blank segment has changed
498 if ( mHoveredBlankSegmentIndex > -1 && mHoveredBlankSegmentIndex != mCurrentBlankSegmentIndex )
499 {
500 setCurrentBlankSegment( mHoveredBlankSegmentIndex );
501 }
502 else
503 {
504 const double distanceFromStart = ( currentBlankSegment->getStartPoint() - e->pos() ).manhattanLength();
505 const double distanceFromEnd = ( currentBlankSegment->getEndPoint() - e->pos() ).manhattanLength();
506
507 // user clicked on start or end point to move it
508 if ( std::min( distanceFromStart, distanceFromEnd ) < TOLERANCE )
509 {
510 if ( distanceFromStart < distanceFromEnd )
511 {
512 // start point become end point because we always edit end point
513 mEditedBlankSegment->setPoints( currentBlankSegment->getPartIndex(), currentBlankSegment->getRingIndex(), currentBlankSegment->getEndIndex(), currentBlankSegment->getStartIndex(), currentBlankSegment->getEndPoint(), currentBlankSegment->getStartPoint() );
514 }
515
516 mState = State::BlankSegmentModificationStarted;
517 }
518 }
519 }
520
521 break;
522
523 case State::BlankSegmentModificationStarted:
524 case State::BlankSegmentCreationStarted:
525
526 // this is a new one
527 if ( mCurrentBlankSegmentIndex < 0 )
528 {
529 mBlankSegments.emplace_back( new QgsBlankSegmentRubberBand( canvas(), mPoints ) );
530 mBlankSegments.back()->copyFrom( *mEditedBlankSegment );
531 mState = State::FeatureSelected;
532 }
533 // modify an existing one
534 else
535 {
536 QObjectUniquePtr<QgsBlankSegmentRubberBand> &blankSegment = mBlankSegments.at( mCurrentBlankSegmentIndex );
537 blankSegment->copyFrom( *mEditedBlankSegment );
538 mState = State::BlankSegmentSelected;
539 }
540
541 updateAttribute();
542 break;
543 }
544}
545
547{
548 // !!! We need to ignore event instead of accept them if we want to consume them
549 // see QgsMapCanvas::keyPressEvent
550
551 switch ( mState )
552 {
553 case State::SelectFeature:
554 return;
555
556 case State::BlankSegmentSelected:
557 if ( e->matches( QKeySequence::Delete ) && mCurrentBlankSegmentIndex > -1 )
558 {
559 mState = State::FeatureSelected;
560 int toRemoveIndex = mCurrentBlankSegmentIndex;
561 setCurrentBlankSegment( -1 );
562 mBlankSegments.erase( mBlankSegments.begin() + toRemoveIndex );
563 updateAttribute();
564 e->ignore();
565 }
566 else if ( e->matches( QKeySequence::Cancel ) )
567 {
568 mState = State::FeatureSelected;
569 setCurrentBlankSegment( -1 );
570 e->ignore();
571 }
572
573 break;
574
575 case State::FeatureSelected:
576 if ( e->matches( QKeySequence::Cancel ) )
577 {
578 mCurrentFeatureId = FID_NULL;
579 mState = State::SelectFeature;
580 loadFeaturePoints();
581 e->ignore();
582 }
583 break;
584
585 case State::BlankSegmentCreationStarted:
586 if ( e->matches( QKeySequence::Cancel ) )
587 {
588 mState = State::FeatureSelected;
589 setCurrentBlankSegment( -1 );
590 e->ignore();
591 }
592 break;
593
594 case State::BlankSegmentModificationStarted:
595 if ( e->matches( QKeySequence::Cancel ) )
596 {
597 mState = State::BlankSegmentSelected;
598 // force original blank segment
599 setCurrentBlankSegment( mCurrentBlankSegmentIndex );
600 e->ignore();
601 }
602 break;
603 }
604}
605
607{
608 double startDistance = 0;
609 const int startIndex = mNeedSwap ? mEndIndex : mStartIndex;
610 const QPointF startPt = mNeedSwap ? mEndPt : mStartPt;
611 for ( int i = 1; i < startIndex; i++ )
612 {
613 startDistance += QgsGeometryUtilsBase::distance2D( ::pointAt( mPoints, mPartIndex, mRingIndex, i - 1 ), ::pointAt( mPoints, mPartIndex, mRingIndex, i ) );
614 }
615
616 startDistance += QgsGeometryUtilsBase::distance2D( ::pointAt( mPoints, mPartIndex, mRingIndex, startIndex - 1 ), startPt );
617
618 double endDistance = startDistance;
619 for ( int i = 1; i < pointsCount(); i++ )
620 {
621 endDistance += QgsGeometryUtilsBase::distance2D( pointAt( i ), pointAt( i - 1 ) );
622 }
623
624 QgsRenderContext renderContext = QgsRenderContext::fromMapSettings( mMapCanvas->mapSettings() );
625
626 startDistance = renderContext.convertFromPainterUnits( startDistance, unit );
627 endDistance = renderContext.convertFromPainterUnits( endDistance, unit );
628
629 return QPair<double, double>( startDistance, endDistance );
630}
631
632int QgsMapToolEditBlankSegmentsBase::closestBlankSegmentIndex( const QPointF &point, double &distance ) const
633{
634 // search for closest blankSegment
635 distance = -1;
636 int iBlankSegment = -1;
637 for ( int i = 0; i < static_cast<int>( mBlankSegments.size() ); i++ )
638 {
639 const QObjectUniquePtr<QgsBlankSegmentRubberBand> &blankSegment = mBlankSegments.at( i );
640 for ( int iPoint = 1; iPoint < blankSegment->pointsCount(); iPoint++ )
641 {
642 double d = 0;
643 ProjectedPointStatus status = ProjectedPointStatus::OK;
644
645 try
646 {
647 projectedPoint( blankSegment->pointAt( iPoint - 1 ), blankSegment->pointAt( iPoint ), point, d, status );
648 }
649 catch ( std::invalid_argument &e )
650 {
651 QgsDebugError( e.what() );
652 continue;
653 }
654
655 switch ( status )
656 {
657 case ProjectedPointStatus::LINE_EMPTY:
658 continue;
659
660 case ProjectedPointStatus::NOT_ON_SEGMENT:
661 d = std::min( ( blankSegment->getStartPoint() - point ).manhattanLength(), ( blankSegment->getEndPoint() - point ).manhattanLength() );
662 break;
663
664 case ProjectedPointStatus::OK:
665 break;
666 }
667
668 if ( distance == -1 || d < distance )
669 {
670 distance = d;
671 iBlankSegment = i;
672 }
673 }
674 }
675
676 return iBlankSegment;
677}
678
679QPointF QgsMapToolEditBlankSegmentsBase::closestPoint( const QPointF &point, double &distance, int &partIndex, int &ringIndex, int &pointIndex ) const
680{
681 distance = -1;
682 QPointF currentPoint;
683
684 // iterate through all points from parts and ring to get the closest one
685 for ( int iPart = 0; iPart < mPoints.count(); iPart++ )
686 {
687 const QList<QPolygonF> &rings = mPoints.at( iPart );
688 for ( int iRing = 0; iRing < rings.count(); iRing++ )
689 {
690 const QPolygonF &points = rings.at( iRing );
691 for ( int i = 1; i < points.count(); i++ )
692 {
693 double d = 0;
694 ProjectedPointStatus status = ProjectedPointStatus::OK;
695 QPointF P = projectedPoint( points.at( i - 1 ), points.at( i ), point, d, status );
696 switch ( status )
697 {
698 case ProjectedPointStatus::LINE_EMPTY:
699 case ProjectedPointStatus::NOT_ON_SEGMENT:
700 continue;
701
702 case ProjectedPointStatus::OK:
703 break;
704 }
705
706 if ( distance == -1 || d < distance )
707 {
708 distance = d;
709 currentPoint = P;
710 partIndex = iPart;
711 ringIndex = iRing;
712 pointIndex = i;
713 }
714 }
715 }
716 }
717 return currentPoint;
718}
719
720
721void QgsMapToolEditBlankSegmentsBase::updateStartEndRubberBand()
722{
723 mStartRubberBand->reset( Qgis::GeometryType::Point );
724 mEndRubberBand->reset( Qgis::GeometryType::Point );
725
726 bool displayEndPoint = true;
727 switch ( mState )
728 {
729 case State::SelectFeature:
730 case State::BlankSegmentCreationStarted:
731 return;
732
733 case State::FeatureSelected:
734 displayEndPoint = false;
735 [[fallthrough]];
736
737 case State::BlankSegmentSelected:
738 case State::BlankSegmentModificationStarted:
739 break;
740 }
741
742 const QgsMapToPixel &m2p = *( canvas()->getCoordinateTransform() );
743
744 const QPointF &startPoint = mEditedBlankSegment->getStartPoint();
745 mStartRubberBand->addPoint( m2p.toMapCoordinates( startPoint.x(), startPoint.y() ) );
746
747 if ( displayEndPoint )
748 {
749 const QPointF &endPoint = mEditedBlankSegment->getEndPoint();
750 mEndRubberBand->addPoint( m2p.toMapCoordinates( endPoint.x(), endPoint.y() ) );
751 }
752}
753
754void QgsMapToolEditBlankSegmentsBase::updateHoveredBlankSegment( const QPoint &pos )
755{
756 double distance = -1;
757 int iBlankSegment = closestBlankSegmentIndex( pos, distance );
758
759 if ( mHoveredBlankSegmentIndex > -1
760 && mHoveredBlankSegmentIndex < static_cast<int>( mBlankSegments.size() )
761 && mHoveredBlankSegmentIndex != mCurrentBlankSegmentIndex )
762 {
763 mBlankSegments.at( mHoveredBlankSegmentIndex )->setHighlighted( false );
764 }
765
766 // blank segment is hovered
767 if ( iBlankSegment > -1 && distance < TOLERANCE )
768 {
769 mHoveredBlankSegmentIndex = iBlankSegment;
770 if ( mHoveredBlankSegmentIndex < static_cast<int>( mBlankSegments.size() ) )
771 {
772 mBlankSegments.at( mHoveredBlankSegmentIndex )->setHighlighted( true );
773 }
774 }
775 // no blank segment hovered, display the first point to create a new blank segment
776 else
777 {
778 mHoveredBlankSegmentIndex = -1;
779 }
780}
781
782void QgsMapToolEditBlankSegmentsBase::setCurrentBlankSegment( int currentBlankSegmentIndex )
783{
784 // copy current blank segment so we can edit it later (and hide the original one)
785
786 if ( mCurrentBlankSegmentIndex > -1
787 && mCurrentBlankSegmentIndex < static_cast<int>( mBlankSegments.size() ) )
788 {
789 mBlankSegments.at( mCurrentBlankSegmentIndex )->setVisible( true );
790 mBlankSegments.at( mCurrentBlankSegmentIndex )->setHighlighted( false );
791 }
792
793 mCurrentBlankSegmentIndex = currentBlankSegmentIndex;
794 // NOLINTBEGIN(bugprone-branch-clone)
795 if ( mCurrentBlankSegmentIndex > -1 && mCurrentBlankSegmentIndex < static_cast<int>( mBlankSegments.size() ) )
796 {
797 mBlankSegments.at( mCurrentBlankSegmentIndex )->setVisible( false );
798 mEditedBlankSegment->copyFrom( *( mBlankSegments.at( mCurrentBlankSegmentIndex ).get() ) );
799 }
800 else
801 {
802 mEditedBlankSegment->setVisible( false );
803 }
804 // NOLINTEND(bugprone-branch-clone)
805
806 updateStartEndRubberBand();
807}
808
809void QgsMapToolEditBlankSegmentsBase::updateAttribute()
810{
811 if ( !mSymbolLayer )
812 return;
813
814 QList<QList<QgsBlankSegmentUtils::BlankSegments>> blankSegments;
815 for ( const QObjectUniquePtr<QgsBlankSegmentRubberBand> &blankSegment : mBlankSegments )
816 {
817 try
818 {
819 QPair<double, double> startEndDistance = blankSegment->getStartEndDistance( mSymbolLayer->blankSegmentsUnit() );
820
821 const int partIndex = blankSegment->getPartIndex();
822 if ( partIndex >= blankSegments.count() )
823 blankSegments.resize( partIndex + 1 );
824
825 QList<QgsBlankSegmentUtils::BlankSegments> &rings = blankSegments[partIndex];
826 const int ringIndex = blankSegment->getRingIndex();
827 if ( ringIndex >= rings.count() )
828 rings.resize( ringIndex + 1 );
829
830 QgsBlankSegmentUtils::BlankSegments &segments = rings[ringIndex];
831 segments << startEndDistance;
832 }
833 catch ( std::invalid_argument &e )
834 {
835 QgsDebugError( e.what() );
836 return;
837 }
838 }
839
840 QStringList strParts;
841 for ( QList<QgsBlankSegmentUtils::BlankSegments> &part : blankSegments )
842 {
843 QStringList strRings;
844 for ( QgsBlankSegmentUtils::BlankSegments &ring : part )
845 {
846 std::sort( ring.begin(), ring.end() );
847 QStringList strDistances;
848 for ( const QPair<double, double> &distance : ring )
849 {
850 strDistances << ( QString::number( distance.first ) + " " + QString::number( distance.second ) );
851 }
852
853 strRings << "(" + strDistances.join( "," ) + ")";
854 }
855
856 strParts << "(" + strRings.join( "," ) + ")";
857 }
858
859 QString strNewBlankSegments = "(" + strParts.join( "," ) + ")";
860
861 mLayer->beginEditCommand( tr( "Set blank segment list" ) );
862 if ( mLayer->changeAttributeValue( mCurrentFeatureId, mBlankSegmentsFieldIndex, strNewBlankSegments ) )
863 {
864 mLayer->endEditCommand();
865 mLayer->triggerRepaint();
866 }
867 else
868 {
869 mLayer->destroyEditCommand();
870 }
871}
872
873void QgsMapToolEditBlankSegmentsBase::loadFeaturePoints()
874{
875 mPoints.clear();
876 mExtent = canvas()->extent();
877 mBlankSegments.clear();
878 setCurrentBlankSegment( -1 );
879
880 if ( FID_IS_NULL( mCurrentFeatureId ) || !mSymbolLayer )
881 return;
882
883 QgsFeature feature;
884 feature = mLayer->getFeature( mCurrentFeatureId );
885 QString currentBlankSegments = feature.attribute( mBlankSegmentsFieldIndex ).toString();
886
887
888 // render feature to update mPoints
889 QgsRenderContext context = QgsRenderContext::fromMapSettings( canvas()->mapSettings() );
890 QgsCoordinateTransform transform( canvas()->mapSettings().layerTransform( mLayer ) );
891 QgsNullPaintDevice nullPaintDevice;
892 QPainter painter( &nullPaintDevice );
893 context.setPainter( &painter );
894 context.setCoordinateTransform( transform );
895 mSymbol->startRender( context );
896 mSymbol->renderFeature( feature, context );
897 mSymbol->stopRender( context );
898
899 if ( mPoints.isEmpty() )
900 return;
901
902 QString error;
903 QList<QList<QgsBlankSegmentUtils::BlankSegments>> allBlankSegments = QgsBlankSegmentUtils::parseBlankSegments( currentBlankSegments, context, mSymbolLayer->blankSegmentsUnit(), error );
904 if ( !error.isEmpty() )
905 {
906 emit messageEmitted( tr( "Error while parsing feature blank segments: %1" ).arg( error ), Qgis::MessageLevel::Critical );
907 return;
908 }
909
910 // iterate through all points from parts and ring to get the closest one
911 for ( int iPart = 0; iPart < mPoints.count(); iPart++ )
912 {
913 const QList<QPolygonF> &rings = mPoints.at( iPart );
914 for ( int iRing = 0; iRing < rings.count(); iRing++ )
915 {
916 if ( iPart >= allBlankSegments.count() || iRing >= allBlankSegments.at( iPart ).count() )
917 continue;
918
919 const QgsBlankSegmentUtils::BlankSegments &blankSegments = allBlankSegments.at( iPart ).at( iRing );
920
921 double currentLength = 0;
922 int iPoint = 0;
923 const QPolygonF &points = rings.at( iRing );
924 for ( QPair<double, double> ba : blankSegments )
925 {
926 while ( iPoint < points.count() - 1 && currentLength < ba.first )
927 {
928 iPoint++;
929 currentLength += QgsGeometryUtilsBase::distance2D( points.at( iPoint ), points.at( iPoint - 1 ) );
930 }
931
932 if ( iPoint == points.count() )
933 break;
934
935 int startIndex = iPoint;
936 Line l( points.at( iPoint ), points.at( iPoint - 1 ) );
937 QPointF startPt = points.at( iPoint ) + l.diffForInterval( currentLength - ba.first );
938
939 while ( iPoint < points.count() - 1 && currentLength < ba.second )
940 {
941 iPoint++;
942 currentLength += QgsGeometryUtilsBase::distance2D( points.at( iPoint ), points.at( iPoint - 1 ) );
943 }
944
945 if ( iPoint == points.count() )
946 break;
947
948 int endIndex = iPoint;
949 Line l2( points.at( iPoint ), points.at( iPoint - 1 ) );
950 QPointF endPt = points.at( iPoint ) + l2.diffForInterval( currentLength - ba.second );
951
952 mBlankSegments.emplace_back( new QgsBlankSegmentRubberBand( iPart, iRing, startIndex, endIndex, startPt, endPt, canvas(), mPoints ) );
953 }
954 }
955 }
956}
957
965
966QgsMapToolEditBlankSegmentsBase::QgsBlankSegmentRubberBand::QgsBlankSegmentRubberBand( int partIndex, int ringIndex, int startIndex, int endIndex, QPointF startPt, QPointF endPt, QgsMapCanvas *canvas, const FeaturePoints &points )
968{
969 setPoints( partIndex, ringIndex, startIndex, endIndex, startPt, endPt );
970}
971
972void QgsMapToolEditBlankSegmentsBase::QgsBlankSegmentRubberBand::setPoints( int partIndex, int ringIndex, int startIndex, int endIndex, QPointF startPt, QPointF endPt )
973{
974 mPartIndex = partIndex;
975 mRingIndex = ringIndex;
976 mStartIndex = startIndex;
977 mEndIndex = endIndex;
978 mStartPt = startPt;
979 mEndPt = endPt;
980
981 mNeedSwap = false;
982 if ( mStartIndex == mEndIndex )
983 {
984 try
985 {
986 if ( const QPointF &startIndexPoint = ::pointAt( mPoints, mPartIndex, mRingIndex, mStartIndex );
987 QgsGeometryUtilsBase::distance2D( startPt, startIndexPoint ) < QgsGeometryUtilsBase::distance2D( endPt, startIndexPoint ) )
988 {
989 mNeedSwap = true;
990 }
991 }
992 catch ( std::invalid_argument &e )
993 {
994 QgsDebugError( e.what() );
995 }
996 }
997 else if ( mStartIndex > mEndIndex )
998 {
999 mNeedSwap = true;
1000 }
1001
1002 updatePoints();
1003}
1004
1005void QgsMapToolEditBlankSegmentsBase::QgsBlankSegmentRubberBand::updatePoints()
1006{
1007 const QgsMapToPixel &m2p = *( mMapCanvas->getCoordinateTransform() );
1008
1009 reset();
1010 for ( int iPoint = 0; iPoint < pointsCount(); iPoint++ )
1011 {
1012 try
1013 {
1014 const QPointF &point = pointAt( iPoint );
1015 const QgsPointXY mapPoint = m2p.toMapCoordinates( point.x(), point.y() );
1016 addPoint( mapPoint, iPoint == pointsCount() - 1 ); // update only last one
1017 }
1018 catch ( std::invalid_argument &e )
1019 {
1020 QgsDebugError( e.what() );
1021 }
1022 }
1023}
1024
1025
1027{
1028 setPoints( blankSegment.mPartIndex, blankSegment.mRingIndex, blankSegment.getStartIndex(), blankSegment.getEndIndex(), blankSegment.getStartPoint(), blankSegment.getEndPoint() );
1029}
1030
1032{
1033 setWidth( QgsGuiUtils::scaleIconSize( highlighted ? 4 : 2 ) );
1034 update();
1035}
1036
1037
1039{
1040 return mStartPt;
1041}
1042
1044{
1045 return mEndPt;
1046}
1047
1052
1057
1062
1067
1069{
1070 return std::abs( mEndIndex - mStartIndex ) + 2;
1071}
1072
1074{
1075 return index == 0 ?
1076 // first point
1077 ( mNeedSwap ? mEndPt : mStartPt )
1078 // last point
1079 : ( index == pointsCount() - 1 ? ( mNeedSwap ? mStartPt : mEndPt )
1080 // point in between
1081 : ::pointAt( mPoints, mPartIndex, mRingIndex, ( mNeedSwap ? mEndIndex : mStartIndex ) + index - 1 ) );
1082}
Keeps a pointer to a QObject and deletes it whenever this object is deleted.
Provides global constants and enumerations for use throughout the application.
Definition qgis.h:59
@ Critical
Critical/error message.
Definition qgis.h:162
@ Point
Points.
Definition qgis.h:366
RenderUnit
Rendering size units.
Definition qgis.h:5255
QList< QPair< double, double > > BlankSegments
static QList< QList< BlankSegments > > parseBlankSegments(const QString &strBlankSegments, const QgsRenderContext &renderContext, Qgis::RenderUnit unit, QString &error)
Parse blank segments string representation strBlankSegments.
Q_INVOKABLE QVariant attribute(const QString &name) const
Lookup attribute value by attribute name.
static double distance2D(double x1, double y1, double x2, double y2)
Returns the 2D distance between (x1, y1) and (x2, y2).
Abstract base class for line symbol layers.
QgsMapCanvas * mMapCanvas
pointer to map canvas
const QgsMapToPixel * getCoordinateTransform()
Gets the current coordinate transform.
QgsRectangle extent() const
Returns the current zoom extent of the map canvas.
A mouse event which is the result of a user interaction with a QgsMapCanvas.
Perform transforms between map coordinates and device coordinates.
QgsPointXY toMapCoordinates(int x, int y) const
Transforms device coordinates to map (world) coordinates.
Rubber band used to draw blank segments on edition.
QgsBlankSegmentRubberBand(int partIndex, int ringIndex, int startIndex, int endIndex, QPointF startPt, QPointF endPt, QgsMapCanvas *canvas, const FeaturePoints &points)
Constructor.
void copyFrom(const QgsBlankSegmentRubberBand &blankSegment)
Copy blankSegment rubber band information into this one.
QPair< double, double > getStartEndDistance(Qgis::RenderUnit unit) const
Returns start and end distance from the first rendered point expressed in unit units.
const QPointF & pointAt(int index) const
Returns blank segments point at index.
void setHighlighted(bool highlighted)
Set highligh state according to highlighted.
void setPoints(int partIndex, int ringIndex, int startIndex, int endIndex, QPointF startPt, QPointF endPt)
Initialize blank segment rubber band points.
void keyPressEvent(QKeyEvent *e) override
Key event for overriding. Default implementation does nothing.
QgsMapToolEditBlankSegmentsBase(QgsMapCanvas *canvas, QgsVectorLayer *layer, QgsLineSymbolLayer *symbolLayer, int blankSegmentFieldIndex)
Constructor.
~QgsMapToolEditBlankSegmentsBase() override
Destructor.
void canvasPressEvent(QgsMapMouseEvent *e) override
Mouse press event for overriding. Default implementation does nothing.
QList< QList< QPolygonF > > FeaturePoints
void canvasMoveEvent(QgsMapMouseEvent *e) override
Mouse move event for overriding. Default implementation does nothing.
void activate() override
called when set as currently active map tool
QgsMapLayer * layer(const QString &id)
Returns the map layer with the matching ID, or nullptr if no layers could be found.
QgsMapCanvas * canvas() const
returns pointer to the tool's map canvas
QgsMapTool(QgsMapCanvas *canvas)
Constructor takes a map canvas as a parameter.
QPointer< QgsMapCanvas > mCanvas
The pointer to the map canvas.
Definition qgsmaptool.h:360
friend class QgsMapCanvas
Definition qgsmaptool.h:380
void messageEmitted(const QString &message, Qgis::MessageLevel level=Qgis::MessageLevel::Info)
Emitted when a message should be shown to the user in the application message bar.
virtual void activate()
called when set as currently active map tool
@ Area
Snapped to an area.
Represents a 2D point.
Definition qgspointxy.h:62
Contains information about the context of a rendering operation.
void setCoordinateTransform(const QgsCoordinateTransform &t)
Sets the current coordinate transform for the context.
double convertFromPainterUnits(double size, Qgis::RenderUnit unit) const
Converts a size from painter units (pixels) to the specified render unit.
void setPainter(QPainter *p)
Sets the destination QPainter for the render operation.
static QgsRenderContext fromMapSettings(const QgsMapSettings &mapSettings)
create initialized QgsRenderContext instance from given QgsMapSettings
Responsible for drawing transient features (e.g.
QgsRubberBand(QgsMapCanvas *mapCanvas, Qgis::GeometryType geometryType=Qgis::GeometryType::Line)
Creates a new RubberBand.
void setWidth(double width)
Sets the width of the line.
@ ICON_BOX
A box is used to highlight points (□).
void setColor(const QColor &color)
Sets the color for the rubberband.
static const QgsSettingsEntryColor * settingsDigitizingLineColor
Settings entry digitizing line color.
virtual QgsStyle::StyleEntity type() const =0
Returns the type of style entity.
An interface for classes which can visit style entity (e.g.
@ SymbolRule
Rule based symbology or label child rule.
virtual bool visitEnter(const QgsStyleEntityVisitorInterface::Node &node)
Called when the visitor starts visiting a node.
virtual bool visit(const QgsStyleEntityVisitorInterface::StyleLeaf &entity)
Called when the visitor will visit a style entity.
A symbol entity for QgsStyle databases.
Definition qgsstyle.h:1398
@ SymbolEntity
Symbols.
Definition qgsstyle.h:206
Abstract base class for symbol layers.
QString id() const
Returns symbol layer identifier This id is unique in the whole project.
Abstract base class for all rendered symbols.
Definition qgssymbol.h:231
QgsSymbolLayer * symbolLayer(int layer)
Returns the symbol layer at the specified index.
int symbolLayerCount() const
Returns the total number of symbol layers contained in the symbol.
Definition qgssymbol.h:353
Base class for templated line symbols, e.g.
Represents a vector layer which manages a vector based dataset.
int scaleIconSize(int standardSize)
Scales an icon size to compensate for display pixel density, making the icon size hi-dpi friendly,...
const char * finder(const char *name)
#define FID_NULL
#define FID_IS_NULL(fid)
#define QgsDebugError(str)
Definition qgslogger.h:59
constexpr int TOLERANCE
QgsFeatureId featureId() const
The id of the feature to which the snapped geometry belongs.
Contains information relating to a node (i.e.
QgsStyleEntityVisitorInterface::NodeType type
Node type.
Contains information relating to the style entity currently being visited.
const QgsStyleEntityInterface * entity
Reference to style entity being visited.