QGIS API Documentation 4.1.0-Master (31622b25bb0)
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
20#include "qgsauxiliarystorage.h"
22#include "qgsfeatureid.h"
23#include "qgsguiutils.h"
24#include "qgslinesymbollayer.h"
25#include "qgsmapcanvas.h"
26#include "qgsmapmouseevent.h"
29#include "qgsrubberband.h"
33#include "qgssnappingutils.h"
35#include "qgssymbol.h"
36#include "qgsvectorlayer.h"
37
38#include "moc_qgsmaptooleditblanksegments.cpp"
39
40constexpr int TOLERANCE = 20;
41
42
44
46
47namespace
48{
49
53 class QgsSymbolLayerFinder : public QgsStyleEntityVisitorInterface
54 {
55 public:
60 QgsSymbolLayerFinder( const QString &symbolLayerId )
61 : mSymbolLayerId( symbolLayerId )
62 {}
63
64 bool visitEnter( const QgsStyleEntityVisitorInterface::Node &node ) override
65 {
67 return false;
68
69 return true;
70 }
71
75 bool visitSymbol( const QgsSymbol *symbol )
76 {
77 if ( symbol && !mSymbol )
78 mSymbol = symbol;
79
80 for ( int idx = 0; idx < symbol->symbolLayerCount(); idx++ )
81 {
82 const QgsSymbolLayer *sl = symbol->symbolLayer( idx );
83 if ( sl->id() == mSymbolLayerId )
84 {
85 mSymbolLayer = dynamic_cast<const QgsTemplatedLineSymbolLayerBase *>( sl );
86 mSymbolLayerIndex = mSymbolLayer ? idx : -1;
87 return false;
88 }
89
90 // recurse over sub symbols
91 if ( const QgsSymbol *subSymbol = const_cast<QgsSymbolLayer *>( sl )->subSymbol(); subSymbol && !visitSymbol( subSymbol ) )
92 {
93 return false;
94 }
95 }
96
97 return true;
98 }
99
100 bool visit( const QgsStyleEntityVisitorInterface::StyleLeaf &leaf ) override
101 {
102 if ( leaf.entity && leaf.entity->type() == QgsStyle::SymbolEntity )
103 {
104 auto symbolEntity = static_cast<const QgsStyleSymbolEntity *>( leaf.entity );
105 if ( symbolEntity->symbol() )
106 visitSymbol( symbolEntity->symbol() );
107 }
108 return true;
109 }
110
114 const QgsSymbol *symbol() const { return mSymbol; }
115
119 const QgsTemplatedLineSymbolLayerBase *symbolLayer() const { return mSymbolLayer; }
120
124 int symbolLayerIndex() const { return mSymbolLayerIndex; }
125
126 private:
127 const QgsSymbol *mSymbol = nullptr;
128 const QgsTemplatedLineSymbolLayerBase *mSymbolLayer = nullptr;
129 int mSymbolLayerIndex = -1;
130 const QString &mSymbolLayerId;
131 };
132
137 const QPointF &pointAt( const QList<QList<QPolygonF>> &points, int partIndex, int ringIndex, int pointIndex )
138 {
139 if ( partIndex < 0 || partIndex >= points.count() )
140 throw std::invalid_argument( "Blank segments internal error : Invalid part index" );
141
142 const QList<QPolygonF> &rings = points.at( partIndex );
143 if ( ringIndex < 0 || ringIndex >= rings.count() )
144 throw std::invalid_argument( "blank segments internal error : Invalid ring index" );
145
146 const QPolygonF &pts = rings.at( ringIndex );
147 if ( pointIndex < 0 || pointIndex >= pts.count() )
148 throw std::invalid_argument( "blank segments internal error : Invalid point index" );
149
150 return pts.at( pointIndex );
151 }
152
153 enum ProjectedPointStatus
154 {
155 OK, // ok, point is on the segment
156 LINE_EMPTY, // line is empty, cannot project point
157 NOT_ON_SEGMENT // point is on the line, but not on the segment
158 };
159
164 QPointF projectedPoint( const QPointF &lineStartPt, const QPointF &lineEndPt, const QPointF &point, double &distance, ProjectedPointStatus &status )
165 {
166 status = ProjectedPointStatus::OK;
167 distance = -1;
168
169 const double Ax = lineStartPt.x();
170 const double Ay = lineStartPt.y();
171 const double Bx = lineEndPt.x();
172 const double By = lineEndPt.y();
173 const double Cx = point.x();
174 const double Cy = point.y();
175
176 const double length = QgsGeometryUtilsBase::distance2D( Ax, Ay, Bx, By );
177 if ( length == 0 )
178 {
179 status = ProjectedPointStatus::LINE_EMPTY;
180 return QPointF();
181 }
182
183 const double r = ( ( Cx - Ax ) * ( Bx - Ax ) + ( Cy - Ay ) * ( By - Ay ) ) / std::pow( length, 2 );
184 if ( r < 0 or r > 1 )
185 {
186 status = ProjectedPointStatus::NOT_ON_SEGMENT;
187 }
188
189 // projected point
190 const double Px = Ax + r * ( Bx - Ax );
191 const double Py = Ay + r * ( By - Ay );
192
193 distance = QgsGeometryUtilsBase::distance2D( Cx, Cy, Px, Py );
194
195 return QPointF( Px, Py );
196 }
197
198} //namespace
200
201
208{
209 public:
221 QgsBlankSegmentRubberBand( int partIndex, int ringIndex, int startIndex, int endIndex, QPointF startPt, QPointF endPt, QgsMapCanvas *canvas, const FeaturePoints &points );
222
228
238 void setPoints( int partIndex, int ringIndex, int startIndex, int endIndex, QPointF startPt, QPointF endPt );
239
243 void copyFrom( const QgsBlankSegmentRubberBand &blankSegment );
244
248 void setHighlighted( bool highlighted );
249
253 const QPointF &getStartPoint() const;
254
258 const QPointF &getEndPoint() const;
259
263 int getStartIndex() const;
264
268 int getEndIndex() const;
269
273 int getPartIndex() const;
274
278 int getRingIndex() const;
279
283 QPair<double, double> getStartEndDistance( Qgis::RenderUnit unit ) const;
284
288 int pointsCount() const;
289
293 const QPointF &pointAt( int index ) const;
294
295 private:
299 void updatePoints();
300
301 int mPartIndex = -1;
302 int mRingIndex = -1;
303 int mStartIndex = -1;
304 int mEndIndex = -1;
305 QPointF mStartPt;
306 QPointF mEndPt;
307 bool mNeedSwap = false;
308 const FeaturePoints &mPoints;
309};
310
312 : QgsMapTool( canvas )
313 , mLayer( layer )
314 , mSymbolLayer( symbolLayer )
315 , mPropertyButton( propertyButton )
316 , mEditedBlankSegment( new QgsBlankSegmentRubberBand( canvas, mPoints ) )
317 , mStartRubberBand( new QgsRubberBand( canvas, Qgis::GeometryType::Point ) )
318 , mEndRubberBand( new QgsRubberBand( canvas, Qgis::GeometryType::Point ) )
319{
320 auto initRubberBand = []( QgsRubberBand *rb ) {
321 rb->setWidth( QgsGuiUtils::scaleIconSize( 4 ) );
324 };
325
326 initRubberBand( mStartRubberBand );
327 initRubberBand( mEndRubberBand );
328
329 mEditedBlankSegment->setHighlighted( true );
330
331 connect( mPropertyButton, &QgsPropertyOverrideButton::changed, this, [this] {
332 mBlankSegmentsFieldIndex = dataDefinedColumnIndex( static_cast<int>( QgsSymbolLayer::Property::BlankSegments ), mSymbolLayer->dataDefinedProperties(), mLayer );
333
334 mCurrentFeatureId = FID_NULL;
335 mState = SelectFeature;
336 loadFeaturePoints();
337 } );
338}
339
341
343{
344 if ( !mLayer || !mLayer->renderer() || !mSymbolLayer )
345 return;
346
347 if ( !mSymbol || !mRenderedPointsSymbolLayer )
348 {
349 // search and symbol and symbol layer
350 QgsSymbolLayerFinder finder( mSymbolLayer->id() );
351 mLayer->renderer()->accept( &finder );
352 if ( finder.symbol() && finder.symbolLayer() && finder.symbolLayerIndex() > -1 )
353 {
354 mSymbol.reset( finder.symbol()->clone() );
355 if ( mRenderedPointsSymbolLayer = createRenderedPointsSymbolLayer( finder.symbolLayer() ); mRenderedPointsSymbolLayer )
356 {
357 // set our on symbol layer to later retrieve rendered points
358 mSymbol->changeSymbolLayer( finder.symbolLayerIndex(), mRenderedPointsSymbolLayer );
359 }
360 else
361 {
362 mSymbol.reset();
363 QgsDebugError( "Fail to create fake templated line symbol layer" );
364 }
365 }
366 else
367 {
368 QgsDebugError( "Fail to retrieve symbol and templated line symbol layer" );
369 }
370 }
371
373}
374
376{
377 if ( !mSymbol || mPoints.isEmpty() )
378 return;
379
380 const QPoint &pos = e->pos();
381
382 if ( canvas()->extent() != mExtent )
383 {
384 loadFeaturePoints();
385
386 // If edition has been started, we cancel them because they are no longer in the same extent reference
387 switch ( mState )
388 {
389 case State::SelectFeature:
390 case State::BlankSegmentSelected:
391 case State::FeatureSelected:
392 break;
393
394 case State::BlankSegmentCreationStarted:
395 mState = State::FeatureSelected;
396 setCurrentBlankSegment( -1 );
397 break;
398
399 case State::BlankSegmentModificationStarted:
400 mState = State::BlankSegmentSelected;
401 // force original blank segment
402 setCurrentBlankSegment( mCurrentBlankSegmentIndex );
403 break;
404 }
405 }
406
407 switch ( mState )
408 {
409 case State::SelectFeature:
410 return;
411
412 case State::BlankSegmentSelected:
413 updateHoveredBlankSegment( pos );
414 break;
415
416 case State::FeatureSelected:
417 {
418 updateHoveredBlankSegment( pos );
419
420 // display current start point to create a new blank segment
421 mStartRubberBand->setVisible( mHoveredBlankSegmentIndex == -1 );
422 if ( mHoveredBlankSegmentIndex == -1 )
423 {
424 const QgsMapToPixel &m2p = *( canvas()->getCoordinateTransform() );
425 mStartRubberBand->reset( Qgis::GeometryType::Point );
426 double distance;
427 int partIndex = -1, ringIndex = -1, pointIndex = -1;
428 const QPointF closestPt = closestPoint( pos, distance, partIndex, ringIndex, pointIndex );
429
430 // for now end point is the same as start one
431 mStartRubberBand->addPoint( m2p.toMapCoordinates( closestPt.x(), closestPt.y() ) );
432 }
433
434 break;
435 }
436
437 case State::BlankSegmentModificationStarted:
438 case State::BlankSegmentCreationStarted:
439 {
440 double distance = -1;
441 int partIndex = -1;
442 int pointIndex = -1;
443 int ringIndex = -1;
444 QPointF P = closestPoint( pos, distance, partIndex, ringIndex, pointIndex );
445 if ( distance > -1 && pointIndex > -1 )
446 {
447 mEditedBlankSegment->setPoints( partIndex, ringIndex, mEditedBlankSegment->getStartIndex(), pointIndex, mEditedBlankSegment->getStartPoint(), P );
448 updateStartEndRubberBand();
449 }
450 }
451 break;
452 }
453}
454
455void QgsMapToolEditBlankSegmentsBase::selectFeature( QgsMapMouseEvent *event )
456{
457 if ( !mLayer || !mSymbolLayer || !mPropertyButton || mPropertyButton->propertyKey() != static_cast<int>( QgsSymbolLayer::Property::BlankSegments ) )
458 return;
459
460 // find the closest feature to the pressed position
461 const QgsPointLocator::Match m = mCanvas->snappingUtils()->snapToCurrentLayer( event->pos(), QgsPointLocator::Types( QgsPointLocator::Edge ) );
462 if ( !m.isValid() )
463 {
464 emit
465 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 );
466 return;
467 }
468
469 // initialize field index if not already done
470 if ( mBlankSegmentsFieldIndex < 0 )
471 {
472 mBlankSegmentsFieldIndex = dataDefinedColumnIndex( static_cast<int>( QgsSymbolLayer::Property::BlankSegments ), mSymbolLayer->dataDefinedProperties(), mLayer );
473 }
474
475 // No data defined property, create one
476 if ( mBlankSegmentsFieldIndex < 0 )
477 {
478 if ( !mLayer->auxiliaryLayer() )
479 {
480 QgsNewAuxiliaryLayerDialog dlg( mLayer );
481 dlg.exec();
482 }
483
484 if ( !mLayer->auxiliaryLayer() )
485 return;
486
487 mBlankSegmentsFieldIndex = QgsAuxiliaryLayer::createProperty( QgsSymbolLayer::Property::BlankSegments, mLayer, mSymbolLayer, false );
488 if ( mBlankSegmentsFieldIndex < 0 )
489 {
490 emit messageEmitted( tr( "Failed to create blank segments auxiliary field for layer '%1'" ).arg( mLayer->name() ) );
491 return;
492 }
493
494 // update property override button from new property
495 QgsPropertyCollection c = mSymbolLayer->dataDefinedProperties();
496 QgsProperty property = c.property( static_cast<int>( QgsSymbolLayer::Property::BlankSegments ) );
497 mPropertyButton->updateFieldLists();
498 mPropertyButton->setToProperty( property );
499 emit mPropertyButton->changed(); // setToProperty doesn't fire the changed signal
500 }
501
502 // start editing
503
504 const bool usesAuxFields = mLayer->fields().fieldOrigin( mBlankSegmentsFieldIndex ) == Qgis::FieldOrigin::Join;
505 if ( !usesAuxFields && !mLayer->isEditable() )
506 {
507 if ( mLayer->startEditing() )
508 {
509 emit messageEmitted( tr( "Layer “%1” was made editable" ).arg( mLayer->name() ) );
510 }
511 else
512 {
513 emit messageEmitted( tr( "Cannot modify blank segments — the layer “%1” could not be made editable" ).arg( mLayer->name() ) );
514 return;
515 }
516 }
517
518 mCurrentFeatureId = m.featureId();
519 loadFeaturePoints();
520 updateHoveredBlankSegment( event->pos() );
521
522 mState = State::FeatureSelected;
523}
524
526{
527 switch ( mState )
528 {
529 case State::SelectFeature:
530 {
531 selectFeature( e );
532 break;
533 }
534 case State::FeatureSelected:
535
536 // new blank segment selected
537 if ( mHoveredBlankSegmentIndex > -1 )
538 {
539 mState = State::BlankSegmentSelected;
540 setCurrentBlankSegment( mHoveredBlankSegmentIndex );
541 }
542 // init first point of new blank segment
543 else
544 {
545 mState = State::BlankSegmentCreationStarted;
546 double distance = -1;
547 int partIndex = -1;
548 int pointIndex = -1;
549 int ringIndex = -1;
550 QPointF P = closestPoint( e->pos(), distance, partIndex, ringIndex, pointIndex );
551 if ( distance > -1 && pointIndex > -1 )
552 {
553 mEditedBlankSegment->setPoints( partIndex, ringIndex, pointIndex, pointIndex, P, P );
554 updateStartEndRubberBand();
555 }
556 }
557
558 break;
559
560 case State::BlankSegmentSelected:
561 {
562 Q_ASSERT( mCurrentBlankSegmentIndex > -1 && mCurrentBlankSegmentIndex < static_cast<int>( mBlankSegments.size() ) );
563 const QObjectUniquePtr<QgsBlankSegmentRubberBand> &currentBlankSegment = mBlankSegments.at( mCurrentBlankSegmentIndex );
564
565 // selected blank segment has changed
566 if ( mHoveredBlankSegmentIndex > -1 && mHoveredBlankSegmentIndex != mCurrentBlankSegmentIndex )
567 {
568 setCurrentBlankSegment( mHoveredBlankSegmentIndex );
569 }
570 else
571 {
572 const double distanceFromStart = ( currentBlankSegment->getStartPoint() - e->pos() ).manhattanLength();
573 const double distanceFromEnd = ( currentBlankSegment->getEndPoint() - e->pos() ).manhattanLength();
574
575 // user clicked on start or end point to move it
576 if ( std::min( distanceFromStart, distanceFromEnd ) < TOLERANCE )
577 {
578 if ( distanceFromStart < distanceFromEnd )
579 {
580 // start point become end point because we always edit end point
581 mEditedBlankSegment->setPoints(
582 currentBlankSegment->getPartIndex(),
583 currentBlankSegment->getRingIndex(),
584 currentBlankSegment->getEndIndex(),
585 currentBlankSegment->getStartIndex(),
586 currentBlankSegment->getEndPoint(),
587 currentBlankSegment->getStartPoint()
588 );
589 }
590
591 mState = State::BlankSegmentModificationStarted;
592 }
593 }
594 }
595
596 break;
597
598 case State::BlankSegmentModificationStarted:
599 case State::BlankSegmentCreationStarted:
600
601 // this is a new one
602 if ( mCurrentBlankSegmentIndex < 0 )
603 {
604 mBlankSegments.emplace_back( new QgsBlankSegmentRubberBand( canvas(), mPoints ) );
605 mBlankSegments.back()->copyFrom( *mEditedBlankSegment );
606 mState = State::FeatureSelected;
607 }
608 // modify an existing one
609 else
610 {
611 QObjectUniquePtr<QgsBlankSegmentRubberBand> &blankSegment = mBlankSegments.at( mCurrentBlankSegmentIndex );
612 blankSegment->copyFrom( *mEditedBlankSegment );
613 mState = State::BlankSegmentSelected;
614 }
615
616 updateAttribute();
617 break;
618 }
619}
620
622{
623 // !!! We need to ignore event instead of accept them if we want to consume them
624 // see QgsMapCanvas::keyPressEvent
625
626 switch ( mState )
627 {
628 case State::SelectFeature:
629 return;
630
631 case State::BlankSegmentSelected:
632 if ( e->matches( QKeySequence::Delete ) && mCurrentBlankSegmentIndex > -1 )
633 {
634 mState = State::FeatureSelected;
635 int toRemoveIndex = mCurrentBlankSegmentIndex;
636 setCurrentBlankSegment( -1 );
637 mBlankSegments.erase( mBlankSegments.begin() + toRemoveIndex );
638 updateAttribute();
639 e->ignore();
640 }
641 else if ( e->matches( QKeySequence::Cancel ) )
642 {
643 mState = State::FeatureSelected;
644 setCurrentBlankSegment( -1 );
645 e->ignore();
646 }
647
648 break;
649
650 case State::FeatureSelected:
651 if ( e->matches( QKeySequence::Cancel ) )
652 {
653 mCurrentFeatureId = FID_NULL;
654 mState = State::SelectFeature;
655 loadFeaturePoints();
656 e->ignore();
657 }
658 break;
659
660 case State::BlankSegmentCreationStarted:
661 if ( e->matches( QKeySequence::Cancel ) )
662 {
663 mState = State::FeatureSelected;
664 setCurrentBlankSegment( -1 );
665 e->ignore();
666 }
667 break;
668
669 case State::BlankSegmentModificationStarted:
670 if ( e->matches( QKeySequence::Cancel ) )
671 {
672 mState = State::BlankSegmentSelected;
673 // force original blank segment
674 setCurrentBlankSegment( mCurrentBlankSegmentIndex );
675 e->ignore();
676 }
677 break;
678 }
679}
680
682{
683 double startDistance = 0;
684 const int startIndex = mNeedSwap ? mEndIndex : mStartIndex;
685 const QPointF startPt = mNeedSwap ? mEndPt : mStartPt;
686 for ( int i = 1; i < startIndex; i++ )
687 {
688 startDistance += QgsGeometryUtilsBase::distance2D( ::pointAt( mPoints, mPartIndex, mRingIndex, i - 1 ), ::pointAt( mPoints, mPartIndex, mRingIndex, i ) );
689 }
690
691 startDistance += QgsGeometryUtilsBase::distance2D( ::pointAt( mPoints, mPartIndex, mRingIndex, startIndex - 1 ), startPt );
692
693 double endDistance = startDistance;
694 for ( int i = 1; i < pointsCount(); i++ )
695 {
696 endDistance += QgsGeometryUtilsBase::distance2D( pointAt( i ), pointAt( i - 1 ) );
697 }
698
699 QgsRenderContext renderContext = QgsRenderContext::fromMapSettings( mMapCanvas->mapSettings() );
700
701 startDistance = renderContext.convertFromPainterUnits( startDistance, unit );
702 endDistance = renderContext.convertFromPainterUnits( endDistance, unit );
703
704 return QPair<double, double>( startDistance, endDistance );
705}
706
707int QgsMapToolEditBlankSegmentsBase::closestBlankSegmentIndex( const QPointF &point, double &distance ) const
708{
709 // search for closest blankSegment
710 distance = -1;
711 int iBlankSegment = -1;
712 for ( int i = 0; i < static_cast<int>( mBlankSegments.size() ); i++ )
713 {
714 const QObjectUniquePtr<QgsBlankSegmentRubberBand> &blankSegment = mBlankSegments.at( i );
715 for ( int iPoint = 1; iPoint < blankSegment->pointsCount(); iPoint++ )
716 {
717 double d = 0;
718 ProjectedPointStatus status = ProjectedPointStatus::OK;
719
720 try
721 {
722 projectedPoint( blankSegment->pointAt( iPoint - 1 ), blankSegment->pointAt( iPoint ), point, d, status );
723 }
724 catch ( std::invalid_argument &e )
725 {
726 QgsDebugError( e.what() );
727 continue;
728 }
729
730 switch ( status )
731 {
732 case ProjectedPointStatus::LINE_EMPTY:
733 continue;
734
735 case ProjectedPointStatus::NOT_ON_SEGMENT:
736 d = std::min( ( blankSegment->getStartPoint() - point ).manhattanLength(), ( blankSegment->getEndPoint() - point ).manhattanLength() );
737 break;
738
739 case ProjectedPointStatus::OK:
740 break;
741 }
742
743 if ( distance == -1 || d < distance )
744 {
745 distance = d;
746 iBlankSegment = i;
747 }
748 }
749 }
750
751 return iBlankSegment;
752}
753
754QPointF QgsMapToolEditBlankSegmentsBase::closestPoint( const QPointF &point, double &distance, int &partIndex, int &ringIndex, int &pointIndex ) const
755{
756 distance = -1;
757 QPointF currentPoint;
758
759 // iterate through all points from parts and ring to get the closest one
760 for ( int iPart = 0; iPart < mPoints.count(); iPart++ )
761 {
762 const QList<QPolygonF> &rings = mPoints.at( iPart );
763 for ( int iRing = 0; iRing < rings.count(); iRing++ )
764 {
765 const QPolygonF &points = rings.at( iRing );
766 for ( int i = 1; i < points.count(); i++ )
767 {
768 double d = 0;
769 ProjectedPointStatus status = ProjectedPointStatus::OK;
770 QPointF P = projectedPoint( points.at( i - 1 ), points.at( i ), point, d, status );
771 switch ( status )
772 {
773 case ProjectedPointStatus::LINE_EMPTY:
774 case ProjectedPointStatus::NOT_ON_SEGMENT:
775 continue;
776
777 case ProjectedPointStatus::OK:
778 break;
779 }
780
781 if ( distance == -1 || d < distance )
782 {
783 distance = d;
784 currentPoint = P;
785 partIndex = iPart;
786 ringIndex = iRing;
787 pointIndex = i;
788 }
789 }
790 }
791 }
792 return currentPoint;
793}
794
795
796void QgsMapToolEditBlankSegmentsBase::updateStartEndRubberBand()
797{
798 mStartRubberBand->reset( Qgis::GeometryType::Point );
799 mEndRubberBand->reset( Qgis::GeometryType::Point );
800
801 bool displayEndPoint = true;
802 switch ( mState )
803 {
804 case State::SelectFeature:
805 case State::BlankSegmentCreationStarted:
806 return;
807
808 case State::FeatureSelected:
809 displayEndPoint = false;
810 [[fallthrough]];
811
812 case State::BlankSegmentSelected:
813 case State::BlankSegmentModificationStarted:
814 break;
815 }
816
817 const QgsMapToPixel &m2p = *( canvas()->getCoordinateTransform() );
818
819 const QPointF &startPoint = mEditedBlankSegment->getStartPoint();
820 mStartRubberBand->addPoint( m2p.toMapCoordinates( startPoint.x(), startPoint.y() ) );
821
822 if ( displayEndPoint )
823 {
824 const QPointF &endPoint = mEditedBlankSegment->getEndPoint();
825 mEndRubberBand->addPoint( m2p.toMapCoordinates( endPoint.x(), endPoint.y() ) );
826 }
827}
828
829void QgsMapToolEditBlankSegmentsBase::updateHoveredBlankSegment( const QPoint &pos )
830{
831 double distance = -1;
832 int iBlankSegment = closestBlankSegmentIndex( pos, distance );
833
834 if ( mHoveredBlankSegmentIndex > -1 && mHoveredBlankSegmentIndex < static_cast<int>( mBlankSegments.size() ) && mHoveredBlankSegmentIndex != mCurrentBlankSegmentIndex )
835 {
836 mBlankSegments.at( mHoveredBlankSegmentIndex )->setHighlighted( false );
837 }
838
839 // blank segment is hovered
840 if ( iBlankSegment > -1 && distance < TOLERANCE )
841 {
842 mHoveredBlankSegmentIndex = iBlankSegment;
843 if ( mHoveredBlankSegmentIndex < static_cast<int>( mBlankSegments.size() ) )
844 {
845 mBlankSegments.at( mHoveredBlankSegmentIndex )->setHighlighted( true );
846 }
847 }
848 // no blank segment hovered, display the first point to create a new blank segment
849 else
850 {
851 mHoveredBlankSegmentIndex = -1;
852 }
853}
854
855void QgsMapToolEditBlankSegmentsBase::setCurrentBlankSegment( int currentBlankSegmentIndex )
856{
857 // copy current blank segment so we can edit it later (and hide the original one)
858
859 if ( mCurrentBlankSegmentIndex > -1 && mCurrentBlankSegmentIndex < static_cast<int>( mBlankSegments.size() ) )
860 {
861 mBlankSegments.at( mCurrentBlankSegmentIndex )->setVisible( true );
862 mBlankSegments.at( mCurrentBlankSegmentIndex )->setHighlighted( false );
863 }
864
865 mCurrentBlankSegmentIndex = currentBlankSegmentIndex;
866 if ( mCurrentBlankSegmentIndex > -1 && mCurrentBlankSegmentIndex < static_cast<int>( mBlankSegments.size() ) )
867 {
868 mBlankSegments.at( mCurrentBlankSegmentIndex )->setVisible( false );
869 mEditedBlankSegment->copyFrom( *( mBlankSegments.at( mCurrentBlankSegmentIndex ).get() ) );
870 }
871 else
872 {
873 mEditedBlankSegment->setVisible( false );
874 }
875
876 updateStartEndRubberBand();
877}
878
879void QgsMapToolEditBlankSegmentsBase::updateAttribute()
880{
881 if ( !mRenderedPointsSymbolLayer )
882 return;
883
884 QList<QList<QgsSymbolLayerUtils::BlankSegments>> blankSegments;
885 for ( const QObjectUniquePtr<QgsBlankSegmentRubberBand> &blankSegment : mBlankSegments )
886 {
887 try
888 {
889 QPair<double, double> startEndDistance = blankSegment->getStartEndDistance( mRenderedPointsSymbolLayer->blankSegmentsUnit() );
890
891 const int partIndex = blankSegment->getPartIndex();
892 if ( partIndex >= blankSegments.count() )
893 blankSegments.resize( partIndex + 1 );
894
895 QList<QgsSymbolLayerUtils::BlankSegments> &rings = blankSegments[partIndex];
896 const int ringIndex = blankSegment->getRingIndex();
897 if ( ringIndex >= rings.count() )
898 rings.resize( ringIndex + 1 );
899
900 QgsSymbolLayerUtils::BlankSegments &segments = rings[ringIndex];
901 segments << startEndDistance;
902 }
903 catch ( std::invalid_argument &e )
904 {
905 QgsDebugError( e.what() );
906 return;
907 }
908 }
909
910 QStringList strParts;
911 for ( QList<QgsSymbolLayerUtils::BlankSegments> &part : blankSegments )
912 {
913 QStringList strRings;
914 for ( QgsSymbolLayerUtils::BlankSegments &ring : part )
915 {
916 std::sort( ring.begin(), ring.end() );
917 QStringList strDistances;
918 for ( const QPair<double, double> &distance : ring )
919 {
920 strDistances << ( QString::number( distance.first ) + " " + QString::number( distance.second ) );
921 }
922
923 strRings << "(" + strDistances.join( "," ) + ")";
924 }
925
926 strParts << "(" + strRings.join( "," ) + ")";
927 }
928
929 QString strNewBlankSegments = "(" + strParts.join( "," ) + ")";
930
931 mLayer->beginEditCommand( tr( "Set blank segment list" ) );
932 if ( mLayer->changeAttributeValue( mCurrentFeatureId, mBlankSegmentsFieldIndex, strNewBlankSegments ) )
933 {
934 mLayer->endEditCommand();
935 mLayer->triggerRepaint();
936 }
937 else
938 {
939 QgsDebugError( "Fail to edit blank segments attribute" );
940 mLayer->destroyEditCommand();
941 }
942}
943
944void QgsMapToolEditBlankSegmentsBase::loadFeaturePoints()
945{
946 mPoints.clear();
947 mExtent = canvas()->extent();
948 mBlankSegments.clear();
949 setCurrentBlankSegment( -1 );
950
951 if ( FID_IS_NULL( mCurrentFeatureId ) || !mRenderedPointsSymbolLayer )
952 return;
953
954 QgsFeature feature;
955 feature = mLayer->getFeature( mCurrentFeatureId );
956 QString currentBlankSegments = feature.attribute( mBlankSegmentsFieldIndex ).toString();
957
958
959 // render feature to update mPoints
960 QgsRenderContext context = QgsRenderContext::fromMapSettings( canvas()->mapSettings() );
961 QgsCoordinateTransform transform( canvas()->mapSettings().layerTransform( mLayer ) );
962 QgsNullPaintDevice nullPaintDevice;
963 QPainter painter( &nullPaintDevice );
964 context.setPainter( &painter );
965 context.setCoordinateTransform( transform );
966 mSymbol->startRender( context );
967 mSymbol->renderFeature( feature, context );
968 mSymbol->stopRender( context );
969
970 if ( mPoints.isEmpty() )
971 return;
972
973 QString error;
974 QList<QList<QgsSymbolLayerUtils::BlankSegments>> allBlankSegments = QgsSymbolLayerUtils::parseBlankSegments( currentBlankSegments, context, mRenderedPointsSymbolLayer->blankSegmentsUnit(), error );
975 if ( !error.isEmpty() )
976 {
977 emit messageEmitted( tr( "Error while parsing feature blank segments: %1" ).arg( error ), Qgis::MessageLevel::Critical );
978 return;
979 }
980
981 // iterate through all points from parts and ring to get the closest one
982 for ( int iPart = 0; iPart < mPoints.count(); iPart++ )
983 {
984 const QList<QPolygonF> &rings = mPoints.at( iPart );
985 for ( int iRing = 0; iRing < rings.count(); iRing++ )
986 {
987 if ( iPart >= allBlankSegments.count() || iRing >= allBlankSegments.at( iPart ).count() )
988 continue;
989
990 const QgsSymbolLayerUtils::BlankSegments &blankSegments = allBlankSegments.at( iPart ).at( iRing );
991
992 double currentLength = 0;
993 int iPoint = 0;
994 const QPolygonF &points = rings.at( iRing );
995 for ( QPair<double, double> ba : blankSegments )
996 {
997 while ( iPoint < points.count() - 1 && currentLength < ba.first )
998 {
999 iPoint++;
1000 currentLength += QgsGeometryUtilsBase::distance2D( points.at( iPoint ), points.at( iPoint - 1 ) );
1001 }
1002
1003 if ( iPoint == points.count() )
1004 break;
1005
1006 int startIndex = iPoint;
1007 Line l( points.at( iPoint ), points.at( iPoint - 1 ) );
1008 QPointF startPt = points.at( iPoint ) + l.diffForInterval( currentLength - ba.first );
1009
1010 while ( iPoint < points.count() - 1 && currentLength < ba.second )
1011 {
1012 iPoint++;
1013 currentLength += QgsGeometryUtilsBase::distance2D( points.at( iPoint ), points.at( iPoint - 1 ) );
1014 }
1015
1016 if ( iPoint == points.count() )
1017 break;
1018
1019 int endIndex = iPoint;
1020 Line l2( points.at( iPoint ), points.at( iPoint - 1 ) );
1021 QPointF endPt = points.at( iPoint ) + l2.diffForInterval( currentLength - ba.second );
1022
1023 mBlankSegments.emplace_back( new QgsBlankSegmentRubberBand( iPart, iRing, startIndex, endIndex, startPt, endPt, canvas(), mPoints ) );
1024 }
1025 }
1026 }
1027}
1028
1036
1038 int partIndex, int ringIndex, int startIndex, int endIndex, QPointF startPt, QPointF endPt, QgsMapCanvas *canvas, const FeaturePoints &points
1039)
1041{
1042 setPoints( partIndex, ringIndex, startIndex, endIndex, startPt, endPt );
1043}
1044
1045void QgsMapToolEditBlankSegmentsBase::QgsBlankSegmentRubberBand::setPoints( int partIndex, int ringIndex, int startIndex, int endIndex, QPointF startPt, QPointF endPt )
1046{
1047 mPartIndex = partIndex;
1048 mRingIndex = ringIndex;
1049 mStartIndex = startIndex;
1050 mEndIndex = endIndex;
1051 mStartPt = startPt;
1052 mEndPt = endPt;
1053
1054 mNeedSwap = false;
1055 if ( mStartIndex == mEndIndex )
1056 {
1057 try
1058 {
1059 if ( const QPointF &startIndexPoint = ::pointAt( mPoints, mPartIndex, mRingIndex, mStartIndex );
1060 QgsGeometryUtilsBase::distance2D( startPt, startIndexPoint ) < QgsGeometryUtilsBase::distance2D( endPt, startIndexPoint ) )
1061 {
1062 mNeedSwap = true;
1063 }
1064 }
1065 catch ( std::invalid_argument &e )
1066 {
1067 QgsDebugError( e.what() );
1068 }
1069 }
1070 else if ( mStartIndex > mEndIndex )
1071 {
1072 mNeedSwap = true;
1073 }
1074
1075 updatePoints();
1076}
1077
1078void QgsMapToolEditBlankSegmentsBase::QgsBlankSegmentRubberBand::updatePoints()
1079{
1080 const QgsMapToPixel &m2p = *( mMapCanvas->getCoordinateTransform() );
1081
1082 reset();
1083 for ( int iPoint = 0; iPoint < pointsCount(); iPoint++ )
1084 {
1085 try
1086 {
1087 const QPointF &point = pointAt( iPoint );
1088 const QgsPointXY mapPoint = m2p.toMapCoordinates( point.x(), point.y() );
1089 addPoint( mapPoint, iPoint == pointsCount() - 1 ); // update only last one
1090 }
1091 catch ( std::invalid_argument &e )
1092 {
1093 QgsDebugError( e.what() );
1094 }
1095 }
1096}
1097
1098
1100{
1101 setPoints( blankSegment.mPartIndex, blankSegment.mRingIndex, blankSegment.getStartIndex(), blankSegment.getEndIndex(), blankSegment.getStartPoint(), blankSegment.getEndPoint() );
1102}
1103
1105{
1106 setWidth( QgsGuiUtils::scaleIconSize( highlighted ? 4 : 2 ) );
1107 update();
1108}
1109
1110
1112{
1113 return mStartPt;
1114}
1115
1117{
1118 return mEndPt;
1119}
1120
1125
1130
1135
1140
1142{
1143 return std::abs( mEndIndex - mStartIndex ) + 2;
1144}
1145
1147{
1148 return index == 0 ?
1149 // first point
1150 ( mNeedSwap ? mEndPt : mStartPt )
1151 // last point
1152 : ( index == pointsCount() - 1 ? ( mNeedSwap ? mStartPt : mEndPt )
1153 // point in between
1154 : ::pointAt( mPoints, mPartIndex, mRingIndex, ( mNeedSwap ? mEndIndex : mStartIndex ) + index - 1 ) );
1155}
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
@ Join
Field originates from a joined layer.
Definition qgis.h:1827
RenderUnit
Rendering size units.
Definition qgis.h:5552
static int createProperty(QgsPalLayerSettings::Property property, QgsVectorLayer *vlayer, bool overwriteExisting=true)
Creates if necessary a new auxiliary field for a PAL property and activates this property in settings...
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).
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() override
Destructor.
void canvasPressEvent(QgsMapMouseEvent *e) override
Mouse press event for overriding. Default implementation does nothing.
QList< QList< QPolygonF > > FeaturePoints
QgsMapToolEditBlankSegmentsBase(QgsMapCanvas *canvas, QgsVectorLayer *layer, QgsTemplatedLineSymbolLayerBase *symbolLayer, QgsPropertyOverrideButton *propertyButton)
Constructor.
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:403
friend class QgsMapCanvas
Definition qgsmaptool.h:423
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
int dataDefinedColumnIndex(int propertyKey, const QgsPropertyCollection &properties, const QgsVectorLayer *vlayer) const
Returns data defined property column index for the propertyKey from properties associated to the laye...
QFlags< Type > Types
@ Edge
Snapped to an edge.
Represents a 2D point.
Definition qgspointxy.h:62
A button for controlling property overrides which may apply to a widget.
void changed()
Emitted when property definition changes.
int propertyKey() const
Returns the property key linked to the button.
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:1462
@ SymbolEntity
Symbols.
Definition qgsstyle.h:207
Abstract base class for symbol layers.
@ BlankSegments
String list of distance to define blank segments along line for templated line 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,...
As part of the API refactoring and improvements which landed in the Processing API was substantially reworked from the x version This was done in order to allow much of the underlying Processing framework to be ported into c
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.