QGIS API Documentation 4.1.0-Master (5bf3c20f3c9)
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
62 bool visitEnter( const QgsStyleEntityVisitorInterface::Node &node ) override
63 {
65 return false;
66
67 return true;
68 }
69
73 bool visitSymbol( const QgsSymbol *symbol )
74 {
75 if ( symbol && !mSymbol )
76 mSymbol = symbol;
77
78 for ( int idx = 0; idx < symbol->symbolLayerCount(); idx++ )
79 {
80 const QgsSymbolLayer *sl = symbol->symbolLayer( idx );
81 if ( sl->id() == mSymbolLayerId )
82 {
83 mSymbolLayer = dynamic_cast<const QgsTemplatedLineSymbolLayerBase *>( sl );
84 mSymbolLayerIndex = mSymbolLayer ? idx : -1;
85 return false;
86 }
87
88 // recurse over sub symbols
89 if ( const QgsSymbol *subSymbol = const_cast<QgsSymbolLayer *>( sl )->subSymbol(); 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
456 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 );
457 return;
458 }
459
460 mCurrentFeatureId = m.featureId();
461 loadFeaturePoints();
462 updateHoveredBlankSegment( e->pos() );
463
464 mState = State::FeatureSelected;
465 break;
466 }
467 case State::FeatureSelected:
468
469 // new blank segment selected
470 if ( mHoveredBlankSegmentIndex > -1 )
471 {
472 mState = State::BlankSegmentSelected;
473 setCurrentBlankSegment( mHoveredBlankSegmentIndex );
474 }
475 // init first point of new blank segment
476 else
477 {
478 mState = State::BlankSegmentCreationStarted;
479 double distance = -1;
480 int partIndex = -1;
481 int pointIndex = -1;
482 int ringIndex = -1;
483 QPointF P = closestPoint( e->pos(), distance, partIndex, ringIndex, pointIndex );
484 if ( distance > -1 && pointIndex > -1 )
485 {
486 mEditedBlankSegment->setPoints( partIndex, ringIndex, pointIndex, pointIndex, P, P );
487 updateStartEndRubberBand();
488 }
489 }
490
491 break;
492
493 case State::BlankSegmentSelected:
494 {
495 Q_ASSERT( mCurrentBlankSegmentIndex > -1 && mCurrentBlankSegmentIndex < static_cast<int>( mBlankSegments.size() ) );
496 const QObjectUniquePtr<QgsBlankSegmentRubberBand> &currentBlankSegment = mBlankSegments.at( mCurrentBlankSegmentIndex );
497
498 // selected blank segment has changed
499 if ( mHoveredBlankSegmentIndex > -1 && mHoveredBlankSegmentIndex != mCurrentBlankSegmentIndex )
500 {
501 setCurrentBlankSegment( mHoveredBlankSegmentIndex );
502 }
503 else
504 {
505 const double distanceFromStart = ( currentBlankSegment->getStartPoint() - e->pos() ).manhattanLength();
506 const double distanceFromEnd = ( currentBlankSegment->getEndPoint() - e->pos() ).manhattanLength();
507
508 // user clicked on start or end point to move it
509 if ( std::min( distanceFromStart, distanceFromEnd ) < TOLERANCE )
510 {
511 if ( distanceFromStart < distanceFromEnd )
512 {
513 // start point become end point because we always edit end point
514 mEditedBlankSegment->setPoints(
515 currentBlankSegment->getPartIndex(),
516 currentBlankSegment->getRingIndex(),
517 currentBlankSegment->getEndIndex(),
518 currentBlankSegment->getStartIndex(),
519 currentBlankSegment->getEndPoint(),
520 currentBlankSegment->getStartPoint()
521 );
522 }
523
524 mState = State::BlankSegmentModificationStarted;
525 }
526 }
527 }
528
529 break;
530
531 case State::BlankSegmentModificationStarted:
532 case State::BlankSegmentCreationStarted:
533
534 // this is a new one
535 if ( mCurrentBlankSegmentIndex < 0 )
536 {
537 mBlankSegments.emplace_back( new QgsBlankSegmentRubberBand( canvas(), mPoints ) );
538 mBlankSegments.back()->copyFrom( *mEditedBlankSegment );
539 mState = State::FeatureSelected;
540 }
541 // modify an existing one
542 else
543 {
544 QObjectUniquePtr<QgsBlankSegmentRubberBand> &blankSegment = mBlankSegments.at( mCurrentBlankSegmentIndex );
545 blankSegment->copyFrom( *mEditedBlankSegment );
546 mState = State::BlankSegmentSelected;
547 }
548
549 updateAttribute();
550 break;
551 }
552}
553
555{
556 // !!! We need to ignore event instead of accept them if we want to consume them
557 // see QgsMapCanvas::keyPressEvent
558
559 switch ( mState )
560 {
561 case State::SelectFeature:
562 return;
563
564 case State::BlankSegmentSelected:
565 if ( e->matches( QKeySequence::Delete ) && mCurrentBlankSegmentIndex > -1 )
566 {
567 mState = State::FeatureSelected;
568 int toRemoveIndex = mCurrentBlankSegmentIndex;
569 setCurrentBlankSegment( -1 );
570 mBlankSegments.erase( mBlankSegments.begin() + toRemoveIndex );
571 updateAttribute();
572 e->ignore();
573 }
574 else if ( e->matches( QKeySequence::Cancel ) )
575 {
576 mState = State::FeatureSelected;
577 setCurrentBlankSegment( -1 );
578 e->ignore();
579 }
580
581 break;
582
583 case State::FeatureSelected:
584 if ( e->matches( QKeySequence::Cancel ) )
585 {
586 mCurrentFeatureId = FID_NULL;
587 mState = State::SelectFeature;
588 loadFeaturePoints();
589 e->ignore();
590 }
591 break;
592
593 case State::BlankSegmentCreationStarted:
594 if ( e->matches( QKeySequence::Cancel ) )
595 {
596 mState = State::FeatureSelected;
597 setCurrentBlankSegment( -1 );
598 e->ignore();
599 }
600 break;
601
602 case State::BlankSegmentModificationStarted:
603 if ( e->matches( QKeySequence::Cancel ) )
604 {
605 mState = State::BlankSegmentSelected;
606 // force original blank segment
607 setCurrentBlankSegment( mCurrentBlankSegmentIndex );
608 e->ignore();
609 }
610 break;
611 }
612}
613
615{
616 double startDistance = 0;
617 const int startIndex = mNeedSwap ? mEndIndex : mStartIndex;
618 const QPointF startPt = mNeedSwap ? mEndPt : mStartPt;
619 for ( int i = 1; i < startIndex; i++ )
620 {
621 startDistance += QgsGeometryUtilsBase::distance2D( ::pointAt( mPoints, mPartIndex, mRingIndex, i - 1 ), ::pointAt( mPoints, mPartIndex, mRingIndex, i ) );
622 }
623
624 startDistance += QgsGeometryUtilsBase::distance2D( ::pointAt( mPoints, mPartIndex, mRingIndex, startIndex - 1 ), startPt );
625
626 double endDistance = startDistance;
627 for ( int i = 1; i < pointsCount(); i++ )
628 {
629 endDistance += QgsGeometryUtilsBase::distance2D( pointAt( i ), pointAt( i - 1 ) );
630 }
631
632 QgsRenderContext renderContext = QgsRenderContext::fromMapSettings( mMapCanvas->mapSettings() );
633
634 startDistance = renderContext.convertFromPainterUnits( startDistance, unit );
635 endDistance = renderContext.convertFromPainterUnits( endDistance, unit );
636
637 return QPair<double, double>( startDistance, endDistance );
638}
639
640int QgsMapToolEditBlankSegmentsBase::closestBlankSegmentIndex( const QPointF &point, double &distance ) const
641{
642 // search for closest blankSegment
643 distance = -1;
644 int iBlankSegment = -1;
645 for ( int i = 0; i < static_cast<int>( mBlankSegments.size() ); i++ )
646 {
647 const QObjectUniquePtr<QgsBlankSegmentRubberBand> &blankSegment = mBlankSegments.at( i );
648 for ( int iPoint = 1; iPoint < blankSegment->pointsCount(); iPoint++ )
649 {
650 double d = 0;
651 ProjectedPointStatus status = ProjectedPointStatus::OK;
652
653 try
654 {
655 projectedPoint( blankSegment->pointAt( iPoint - 1 ), blankSegment->pointAt( iPoint ), point, d, status );
656 }
657 catch ( std::invalid_argument &e )
658 {
659 QgsDebugError( e.what() );
660 continue;
661 }
662
663 switch ( status )
664 {
665 case ProjectedPointStatus::LINE_EMPTY:
666 continue;
667
668 case ProjectedPointStatus::NOT_ON_SEGMENT:
669 d = std::min( ( blankSegment->getStartPoint() - point ).manhattanLength(), ( blankSegment->getEndPoint() - point ).manhattanLength() );
670 break;
671
672 case ProjectedPointStatus::OK:
673 break;
674 }
675
676 if ( distance == -1 || d < distance )
677 {
678 distance = d;
679 iBlankSegment = i;
680 }
681 }
682 }
683
684 return iBlankSegment;
685}
686
687QPointF QgsMapToolEditBlankSegmentsBase::closestPoint( const QPointF &point, double &distance, int &partIndex, int &ringIndex, int &pointIndex ) const
688{
689 distance = -1;
690 QPointF currentPoint;
691
692 // iterate through all points from parts and ring to get the closest one
693 for ( int iPart = 0; iPart < mPoints.count(); iPart++ )
694 {
695 const QList<QPolygonF> &rings = mPoints.at( iPart );
696 for ( int iRing = 0; iRing < rings.count(); iRing++ )
697 {
698 const QPolygonF &points = rings.at( iRing );
699 for ( int i = 1; i < points.count(); i++ )
700 {
701 double d = 0;
702 ProjectedPointStatus status = ProjectedPointStatus::OK;
703 QPointF P = projectedPoint( points.at( i - 1 ), points.at( i ), point, d, status );
704 switch ( status )
705 {
706 case ProjectedPointStatus::LINE_EMPTY:
707 case ProjectedPointStatus::NOT_ON_SEGMENT:
708 continue;
709
710 case ProjectedPointStatus::OK:
711 break;
712 }
713
714 if ( distance == -1 || d < distance )
715 {
716 distance = d;
717 currentPoint = P;
718 partIndex = iPart;
719 ringIndex = iRing;
720 pointIndex = i;
721 }
722 }
723 }
724 }
725 return currentPoint;
726}
727
728
729void QgsMapToolEditBlankSegmentsBase::updateStartEndRubberBand()
730{
731 mStartRubberBand->reset( Qgis::GeometryType::Point );
732 mEndRubberBand->reset( Qgis::GeometryType::Point );
733
734 bool displayEndPoint = true;
735 switch ( mState )
736 {
737 case State::SelectFeature:
738 case State::BlankSegmentCreationStarted:
739 return;
740
741 case State::FeatureSelected:
742 displayEndPoint = false;
743 [[fallthrough]];
744
745 case State::BlankSegmentSelected:
746 case State::BlankSegmentModificationStarted:
747 break;
748 }
749
750 const QgsMapToPixel &m2p = *( canvas()->getCoordinateTransform() );
751
752 const QPointF &startPoint = mEditedBlankSegment->getStartPoint();
753 mStartRubberBand->addPoint( m2p.toMapCoordinates( startPoint.x(), startPoint.y() ) );
754
755 if ( displayEndPoint )
756 {
757 const QPointF &endPoint = mEditedBlankSegment->getEndPoint();
758 mEndRubberBand->addPoint( m2p.toMapCoordinates( endPoint.x(), endPoint.y() ) );
759 }
760}
761
762void QgsMapToolEditBlankSegmentsBase::updateHoveredBlankSegment( const QPoint &pos )
763{
764 double distance = -1;
765 int iBlankSegment = closestBlankSegmentIndex( pos, distance );
766
767 if ( mHoveredBlankSegmentIndex > -1 && mHoveredBlankSegmentIndex < static_cast<int>( mBlankSegments.size() ) && mHoveredBlankSegmentIndex != mCurrentBlankSegmentIndex )
768 {
769 mBlankSegments.at( mHoveredBlankSegmentIndex )->setHighlighted( false );
770 }
771
772 // blank segment is hovered
773 if ( iBlankSegment > -1 && distance < TOLERANCE )
774 {
775 mHoveredBlankSegmentIndex = iBlankSegment;
776 if ( mHoveredBlankSegmentIndex < static_cast<int>( mBlankSegments.size() ) )
777 {
778 mBlankSegments.at( mHoveredBlankSegmentIndex )->setHighlighted( true );
779 }
780 }
781 // no blank segment hovered, display the first point to create a new blank segment
782 else
783 {
784 mHoveredBlankSegmentIndex = -1;
785 }
786}
787
788void QgsMapToolEditBlankSegmentsBase::setCurrentBlankSegment( int currentBlankSegmentIndex )
789{
790 // copy current blank segment so we can edit it later (and hide the original one)
791
792 if ( mCurrentBlankSegmentIndex > -1 && mCurrentBlankSegmentIndex < static_cast<int>( mBlankSegments.size() ) )
793 {
794 mBlankSegments.at( mCurrentBlankSegmentIndex )->setVisible( true );
795 mBlankSegments.at( mCurrentBlankSegmentIndex )->setHighlighted( false );
796 }
797
798 mCurrentBlankSegmentIndex = currentBlankSegmentIndex;
799 if ( mCurrentBlankSegmentIndex > -1 && mCurrentBlankSegmentIndex < static_cast<int>( mBlankSegments.size() ) )
800 {
801 mBlankSegments.at( mCurrentBlankSegmentIndex )->setVisible( false );
802 mEditedBlankSegment->copyFrom( *( mBlankSegments.at( mCurrentBlankSegmentIndex ).get() ) );
803 }
804 else
805 {
806 mEditedBlankSegment->setVisible( false );
807 }
808
809 updateStartEndRubberBand();
810}
811
812void QgsMapToolEditBlankSegmentsBase::updateAttribute()
813{
814 if ( !mSymbolLayer )
815 return;
816
817 QList<QList<QgsBlankSegmentUtils::BlankSegments>> blankSegments;
818 for ( const QObjectUniquePtr<QgsBlankSegmentRubberBand> &blankSegment : mBlankSegments )
819 {
820 try
821 {
822 QPair<double, double> startEndDistance = blankSegment->getStartEndDistance( mSymbolLayer->blankSegmentsUnit() );
823
824 const int partIndex = blankSegment->getPartIndex();
825 if ( partIndex >= blankSegments.count() )
826 blankSegments.resize( partIndex + 1 );
827
828 QList<QgsBlankSegmentUtils::BlankSegments> &rings = blankSegments[partIndex];
829 const int ringIndex = blankSegment->getRingIndex();
830 if ( ringIndex >= rings.count() )
831 rings.resize( ringIndex + 1 );
832
833 QgsBlankSegmentUtils::BlankSegments &segments = rings[ringIndex];
834 segments << startEndDistance;
835 }
836 catch ( std::invalid_argument &e )
837 {
838 QgsDebugError( e.what() );
839 return;
840 }
841 }
842
843 QStringList strParts;
844 for ( QList<QgsBlankSegmentUtils::BlankSegments> &part : blankSegments )
845 {
846 QStringList strRings;
847 for ( QgsBlankSegmentUtils::BlankSegments &ring : part )
848 {
849 std::sort( ring.begin(), ring.end() );
850 QStringList strDistances;
851 for ( const QPair<double, double> &distance : ring )
852 {
853 strDistances << ( QString::number( distance.first ) + " " + QString::number( distance.second ) );
854 }
855
856 strRings << "(" + strDistances.join( "," ) + ")";
857 }
858
859 strParts << "(" + strRings.join( "," ) + ")";
860 }
861
862 QString strNewBlankSegments = "(" + strParts.join( "," ) + ")";
863
864 mLayer->beginEditCommand( tr( "Set blank segment list" ) );
865 if ( mLayer->changeAttributeValue( mCurrentFeatureId, mBlankSegmentsFieldIndex, strNewBlankSegments ) )
866 {
867 mLayer->endEditCommand();
868 mLayer->triggerRepaint();
869 }
870 else
871 {
872 mLayer->destroyEditCommand();
873 }
874}
875
876void QgsMapToolEditBlankSegmentsBase::loadFeaturePoints()
877{
878 mPoints.clear();
879 mExtent = canvas()->extent();
880 mBlankSegments.clear();
881 setCurrentBlankSegment( -1 );
882
883 if ( FID_IS_NULL( mCurrentFeatureId ) || !mSymbolLayer )
884 return;
885
886 QgsFeature feature;
887 feature = mLayer->getFeature( mCurrentFeatureId );
888 QString currentBlankSegments = feature.attribute( mBlankSegmentsFieldIndex ).toString();
889
890
891 // render feature to update mPoints
892 QgsRenderContext context = QgsRenderContext::fromMapSettings( canvas()->mapSettings() );
893 QgsCoordinateTransform transform( canvas()->mapSettings().layerTransform( mLayer ) );
894 QgsNullPaintDevice nullPaintDevice;
895 QPainter painter( &nullPaintDevice );
896 context.setPainter( &painter );
897 context.setCoordinateTransform( transform );
898 mSymbol->startRender( context );
899 mSymbol->renderFeature( feature, context );
900 mSymbol->stopRender( context );
901
902 if ( mPoints.isEmpty() )
903 return;
904
905 QString error;
906 QList<QList<QgsBlankSegmentUtils::BlankSegments>> allBlankSegments = QgsBlankSegmentUtils::parseBlankSegments( currentBlankSegments, context, mSymbolLayer->blankSegmentsUnit(), error );
907 if ( !error.isEmpty() )
908 {
909 emit messageEmitted( tr( "Error while parsing feature blank segments: %1" ).arg( error ), Qgis::MessageLevel::Critical );
910 return;
911 }
912
913 // iterate through all points from parts and ring to get the closest one
914 for ( int iPart = 0; iPart < mPoints.count(); iPart++ )
915 {
916 const QList<QPolygonF> &rings = mPoints.at( iPart );
917 for ( int iRing = 0; iRing < rings.count(); iRing++ )
918 {
919 if ( iPart >= allBlankSegments.count() || iRing >= allBlankSegments.at( iPart ).count() )
920 continue;
921
922 const QgsBlankSegmentUtils::BlankSegments &blankSegments = allBlankSegments.at( iPart ).at( iRing );
923
924 double currentLength = 0;
925 int iPoint = 0;
926 const QPolygonF &points = rings.at( iRing );
927 for ( QPair<double, double> ba : blankSegments )
928 {
929 while ( iPoint < points.count() - 1 && currentLength < ba.first )
930 {
931 iPoint++;
932 currentLength += QgsGeometryUtilsBase::distance2D( points.at( iPoint ), points.at( iPoint - 1 ) );
933 }
934
935 if ( iPoint == points.count() )
936 break;
937
938 int startIndex = iPoint;
939 Line l( points.at( iPoint ), points.at( iPoint - 1 ) );
940 QPointF startPt = points.at( iPoint ) + l.diffForInterval( currentLength - ba.first );
941
942 while ( iPoint < points.count() - 1 && currentLength < ba.second )
943 {
944 iPoint++;
945 currentLength += QgsGeometryUtilsBase::distance2D( points.at( iPoint ), points.at( iPoint - 1 ) );
946 }
947
948 if ( iPoint == points.count() )
949 break;
950
951 int endIndex = iPoint;
952 Line l2( points.at( iPoint ), points.at( iPoint - 1 ) );
953 QPointF endPt = points.at( iPoint ) + l2.diffForInterval( currentLength - ba.second );
954
955 mBlankSegments.emplace_back( new QgsBlankSegmentRubberBand( iPart, iRing, startIndex, endIndex, startPt, endPt, canvas(), mPoints ) );
956 }
957 }
958 }
959}
960
968
970 int partIndex, int ringIndex, int startIndex, int endIndex, QPointF startPt, QPointF endPt, QgsMapCanvas *canvas, const FeaturePoints &points
971)
973{
974 setPoints( partIndex, ringIndex, startIndex, endIndex, startPt, endPt );
975}
976
977void QgsMapToolEditBlankSegmentsBase::QgsBlankSegmentRubberBand::setPoints( int partIndex, int ringIndex, int startIndex, int endIndex, QPointF startPt, QPointF endPt )
978{
979 mPartIndex = partIndex;
980 mRingIndex = ringIndex;
981 mStartIndex = startIndex;
982 mEndIndex = endIndex;
983 mStartPt = startPt;
984 mEndPt = endPt;
985
986 mNeedSwap = false;
987 if ( mStartIndex == mEndIndex )
988 {
989 try
990 {
991 if ( const QPointF &startIndexPoint = ::pointAt( mPoints, mPartIndex, mRingIndex, mStartIndex );
992 QgsGeometryUtilsBase::distance2D( startPt, startIndexPoint ) < QgsGeometryUtilsBase::distance2D( endPt, startIndexPoint ) )
993 {
994 mNeedSwap = true;
995 }
996 }
997 catch ( std::invalid_argument &e )
998 {
999 QgsDebugError( e.what() );
1000 }
1001 }
1002 else if ( mStartIndex > mEndIndex )
1003 {
1004 mNeedSwap = true;
1005 }
1006
1007 updatePoints();
1008}
1009
1010void QgsMapToolEditBlankSegmentsBase::QgsBlankSegmentRubberBand::updatePoints()
1011{
1012 const QgsMapToPixel &m2p = *( mMapCanvas->getCoordinateTransform() );
1013
1014 reset();
1015 for ( int iPoint = 0; iPoint < pointsCount(); iPoint++ )
1016 {
1017 try
1018 {
1019 const QPointF &point = pointAt( iPoint );
1020 const QgsPointXY mapPoint = m2p.toMapCoordinates( point.x(), point.y() );
1021 addPoint( mapPoint, iPoint == pointsCount() - 1 ); // update only last one
1022 }
1023 catch ( std::invalid_argument &e )
1024 {
1025 QgsDebugError( e.what() );
1026 }
1027 }
1028}
1029
1030
1032{
1033 setPoints( blankSegment.mPartIndex, blankSegment.mRingIndex, blankSegment.getStartIndex(), blankSegment.getEndIndex(), blankSegment.getStartPoint(), blankSegment.getEndPoint() );
1034}
1035
1037{
1038 setWidth( QgsGuiUtils::scaleIconSize( highlighted ? 4 : 2 ) );
1039 update();
1040}
1041
1042
1044{
1045 return mStartPt;
1046}
1047
1049{
1050 return mEndPt;
1051}
1052
1057
1062
1067
1072
1074{
1075 return std::abs( mEndIndex - mStartIndex ) + 2;
1076}
1077
1079{
1080 return index == 0 ?
1081 // first point
1082 ( mNeedSwap ? mEndPt : mStartPt )
1083 // last point
1084 : ( index == pointsCount() - 1 ? ( mNeedSwap ? mStartPt : mEndPt )
1085 // point in between
1086 : ::pointAt( mPoints, mPartIndex, mRingIndex, ( mNeedSwap ? mEndIndex : mStartIndex ) + index - 1 ) );
1087}
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:62
@ Critical
Critical/error message.
Definition qgis.h:163
@ Point
Points.
Definition qgis.h:380
RenderUnit
Rendering size units.
Definition qgis.h:5340
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:1393
@ SymbolEntity
Symbols.
Definition qgsstyle.h:205
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:227
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:357
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.