QGIS API Documentation 3.99.0-Master (c03dd32cbdd)
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 if ( mCurrentBlankSegmentIndex > -1 && mCurrentBlankSegmentIndex < static_cast<int>( mBlankSegments.size() ) )
795 {
796 mBlankSegments.at( mCurrentBlankSegmentIndex )->setVisible( false );
797 mEditedBlankSegment->copyFrom( *( mBlankSegments.at( mCurrentBlankSegmentIndex ).get() ) );
798 }
799 else
800 {
801 mEditedBlankSegment->setVisible( false );
802 }
803
804 updateStartEndRubberBand();
805}
806
807void QgsMapToolEditBlankSegmentsBase::updateAttribute()
808{
809 if ( !mSymbolLayer )
810 return;
811
812 QList<QList<QgsBlankSegmentUtils::BlankSegments>> blankSegments;
813 for ( const QObjectUniquePtr<QgsBlankSegmentRubberBand> &blankSegment : mBlankSegments )
814 {
815 try
816 {
817 QPair<double, double> startEndDistance = blankSegment->getStartEndDistance( mSymbolLayer->blankSegmentsUnit() );
818
819 const int partIndex = blankSegment->getPartIndex();
820 if ( partIndex >= blankSegments.count() )
821 blankSegments.resize( partIndex + 1 );
822
823 QList<QgsBlankSegmentUtils::BlankSegments> &rings = blankSegments[partIndex];
824 const int ringIndex = blankSegment->getRingIndex();
825 if ( ringIndex >= rings.count() )
826 rings.resize( ringIndex + 1 );
827
828 QgsBlankSegmentUtils::BlankSegments &segments = rings[ringIndex];
829 segments << startEndDistance;
830 }
831 catch ( std::invalid_argument &e )
832 {
833 QgsDebugError( e.what() );
834 return;
835 }
836 }
837
838 QStringList strParts;
839 for ( QList<QgsBlankSegmentUtils::BlankSegments> &part : blankSegments )
840 {
841 QStringList strRings;
842 for ( QgsBlankSegmentUtils::BlankSegments &ring : part )
843 {
844 std::sort( ring.begin(), ring.end() );
845 QStringList strDistances;
846 for ( const QPair<double, double> &distance : ring )
847 {
848 strDistances << ( QString::number( distance.first ) + " " + QString::number( distance.second ) );
849 }
850
851 strRings << "(" + strDistances.join( "," ) + ")";
852 }
853
854 strParts << "(" + strRings.join( "," ) + ")";
855 }
856
857 QString strNewBlankSegments = "(" + strParts.join( "," ) + ")";
858
859 mLayer->beginEditCommand( tr( "Set blank segment list" ) );
860 if ( mLayer->changeAttributeValue( mCurrentFeatureId, mBlankSegmentsFieldIndex, strNewBlankSegments ) )
861 {
862 mLayer->endEditCommand();
863 mLayer->triggerRepaint();
864 }
865 else
866 {
867 mLayer->destroyEditCommand();
868 }
869}
870
871void QgsMapToolEditBlankSegmentsBase::loadFeaturePoints()
872{
873 mPoints.clear();
874 mExtent = canvas()->extent();
875 mBlankSegments.clear();
876 setCurrentBlankSegment( -1 );
877
878 if ( FID_IS_NULL( mCurrentFeatureId ) || !mSymbolLayer )
879 return;
880
881 QgsFeature feature;
882 feature = mLayer->getFeature( mCurrentFeatureId );
883 QString currentBlankSegments = feature.attribute( mBlankSegmentsFieldIndex ).toString();
884
885
886 // render feature to update mPoints
887 QgsRenderContext context = QgsRenderContext::fromMapSettings( canvas()->mapSettings() );
888 QgsCoordinateTransform transform( canvas()->mapSettings().layerTransform( mLayer ) );
889 QgsNullPaintDevice nullPaintDevice;
890 QPainter painter( &nullPaintDevice );
891 context.setPainter( &painter );
892 context.setCoordinateTransform( transform );
893 mSymbol->startRender( context );
894 mSymbol->renderFeature( feature, context );
895 mSymbol->stopRender( context );
896
897 if ( mPoints.isEmpty() )
898 return;
899
900 QString error;
901 QList<QList<QgsBlankSegmentUtils::BlankSegments>> allBlankSegments = QgsBlankSegmentUtils::parseBlankSegments( currentBlankSegments, context, mSymbolLayer->blankSegmentsUnit(), error );
902 if ( !error.isEmpty() )
903 {
904 emit messageEmitted( tr( "Error while parsing feature blank segments: %1" ).arg( error ), Qgis::MessageLevel::Critical );
905 return;
906 }
907
908 // iterate through all points from parts and ring to get the closest one
909 for ( int iPart = 0; iPart < mPoints.count(); iPart++ )
910 {
911 const QList<QPolygonF> &rings = mPoints.at( iPart );
912 for ( int iRing = 0; iRing < rings.count(); iRing++ )
913 {
914 if ( iPart >= allBlankSegments.count() || iRing >= allBlankSegments.at( iPart ).count() )
915 continue;
916
917 const QgsBlankSegmentUtils::BlankSegments &blankSegments = allBlankSegments.at( iPart ).at( iRing );
918
919 double currentLength = 0;
920 int iPoint = 0;
921 const QPolygonF &points = rings.at( iRing );
922 for ( QPair<double, double> ba : blankSegments )
923 {
924 while ( iPoint < points.count() - 1 && currentLength < ba.first )
925 {
926 iPoint++;
927 currentLength += QgsGeometryUtilsBase::distance2D( points.at( iPoint ), points.at( iPoint - 1 ) );
928 }
929
930 if ( iPoint == points.count() )
931 break;
932
933 int startIndex = iPoint;
934 Line l( points.at( iPoint ), points.at( iPoint - 1 ) );
935 QPointF startPt = points.at( iPoint ) + l.diffForInterval( currentLength - ba.first );
936
937 while ( iPoint < points.count() - 1 && currentLength < ba.second )
938 {
939 iPoint++;
940 currentLength += QgsGeometryUtilsBase::distance2D( points.at( iPoint ), points.at( iPoint - 1 ) );
941 }
942
943 if ( iPoint == points.count() )
944 break;
945
946 int endIndex = iPoint;
947 Line l2( points.at( iPoint ), points.at( iPoint - 1 ) );
948 QPointF endPt = points.at( iPoint ) + l2.diffForInterval( currentLength - ba.second );
949
950 mBlankSegments.emplace_back( new QgsBlankSegmentRubberBand( iPart, iRing, startIndex, endIndex, startPt, endPt, canvas(), mPoints ) );
951 }
952 }
953 }
954}
955
963
964QgsMapToolEditBlankSegmentsBase::QgsBlankSegmentRubberBand::QgsBlankSegmentRubberBand( int partIndex, int ringIndex, int startIndex, int endIndex, QPointF startPt, QPointF endPt, QgsMapCanvas *canvas, const FeaturePoints &points )
966{
967 setPoints( partIndex, ringIndex, startIndex, endIndex, startPt, endPt );
968}
969
970void QgsMapToolEditBlankSegmentsBase::QgsBlankSegmentRubberBand::setPoints( int partIndex, int ringIndex, int startIndex, int endIndex, QPointF startPt, QPointF endPt )
971{
972 mPartIndex = partIndex;
973 mRingIndex = ringIndex;
974 mStartIndex = startIndex;
975 mEndIndex = endIndex;
976 mStartPt = startPt;
977 mEndPt = endPt;
978
979 mNeedSwap = false;
980 if ( mStartIndex == mEndIndex )
981 {
982 try
983 {
984 if ( const QPointF &startIndexPoint = ::pointAt( mPoints, mPartIndex, mRingIndex, mStartIndex );
985 QgsGeometryUtilsBase::distance2D( startPt, startIndexPoint ) < QgsGeometryUtilsBase::distance2D( endPt, startIndexPoint ) )
986 {
987 mNeedSwap = true;
988 }
989 }
990 catch ( std::invalid_argument &e )
991 {
992 QgsDebugError( e.what() );
993 }
994 }
995 else if ( mStartIndex > mEndIndex )
996 {
997 mNeedSwap = true;
998 }
999
1000 updatePoints();
1001}
1002
1003void QgsMapToolEditBlankSegmentsBase::QgsBlankSegmentRubberBand::updatePoints()
1004{
1005 const QgsMapToPixel &m2p = *( mMapCanvas->getCoordinateTransform() );
1006
1007 reset();
1008 for ( int iPoint = 0; iPoint < pointsCount(); iPoint++ )
1009 {
1010 try
1011 {
1012 const QPointF &point = pointAt( iPoint );
1013 const QgsPointXY mapPoint = m2p.toMapCoordinates( point.x(), point.y() );
1014 addPoint( mapPoint, iPoint == pointsCount() - 1 ); // update only last one
1015 }
1016 catch ( std::invalid_argument &e )
1017 {
1018 QgsDebugError( e.what() );
1019 }
1020 }
1021}
1022
1023
1025{
1026 setPoints( blankSegment.mPartIndex, blankSegment.mRingIndex, blankSegment.getStartIndex(), blankSegment.getEndIndex(), blankSegment.getStartPoint(), blankSegment.getEndPoint() );
1027}
1028
1030{
1031 setWidth( QgsGuiUtils::scaleIconSize( highlighted ? 4 : 2 ) );
1032 update();
1033}
1034
1035
1037{
1038 return mStartPt;
1039}
1040
1042{
1043 return mEndPt;
1044}
1045
1050
1055
1060
1065
1067{
1068 return std::abs( mEndIndex - mStartIndex ) + 2;
1069}
1070
1072{
1073 return index == 0 ?
1074 // first point
1075 ( mNeedSwap ? mEndPt : mStartPt )
1076 // last point
1077 : ( index == pointsCount() - 1 ? ( mNeedSwap ? mStartPt : mEndPt )
1078 // point in between
1079 : ::pointAt( mPoints, mPartIndex, mRingIndex, ( mNeedSwap ? mEndIndex : mStartIndex ) + index - 1 ) );
1080}
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:161
@ Point
Points.
Definition qgis.h:377
RenderUnit
Rendering size units.
Definition qgis.h:5292
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:369
friend class QgsMapCanvas
Definition qgsmaptool.h:389
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.