LCOV - code coverage report
Current view: top level - analysis/processing - qgsalgorithmjoinbynearest.cpp (source / functions) Hit Total Coverage
Test: coverage.info.cleaned Lines: 0 194 0.0 %
Date: 2021-04-10 08:29:14 Functions: 0 0 -
Branches: 0 0 -

           Branch data     Line data    Source code
       1                 :            : /***************************************************************************
       2                 :            :                          qgsalgorithmjoinbynearest.cpp
       3                 :            :                          ---------------------
       4                 :            :     begin                : April 2017
       5                 :            :     copyright            : (C) 2017 by Nyall Dawson
       6                 :            :     email                : nyall dot dawson at gmail dot com
       7                 :            :  ***************************************************************************/
       8                 :            : 
       9                 :            : /***************************************************************************
      10                 :            :  *                                                                         *
      11                 :            :  *   This program is free software; you can redistribute it and/or modify  *
      12                 :            :  *   it under the terms of the GNU General Public License as published by  *
      13                 :            :  *   the Free Software Foundation; either version 2 of the License, or     *
      14                 :            :  *   (at your option) any later version.                                   *
      15                 :            :  *                                                                         *
      16                 :            :  ***************************************************************************/
      17                 :            : 
      18                 :            : #include "qgsalgorithmjoinbynearest.h"
      19                 :            : #include "qgsprocessingoutputs.h"
      20                 :            : #include "qgslinestring.h"
      21                 :            : 
      22                 :            : #include <algorithm>
      23                 :            : 
      24                 :            : ///@cond PRIVATE
      25                 :            : 
      26                 :          0 : QString QgsJoinByNearestAlgorithm::name() const
      27                 :            : {
      28                 :          0 :   return QStringLiteral( "joinbynearest" );
      29                 :            : }
      30                 :            : 
      31                 :          0 : QString QgsJoinByNearestAlgorithm::displayName() const
      32                 :            : {
      33                 :          0 :   return QObject::tr( "Join attributes by nearest" );
      34                 :            : }
      35                 :            : 
      36                 :          0 : QStringList QgsJoinByNearestAlgorithm::tags() const
      37                 :            : {
      38                 :          0 :   return QObject::tr( "join,connect,attributes,values,fields,tables,proximity,closest,neighbour,neighbor,n-nearest,distance" ).split( ',' );
      39                 :          0 : }
      40                 :            : 
      41                 :          0 : QString QgsJoinByNearestAlgorithm::group() const
      42                 :            : {
      43                 :          0 :   return QObject::tr( "Vector general" );
      44                 :            : }
      45                 :            : 
      46                 :          0 : QString QgsJoinByNearestAlgorithm::groupId() const
      47                 :            : {
      48                 :          0 :   return QStringLiteral( "vectorgeneral" );
      49                 :            : }
      50                 :            : 
      51                 :          0 : void QgsJoinByNearestAlgorithm::initAlgorithm( const QVariantMap & )
      52                 :            : {
      53                 :          0 :   addParameter( new QgsProcessingParameterFeatureSource( QStringLiteral( "INPUT" ),
      54                 :          0 :                 QObject::tr( "Input layer" ) ) );
      55                 :          0 :   addParameter( new QgsProcessingParameterFeatureSource( QStringLiteral( "INPUT_2" ),
      56                 :          0 :                 QObject::tr( "Input layer 2" ) ) );
      57                 :            : 
      58                 :          0 :   addParameter( new QgsProcessingParameterField( QStringLiteral( "FIELDS_TO_COPY" ),
      59                 :          0 :                 QObject::tr( "Layer 2 fields to copy (leave empty to copy all fields)" ),
      60                 :          0 :                 QVariant(), QStringLiteral( "INPUT_2" ), QgsProcessingParameterField::Any,
      61                 :            :                 true, true ) );
      62                 :            : 
      63                 :          0 :   addParameter( new QgsProcessingParameterBoolean( QStringLiteral( "DISCARD_NONMATCHING" ),
      64                 :          0 :                 QObject::tr( "Discard records which could not be joined" ),
      65                 :          0 :                 false ) );
      66                 :            : 
      67                 :          0 :   addParameter( new QgsProcessingParameterString( QStringLiteral( "PREFIX" ),
      68                 :          0 :                 QObject::tr( "Joined field prefix" ), QVariant(), false, true ) );
      69                 :            : 
      70                 :          0 :   addParameter( new QgsProcessingParameterNumber( QStringLiteral( "NEIGHBORS" ),
      71                 :          0 :                 QObject::tr( "Maximum nearest neighbors" ), QgsProcessingParameterNumber::Integer, 1, false, 1 ) );
      72                 :            : 
      73                 :          0 :   addParameter( new QgsProcessingParameterDistance( QStringLiteral( "MAX_DISTANCE" ),
      74                 :          0 :                 QObject::tr( "Maximum distance" ), QVariant(), QStringLiteral( "INPUT" ), true, 0 ) );
      75                 :            : 
      76                 :          0 :   addParameter( new QgsProcessingParameterFeatureSink( QStringLiteral( "OUTPUT" ), QObject::tr( "Joined layer" ), QgsProcessing::TypeVectorAnyGeometry, QVariant(), true, true ) );
      77                 :            : 
      78                 :          0 :   std::unique_ptr< QgsProcessingParameterFeatureSink > nonMatchingSink = std::make_unique< QgsProcessingParameterFeatureSink >(
      79                 :          0 :         QStringLiteral( "NON_MATCHING" ), QObject::tr( "Unjoinable features from first layer" ), QgsProcessing::TypeVectorAnyGeometry, QVariant(), true, false );
      80                 :            :   // TODO GUI doesn't support advanced outputs yet
      81                 :            :   //nonMatchingSink->setFlags(nonMatchingSink->flags() | QgsProcessingParameterDefinition::FlagAdvanced );
      82                 :          0 :   addParameter( nonMatchingSink.release() );
      83                 :            : 
      84                 :          0 :   addOutput( new QgsProcessingOutputNumber( QStringLiteral( "JOINED_COUNT" ), QObject::tr( "Number of joined features from input table" ) ) );
      85                 :          0 :   addOutput( new QgsProcessingOutputNumber( QStringLiteral( "UNJOINABLE_COUNT" ), QObject::tr( "Number of unjoinable features from input table" ) ) );
      86                 :          0 : }
      87                 :            : 
      88                 :          0 : QString QgsJoinByNearestAlgorithm::shortHelpString() const
      89                 :            : {
      90                 :          0 :   return QObject::tr( "This algorithm takes an input vector layer and creates a new vector layer that is an extended version of the "
      91                 :            :                       "input one, with additional attributes in its attribute table.\n\n"
      92                 :            :                       "The additional attributes and their values are taken from a second vector layer, where features are joined "
      93                 :            :                       "by finding the closest features from each layer. By default only the single nearest feature is joined,"
      94                 :            :                       "but optionally the join can use the n-nearest neighboring features instead. If multiple features are found "
      95                 :            :                       "with identical distances these will all be returned (even if the total number of features exceeds the specified "
      96                 :            :                       "maximum feature count).\n\n"
      97                 :            :                       "If a maximum distance is specified, then only features which are closer than this distance "
      98                 :            :                       "will be matched.\n\n"
      99                 :            :                       "The output features will contain the selected attributes from the nearest feature, "
     100                 :            :                       "along with new attributes for the distance to the near feature, the index of the feature, "
     101                 :            :                       "and the coordinates of the closest point on the input feature (feature_x, feature_y) "
     102                 :            :                       "to the matched nearest feature, and the coordinates of the closet point on the matched feature "
     103                 :            :                       "(nearest_x, nearest_y).\n\n"
     104                 :            :                       "This algorithm uses purely Cartesian calculations for distance, and does not consider "
     105                 :            :                       "geodetic or ellipsoid properties when determining feature proximity." );
     106                 :            : }
     107                 :            : 
     108                 :          0 : QString QgsJoinByNearestAlgorithm::shortDescription() const
     109                 :            : {
     110                 :          0 :   return QObject::tr( "Joins a layer to another layer, using the closest features (nearest neighbors)." );
     111                 :            : }
     112                 :            : 
     113                 :          0 : QgsJoinByNearestAlgorithm *QgsJoinByNearestAlgorithm::createInstance() const
     114                 :            : {
     115                 :          0 :   return new QgsJoinByNearestAlgorithm();
     116                 :            : }
     117                 :            : 
     118                 :          0 : QVariantMap QgsJoinByNearestAlgorithm::processAlgorithm( const QVariantMap &parameters, QgsProcessingContext &context, QgsProcessingFeedback *feedback )
     119                 :            : {
     120                 :          0 :   const int neighbors = parameterAsInt( parameters, QStringLiteral( "NEIGHBORS" ), context );
     121                 :          0 :   const bool discardNonMatching = parameterAsBoolean( parameters, QStringLiteral( "DISCARD_NONMATCHING" ), context );
     122                 :          0 :   const double maxDistance = parameters.value( QStringLiteral( "MAX_DISTANCE" ) ).isValid() ? parameterAsDouble( parameters, QStringLiteral( "MAX_DISTANCE" ), context ) : std::numeric_limits< double >::quiet_NaN();
     123                 :          0 :   std::unique_ptr< QgsProcessingFeatureSource > input( parameterAsSource( parameters, QStringLiteral( "INPUT" ), context ) );
     124                 :          0 :   if ( !input )
     125                 :          0 :     throw QgsProcessingException( invalidSourceError( parameters, QStringLiteral( "INPUT" ) ) );
     126                 :            : 
     127                 :          0 :   std::unique_ptr< QgsProcessingFeatureSource > input2( parameterAsSource( parameters, QStringLiteral( "INPUT_2" ), context ) );
     128                 :          0 :   if ( !input2 )
     129                 :          0 :     throw QgsProcessingException( invalidSourceError( parameters, QStringLiteral( "INPUT_2" ) ) );
     130                 :            : 
     131                 :          0 :   const bool sameSourceAndTarget = parameters.value( QStringLiteral( "INPUT" ) ) == parameters.value( QStringLiteral( "INPUT_2" ) );
     132                 :            : 
     133                 :          0 :   QString prefix = parameterAsString( parameters, QStringLiteral( "PREFIX" ), context );
     134                 :          0 :   const QStringList fieldsToCopy = parameterAsFields( parameters, QStringLiteral( "FIELDS_TO_COPY" ), context );
     135                 :            : 
     136                 :          0 :   QgsFields outFields2;
     137                 :          0 :   QgsAttributeList fields2Indices;
     138                 :          0 :   if ( fieldsToCopy.empty() )
     139                 :            :   {
     140                 :          0 :     outFields2 = input2->fields();
     141                 :          0 :     fields2Indices.reserve( outFields2.count() );
     142                 :          0 :     for ( int i = 0; i < outFields2.count(); ++i )
     143                 :            :     {
     144                 :          0 :       fields2Indices << i;
     145                 :          0 :     }
     146                 :          0 :   }
     147                 :            :   else
     148                 :            :   {
     149                 :          0 :     fields2Indices.reserve( fieldsToCopy.count() );
     150                 :          0 :     for ( const QString &field : fieldsToCopy )
     151                 :            :     {
     152                 :          0 :       int index = input2->fields().lookupField( field );
     153                 :          0 :       if ( index >= 0 )
     154                 :            :       {
     155                 :          0 :         fields2Indices << index;
     156                 :          0 :         outFields2.append( input2->fields().at( index ) );
     157                 :          0 :       }
     158                 :            :     }
     159                 :            :   }
     160                 :            : 
     161                 :          0 :   if ( !prefix.isEmpty() )
     162                 :            :   {
     163                 :          0 :     for ( int i = 0; i < outFields2.count(); ++i )
     164                 :            :     {
     165                 :          0 :       outFields2.rename( i, prefix + outFields2[ i ].name() );
     166                 :          0 :     }
     167                 :          0 :   }
     168                 :            : 
     169                 :          0 :   QgsAttributeList fields2Fetch = fields2Indices;
     170                 :            : 
     171                 :          0 :   QgsFields outFields = QgsProcessingUtils::combineFields( input->fields(), outFields2 );
     172                 :          0 :   outFields.append( QgsField( QStringLiteral( "n" ), QVariant::Int ) );
     173                 :          0 :   outFields.append( QgsField( QStringLiteral( "distance" ), QVariant::Double ) );
     174                 :          0 :   outFields.append( QgsField( QStringLiteral( "feature_x" ), QVariant::Double ) );
     175                 :          0 :   outFields.append( QgsField( QStringLiteral( "feature_y" ), QVariant::Double ) );
     176                 :          0 :   outFields.append( QgsField( QStringLiteral( "nearest_x" ), QVariant::Double ) );
     177                 :          0 :   outFields.append( QgsField( QStringLiteral( "nearest_y" ), QVariant::Double ) );
     178                 :            : 
     179                 :          0 :   QString dest;
     180                 :          0 :   std::unique_ptr< QgsFeatureSink > sink( parameterAsSink( parameters, QStringLiteral( "OUTPUT" ), context, dest, outFields,
     181                 :          0 :                                           input->wkbType(), input->sourceCrs(), QgsFeatureSink::RegeneratePrimaryKey ) );
     182                 :          0 :   if ( parameters.value( QStringLiteral( "OUTPUT" ) ).isValid() && !sink )
     183                 :          0 :     throw QgsProcessingException( invalidSinkError( parameters, QStringLiteral( "OUTPUT" ) ) );
     184                 :            : 
     185                 :          0 :   QString destNonMatching1;
     186                 :          0 :   std::unique_ptr< QgsFeatureSink > sinkNonMatching1( parameterAsSink( parameters, QStringLiteral( "NON_MATCHING" ), context, destNonMatching1, input->fields(),
     187                 :          0 :       input->wkbType(), input->sourceCrs(), QgsFeatureSink::RegeneratePrimaryKey ) );
     188                 :          0 :   if ( parameters.value( QStringLiteral( "NON_MATCHING" ) ).isValid() && !sinkNonMatching1 )
     189                 :          0 :     throw QgsProcessingException( invalidSinkError( parameters, QStringLiteral( "NON_MATCHING" ) ) );
     190                 :            : 
     191                 :            :   // make spatial index
     192                 :          0 :   QgsFeatureIterator f2 = input2->getFeatures( QgsFeatureRequest().setDestinationCrs( input->sourceCrs(), context.transformContext() ).setSubsetOfAttributes( fields2Fetch ) );
     193                 :          0 :   QHash< QgsFeatureId, QgsAttributes > input2AttributeCache;
     194                 :          0 :   double step = input2->featureCount() > 0 ? 50.0 / input2->featureCount() : 1;
     195                 :          0 :   int i = 0;
     196                 :          0 :   QgsSpatialIndex index( f2, [&]( const QgsFeature & f )->bool
     197                 :            :   {
     198                 :          0 :     i++;
     199                 :          0 :     if ( feedback->isCanceled() )
     200                 :          0 :       return false;
     201                 :            : 
     202                 :          0 :     feedback->setProgress( i * step );
     203                 :            : 
     204                 :          0 :     if ( !f.hasGeometry() )
     205                 :          0 :       return true;
     206                 :            : 
     207                 :            :     // only keep selected attributes
     208                 :          0 :     QgsAttributes attributes;
     209                 :          0 :     for ( int j = 0; j < f.attributes().count(); ++j )
     210                 :            :     {
     211                 :          0 :       if ( ! fields2Indices.contains( j ) )
     212                 :          0 :         continue;
     213                 :          0 :       attributes << f.attribute( j );
     214                 :          0 :     }
     215                 :          0 :     input2AttributeCache.insert( f.id(), attributes );
     216                 :            : 
     217                 :          0 :     return true;
     218                 :          0 :   }, QgsSpatialIndex::FlagStoreFeatureGeometries );
     219                 :            : 
     220                 :          0 :   QgsFeature f;
     221                 :            : 
     222                 :            :   // create extra null attributes for non-matched records (the +2 is for the "n" and "distance", and start/end x/y fields)
     223                 :          0 :   QgsAttributes nullMatch;
     224                 :          0 :   nullMatch.reserve( fields2Indices.size() + 6 );
     225                 :          0 :   for ( int i = 0; i < fields2Indices.count() + 6; ++i )
     226                 :          0 :     nullMatch << QVariant();
     227                 :            : 
     228                 :          0 :   long long joinedCount = 0;
     229                 :          0 :   long long unjoinedCount = 0;
     230                 :            : 
     231                 :            :   // Create output vector layer with additional attributes
     232                 :          0 :   step = input->featureCount() > 0 ? 50.0 / input->featureCount() : 1;
     233                 :          0 :   QgsFeatureIterator features = input->getFeatures();
     234                 :          0 :   i = 0;
     235                 :          0 :   while ( features.nextFeature( f ) )
     236                 :            :   {
     237                 :          0 :     i++;
     238                 :          0 :     if ( feedback->isCanceled() )
     239                 :            :     {
     240                 :          0 :       break;
     241                 :            :     }
     242                 :            : 
     243                 :          0 :     feedback->setProgress( 50 + i * step );
     244                 :            : 
     245                 :          0 :     if ( !f.hasGeometry() )
     246                 :            :     {
     247                 :          0 :       unjoinedCount++;
     248                 :          0 :       if ( sinkNonMatching1 )
     249                 :            :       {
     250                 :          0 :         sinkNonMatching1->addFeature( f, QgsFeatureSink::FastInsert );
     251                 :          0 :       }
     252                 :          0 :       if ( sink && !discardNonMatching )
     253                 :            :       {
     254                 :          0 :         QgsAttributes attr = f.attributes();
     255                 :          0 :         attr.append( nullMatch );
     256                 :          0 :         f.setAttributes( attr );
     257                 :          0 :         sink->addFeature( f, QgsFeatureSink::FastInsert );
     258                 :          0 :       }
     259                 :          0 :     }
     260                 :            :     else
     261                 :            :     {
     262                 :            :       // note - if using same source as target, we have to get one extra neighbor, since the first match will be the input feature
     263                 :            : 
     264                 :            :       // if the user didn't specify a distance (isnan), then use 0 for nearestNeighbor() parameter
     265                 :            :       // if the user specified 0 exactly, then use the smallest positive double value instead
     266                 :          0 :       const double searchDistance = std::isnan( maxDistance ) ? 0 : std::max( std::numeric_limits<double>::min(), maxDistance );
     267                 :          0 :       const QList< QgsFeatureId > nearest = index.nearestNeighbor( f.geometry(), neighbors + ( sameSourceAndTarget ? 1 : 0 ), searchDistance );
     268                 :            : 
     269                 :          0 :       if ( nearest.count() > neighbors + ( sameSourceAndTarget ? 1 : 0 ) )
     270                 :            :       {
     271                 :          0 :         feedback->pushInfo( QObject::tr( "Multiple matching features found at same distance from search feature, found %1 features instead of %2" ).arg( nearest.count() - ( sameSourceAndTarget ? 1 : 0 ) ).arg( neighbors ) );
     272                 :          0 :       }
     273                 :          0 :       QgsFeature out;
     274                 :          0 :       out.setGeometry( f.geometry() );
     275                 :          0 :       int j = 0;
     276                 :          0 :       for ( QgsFeatureId id : nearest )
     277                 :            :       {
     278                 :          0 :         if ( sameSourceAndTarget && id == f.id() )
     279                 :          0 :           continue; // don't match to same feature if using a single input table
     280                 :          0 :         j++;
     281                 :          0 :         if ( sink )
     282                 :            :         {
     283                 :          0 :           QgsAttributes attr = f.attributes();
     284                 :          0 :           attr.append( input2AttributeCache.value( id ) );
     285                 :          0 :           attr.append( j );
     286                 :            : 
     287                 :          0 :           const QgsGeometry closestLine = f.geometry().shortestLine( index.geometry( id ) );
     288                 :          0 :           if ( const QgsLineString *line = qgsgeometry_cast< const QgsLineString *>( closestLine.constGet() ) )
     289                 :            :           {
     290                 :          0 :             attr.append( line->length() );
     291                 :          0 :             attr.append( line->startPoint().x() );
     292                 :          0 :             attr.append( line->startPoint().y() );
     293                 :          0 :             attr.append( line->endPoint().x() );
     294                 :          0 :             attr.append( line->endPoint().y() );
     295                 :          0 :           }
     296                 :            :           else
     297                 :            :           {
     298                 :          0 :             attr.append( QVariant() ); //distance
     299                 :          0 :             attr.append( QVariant() ); //start x
     300                 :          0 :             attr.append( QVariant() ); //start y
     301                 :          0 :             attr.append( QVariant() ); //end x
     302                 :          0 :             attr.append( QVariant() ); //end y
     303                 :            :           }
     304                 :          0 :           out.setAttributes( attr );
     305                 :          0 :           sink->addFeature( out, QgsFeatureSink::FastInsert );
     306                 :          0 :         }
     307                 :            :       }
     308                 :          0 :       if ( j > 0 )
     309                 :          0 :         joinedCount++;
     310                 :            :       else
     311                 :            :       {
     312                 :          0 :         if ( sinkNonMatching1 )
     313                 :            :         {
     314                 :          0 :           sinkNonMatching1->addFeature( f, QgsFeatureSink::FastInsert );
     315                 :          0 :         }
     316                 :          0 :         if ( !discardNonMatching && sink )
     317                 :            :         {
     318                 :          0 :           QgsAttributes attr = f.attributes();
     319                 :          0 :           attr.append( nullMatch );
     320                 :          0 :           f.setAttributes( attr );
     321                 :          0 :           sink->addFeature( f, QgsFeatureSink::FastInsert );
     322                 :          0 :         }
     323                 :          0 :         unjoinedCount++;
     324                 :            :       }
     325                 :          0 :     }
     326                 :            :   }
     327                 :            : 
     328                 :          0 :   QVariantMap outputs;
     329                 :          0 :   outputs.insert( QStringLiteral( "JOINED_COUNT" ), joinedCount );
     330                 :          0 :   outputs.insert( QStringLiteral( "UNJOINABLE_COUNT" ), unjoinedCount );
     331                 :          0 :   if ( sink )
     332                 :          0 :     outputs.insert( QStringLiteral( "OUTPUT" ), dest );
     333                 :          0 :   if ( sinkNonMatching1 )
     334                 :          0 :     outputs.insert( QStringLiteral( "NON_MATCHING" ), destNonMatching1 );
     335                 :          0 :   return outputs;
     336                 :          0 : }
     337                 :            : 
     338                 :            : 
     339                 :            : ///@endcond

Generated by: LCOV version 1.14