void QgsMapToolIdentify::closestVertexAttributes( const QgsAbstractGeometry& geometry, QgsVertexId vId, QgsMapLayer *layer, QMap< QString, QString >& derivedAttributes ) { QString str = QLocale::system().toString( vId.vertex + 1 ); derivedAttributes.insert( tr( "Closest vertex number" ), str ); QgsPointV2 closestPoint = geometry.vertexAt( vId ); QgsPoint closestPointMapCoords = mCanvas->mapSettings().layerToMapCoordinates( layer, QgsPoint( closestPoint.x(), closestPoint.y() ) ); derivedAttributes.insert( "Closest vertex X", formatXCoordinate( closestPointMapCoords ) ); derivedAttributes.insert( "Closest vertex Y", formatYCoordinate( closestPointMapCoords ) ); if ( closestPoint.is3D() ) { str = QLocale::system().toString( closestPoint.z(), 'g', 10 ); derivedAttributes.insert( "Closest vertex Z", str ); } if ( closestPoint.isMeasure() ) { str = QLocale::system().toString( closestPoint.m(), 'g', 10 ); derivedAttributes.insert( "Closest vertex M", str ); } if ( vId.type == QgsVertexId::CurveVertex ) { double radius, centerX, centerY; QgsVertexId vIdBefore = vId; --vIdBefore.vertex; QgsVertexId vIdAfter = vId; ++vIdAfter.vertex; QgsGeometryUtils::circleCenterRadius( geometry.vertexAt( vIdBefore ), geometry.vertexAt( vId ), geometry.vertexAt( vIdAfter ), radius, centerX, centerY ); derivedAttributes.insert( "Closest vertex radius", QLocale::system().toString( radius ) ); } }
bool QgsMapToolIdentify::identifyRasterLayer( QList<IdentifyResult> *results, QgsRasterLayer *layer, QgsPoint point, const QgsRectangle& viewExtent, double mapUnitsPerPixel ) { QgsDebugMsg( "point = " + point.toString() ); if ( !layer ) return false; QgsRasterDataProvider *dprovider = layer->dataProvider(); if ( !dprovider ) return false; int capabilities = dprovider->capabilities(); if ( !( capabilities & QgsRasterDataProvider::Identify ) ) return false; QgsPoint pointInCanvasCrs = point; try { point = toLayerCoordinates( layer, point ); } catch ( QgsCsException &cse ) { Q_UNUSED( cse ); QgsDebugMsg( QString( "coordinate not reprojectable: %1" ).arg( cse.what() ) ); return false; } QgsDebugMsg( QString( "point = %1 %2" ).arg( point.x() ).arg( point.y() ) ); if ( !layer->extent().contains( point ) ) return false; QMap< QString, QString > attributes, derivedAttributes; QgsRaster::IdentifyFormat format = QgsRasterDataProvider::identifyFormatFromName( layer->customProperty( "identify/format" ).toString() ); // check if the format is really supported otherwise use first supported format if ( !( QgsRasterDataProvider::identifyFormatToCapability( format ) & capabilities ) ) { if ( capabilities & QgsRasterInterface::IdentifyFeature ) format = QgsRaster::IdentifyFormatFeature; else if ( capabilities & QgsRasterInterface::IdentifyValue ) format = QgsRaster::IdentifyFormatValue; else if ( capabilities & QgsRasterInterface::IdentifyHtml ) format = QgsRaster::IdentifyFormatHtml; else if ( capabilities & QgsRasterInterface::IdentifyText ) format = QgsRaster::IdentifyFormatText; else return false; } QgsRasterIdentifyResult identifyResult; // We can only use current map canvas context (extent, width, height) if layer is not reprojected, if ( mCanvas->hasCrsTransformEnabled() && dprovider->crs() != mCanvas->mapSettings().destinationCrs() ) { // To get some reasonable response for point/line WMS vector layers we must // use a context with approximately a resolution in layer CRS units // corresponding to current map canvas resolution (for examplei UMN Mapserver // in msWMSFeatureInfo() -> msQueryByRect() is using requested pixel // + TOLERANCE (layer param) for feature selection) // QgsRectangle r; r.setXMinimum( pointInCanvasCrs.x() - mapUnitsPerPixel / 2. ); r.setXMaximum( pointInCanvasCrs.x() + mapUnitsPerPixel / 2. ); r.setYMinimum( pointInCanvasCrs.y() - mapUnitsPerPixel / 2. ); r.setYMaximum( pointInCanvasCrs.y() + mapUnitsPerPixel / 2. ); r = toLayerCoordinates( layer, r ); // will be a bit larger // Mapserver (6.0.3, for example) does not work with 1x1 pixel box // but that is fixed (the rect is enlarged) in the WMS provider identifyResult = dprovider->identify( point, format, r, 1, 1 ); } else { // It would be nice to use the same extent and size which was used for drawing, // so that WCS can use cache from last draw, unfortunately QgsRasterLayer::draw() // is doing some tricks with extent and size to allign raster to output which // would be difficult to replicate here. // Note: cutting the extent may result in slightly different x and y resolutions // and thus shifted point calculated back in QGIS WMS (using average resolution) //viewExtent = dprovider->extent().intersect( &viewExtent ); // Width and height are calculated from not projected extent and we hope that // are similar to source width and height used to reproject layer for drawing. // TODO: may be very dangerous, because it may result in different resolutions // in source CRS, and WMS server (QGIS server) calcs wrong coor using average resolution. int width = qRound( viewExtent.width() / mapUnitsPerPixel ); int height = qRound( viewExtent.height() / mapUnitsPerPixel ); QgsDebugMsg( QString( "viewExtent.width = %1 viewExtent.height = %2" ).arg( viewExtent.width() ).arg( viewExtent.height() ) ); QgsDebugMsg( QString( "width = %1 height = %2" ).arg( width ).arg( height ) ); QgsDebugMsg( QString( "xRes = %1 yRes = %2 mapUnitsPerPixel = %3" ).arg( viewExtent.width() / width ).arg( viewExtent.height() / height ).arg( mapUnitsPerPixel ) ); identifyResult = dprovider->identify( point, format, viewExtent, width, height ); } derivedAttributes.insert( tr( "(clicked coordinate X)" ), formatXCoordinate( pointInCanvasCrs ) ); derivedAttributes.insert( tr( "(clicked coordinate Y)" ), formatYCoordinate( pointInCanvasCrs ) ); if ( identifyResult.isValid() ) { QMap<int, QVariant> values = identifyResult.results(); QgsGeometry geometry; if ( format == QgsRaster::IdentifyFormatValue ) { Q_FOREACH ( int bandNo, values.keys() ) { QString valueString; if ( values.value( bandNo ).isNull() ) { valueString = tr( "no data" ); } else { QVariant value( values.value( bandNo ) ); // The cast is legit. Quoting QT doc : // "Although this function is declared as returning QVariant::Type, // the return value should be interpreted as QMetaType::Type" if ( static_cast<QMetaType::Type>( value.type() ) == QMetaType::Float ) { valueString = QgsRasterBlock::printValue( value.toFloat() ); } else { valueString = QgsRasterBlock::printValue( value.toDouble() ); } } attributes.insert( dprovider->generateBandName( bandNo ), valueString ); } QString label = layer->name(); results->append( IdentifyResult( qobject_cast<QgsMapLayer *>( layer ), label, attributes, derivedAttributes ) ); }
QMap< QString, QString > QgsMapToolIdentify::featureDerivedAttributes( QgsFeature *feature, QgsMapLayer *layer, const QgsPoint& layerPoint ) { // Calculate derived attributes and insert: // measure distance or area depending on geometry type QMap< QString, QString > derivedAttributes; // init distance/area calculator QString ellipsoid = QgsProject::instance()->ellipsoid(); QgsDistanceArea calc; calc.setEllipsoidalMode( mCanvas->hasCrsTransformEnabled() ); calc.setEllipsoid( ellipsoid ); calc.setSourceCrs( layer->crs().srsid() ); QgsWkbTypes::Type wkbType = QgsWkbTypes::NoGeometry; QgsWkbTypes::GeometryType geometryType = QgsWkbTypes::NullGeometry; QgsVertexId vId; QgsPointV2 closestPoint; if ( feature->hasGeometry() ) { geometryType = feature->geometry().type(); wkbType = feature->geometry().geometry()->wkbType(); //find closest vertex to clicked point closestPoint = QgsGeometryUtils::closestVertex( *feature->geometry().geometry(), QgsPointV2( layerPoint.x(), layerPoint.y() ), vId ); } if ( QgsWkbTypes::isMultiType( wkbType ) ) { QString str = QLocale::system().toString( static_cast<const QgsGeometryCollection*>( feature->geometry().geometry() )->numGeometries() ); derivedAttributes.insert( tr( "Parts" ), str ); str = QLocale::system().toString( vId.part + 1 ); derivedAttributes.insert( tr( "Part number" ), str ); } if ( geometryType == QgsWkbTypes::LineGeometry ) { double dist = calc.measureLength( feature->geometry() ); dist = calc.convertLengthMeasurement( dist, displayDistanceUnits() ); QString str = formatDistance( dist ); derivedAttributes.insert( tr( "Length" ), str ); const QgsCurve* curve = dynamic_cast< const QgsCurve* >( feature->geometry().geometry() ); if ( curve ) { str = QLocale::system().toString( curve->nCoordinates() ); derivedAttributes.insert( tr( "Vertices" ), str ); //add details of closest vertex to identify point closestVertexAttributes( *curve, vId, layer, derivedAttributes ); // Add the start and end points in as derived attributes QgsPoint pnt = mCanvas->mapSettings().layerToMapCoordinates( layer, QgsPoint( curve->startPoint().x(), curve->startPoint().y() ) ); str = formatXCoordinate( pnt ); derivedAttributes.insert( tr( "firstX", "attributes get sorted; translation for lastX should be lexically larger than this one" ), str ); str = formatYCoordinate( pnt ); derivedAttributes.insert( tr( "firstY" ), str ); pnt = mCanvas->mapSettings().layerToMapCoordinates( layer, QgsPoint( curve->endPoint().x(), curve->endPoint().y() ) ); str = formatXCoordinate( pnt ); derivedAttributes.insert( tr( "lastX", "attributes get sorted; translation for firstX should be lexically smaller than this one" ), str ); str = formatYCoordinate( pnt ); derivedAttributes.insert( tr( "lastY" ), str ); } } else if ( geometryType == QgsWkbTypes::PolygonGeometry ) { double area = calc.measureArea( feature->geometry() ); area = calc.convertAreaMeasurement( area, displayAreaUnits() ); QString str = formatArea( area ); derivedAttributes.insert( tr( "Area" ), str ); double perimeter = calc.measurePerimeter( feature->geometry() ); perimeter = calc.convertLengthMeasurement( perimeter, displayDistanceUnits() ); str = formatDistance( perimeter ); derivedAttributes.insert( tr( "Perimeter" ), str ); str = QLocale::system().toString( feature->geometry().geometry()->nCoordinates() ); derivedAttributes.insert( tr( "Vertices" ), str ); //add details of closest vertex to identify point closestVertexAttributes( *feature->geometry().geometry(), vId, layer, derivedAttributes ); } else if ( geometryType == QgsWkbTypes::PointGeometry && QgsWkbTypes::flatType( wkbType ) == QgsWkbTypes::Point ) { // Include the x and y coordinates of the point as a derived attribute QgsPoint pnt = mCanvas->mapSettings().layerToMapCoordinates( layer, feature->geometry().asPoint() ); QString str = formatXCoordinate( pnt ); derivedAttributes.insert( "X", str ); str = formatYCoordinate( pnt ); derivedAttributes.insert( "Y", str ); if ( QgsWkbTypes::hasZ( wkbType ) ) { str = QLocale::system().toString( static_cast<const QgsPointV2*>( feature->geometry().geometry() )->z(), 'g', 10 ); derivedAttributes.insert( "Z", str ); } if ( QgsWkbTypes::hasM( wkbType ) ) { str = QLocale::system().toString( static_cast<const QgsPointV2*>( feature->geometry().geometry() )->m(), 'g', 10 ); derivedAttributes.insert( "M", str ); } } return derivedAttributes; }
bool QgsMapToolIdentify::identifyVectorLayer( QList<IdentifyResult> *results, QgsVectorLayer *layer, const QgsPoint& point ) { if ( !layer || !layer->hasGeometryType() ) return false; if ( !layer->isInScaleRange( mCanvas->mapSettings().scale() ) ) { QgsDebugMsg( "Out of scale limits" ); return false; } QApplication::setOverrideCursor( Qt::WaitCursor ); QMap< QString, QString > commonDerivedAttributes; commonDerivedAttributes.insert( tr( "(clicked coordinate X)" ), formatXCoordinate( point ) ); commonDerivedAttributes.insert( tr( "(clicked coordinate Y)" ), formatYCoordinate( point ) ); int featureCount = 0; QgsFeatureList featureList; // toLayerCoordinates will throw an exception for an 'invalid' point. // For example, if you project a world map onto a globe using EPSG 2163 // and then click somewhere off the globe, an exception will be thrown. try { // create the search rectangle double searchRadius = searchRadiusMU( mCanvas ); QgsRectangle r; r.setXMinimum( point.x() - searchRadius ); r.setXMaximum( point.x() + searchRadius ); r.setYMinimum( point.y() - searchRadius ); r.setYMaximum( point.y() + searchRadius ); r = toLayerCoordinates( layer, r ); QgsFeatureIterator fit = layer->getFeatures( QgsFeatureRequest().setFilterRect( r ).setFlags( QgsFeatureRequest::ExactIntersect ) ); QgsFeature f; while ( fit.nextFeature( f ) ) featureList << QgsFeature( f ); } catch ( QgsCsException & cse ) { Q_UNUSED( cse ); // catch exception for 'invalid' point and proceed with no features found QgsDebugMsg( QString( "Caught CRS exception %1" ).arg( cse.what() ) ); } QgsFeatureList::iterator f_it = featureList.begin(); bool filter = false; QgsRenderContext context( QgsRenderContext::fromMapSettings( mCanvas->mapSettings() ) ); context.expressionContext() << QgsExpressionContextUtils::layerScope( layer ); QgsFeatureRenderer* renderer = layer->renderer(); if ( renderer && renderer->capabilities() & QgsFeatureRenderer::ScaleDependent ) { // setup scale for scale dependent visibility (rule based) renderer->startRender( context, layer->fields() ); filter = renderer->capabilities() & QgsFeatureRenderer::Filter; } for ( ; f_it != featureList.end(); ++f_it ) { QMap< QString, QString > derivedAttributes = commonDerivedAttributes; QgsFeatureId fid = f_it->id(); context.expressionContext().setFeature( *f_it ); if ( filter && !renderer->willRenderFeature( *f_it, context ) ) continue; featureCount++; derivedAttributes.unite( featureDerivedAttributes( &( *f_it ), layer, toLayerCoordinates( layer, point ) ) ); derivedAttributes.insert( tr( "feature id" ), fid < 0 ? tr( "new feature" ) : FID_TO_STRING( fid ) ); results->append( IdentifyResult( qobject_cast<QgsMapLayer *>( layer ), *f_it, derivedAttributes ) ); } if ( renderer && renderer->capabilities() & QgsFeatureRenderer::ScaleDependent ) { renderer->stopRender( context ); } QgsDebugMsg( "Feature count on identify: " + QString::number( featureCount ) ); QApplication::restoreOverrideCursor(); return featureCount > 0; }
bool QgsMapToolIdentify::identifyRasterLayer( QList<IdentifyResult> *results, QgsRasterLayer *layer, QgsPointXY point, const QgsRectangle &viewExtent, double mapUnitsPerPixel ) { QgsDebugMsg( "point = " + point.toString() ); if ( !layer ) return false; QgsRasterDataProvider *dprovider = layer->dataProvider(); if ( !dprovider ) return false; int capabilities = dprovider->capabilities(); if ( !( capabilities & QgsRasterDataProvider::Identify ) ) return false; QgsPointXY pointInCanvasCrs = point; try { point = toLayerCoordinates( layer, point ); } catch ( QgsCsException &cse ) { Q_UNUSED( cse ); QgsDebugMsg( QString( "coordinate not reprojectable: %1" ).arg( cse.what() ) ); return false; } QgsDebugMsg( QString( "point = %1 %2" ).arg( point.x() ).arg( point.y() ) ); if ( !layer->extent().contains( point ) ) return false; QMap< QString, QString > attributes, derivedAttributes; QgsRaster::IdentifyFormat format = QgsRasterDataProvider::identifyFormatFromName( layer->customProperty( QStringLiteral( "identify/format" ) ).toString() ); // check if the format is really supported otherwise use first supported format if ( !( QgsRasterDataProvider::identifyFormatToCapability( format ) & capabilities ) ) { if ( capabilities & QgsRasterInterface::IdentifyFeature ) format = QgsRaster::IdentifyFormatFeature; else if ( capabilities & QgsRasterInterface::IdentifyValue ) format = QgsRaster::IdentifyFormatValue; else if ( capabilities & QgsRasterInterface::IdentifyHtml ) format = QgsRaster::IdentifyFormatHtml; else if ( capabilities & QgsRasterInterface::IdentifyText ) format = QgsRaster::IdentifyFormatText; else return false; } QgsRasterIdentifyResult identifyResult; // We can only use current map canvas context (extent, width, height) if layer is not reprojected, if ( dprovider->crs() != mCanvas->mapSettings().destinationCrs() ) { // To get some reasonable response for point/line WMS vector layers we must // use a context with approximately a resolution in layer CRS units // corresponding to current map canvas resolution (for examplei UMN Mapserver // in msWMSFeatureInfo() -> msQueryByRect() is using requested pixel // + TOLERANCE (layer param) for feature selection) // QgsRectangle r; r.setXMinimum( pointInCanvasCrs.x() - mapUnitsPerPixel / 2. ); r.setXMaximum( pointInCanvasCrs.x() + mapUnitsPerPixel / 2. ); r.setYMinimum( pointInCanvasCrs.y() - mapUnitsPerPixel / 2. ); r.setYMaximum( pointInCanvasCrs.y() + mapUnitsPerPixel / 2. ); r = toLayerCoordinates( layer, r ); // will be a bit larger // Mapserver (6.0.3, for example) does not work with 1x1 pixel box // but that is fixed (the rect is enlarged) in the WMS provider identifyResult = dprovider->identify( point, format, r, 1, 1 ); } else { // It would be nice to use the same extent and size which was used for drawing, // so that WCS can use cache from last draw, unfortunately QgsRasterLayer::draw() // is doing some tricks with extent and size to align raster to output which // would be difficult to replicate here. // Note: cutting the extent may result in slightly different x and y resolutions // and thus shifted point calculated back in QGIS WMS (using average resolution) //viewExtent = dprovider->extent().intersect( &viewExtent ); // Width and height are calculated from not projected extent and we hope that // are similar to source width and height used to reproject layer for drawing. // TODO: may be very dangerous, because it may result in different resolutions // in source CRS, and WMS server (QGIS server) calcs wrong coor using average resolution. int width = std::round( viewExtent.width() / mapUnitsPerPixel ); int height = std::round( viewExtent.height() / mapUnitsPerPixel ); QgsDebugMsg( QString( "viewExtent.width = %1 viewExtent.height = %2" ).arg( viewExtent.width() ).arg( viewExtent.height() ) ); QgsDebugMsg( QString( "width = %1 height = %2" ).arg( width ).arg( height ) ); QgsDebugMsg( QString( "xRes = %1 yRes = %2 mapUnitsPerPixel = %3" ).arg( viewExtent.width() / width ).arg( viewExtent.height() / height ).arg( mapUnitsPerPixel ) ); identifyResult = dprovider->identify( point, format, viewExtent, width, height ); } derivedAttributes.insert( tr( "(clicked coordinate X)" ), formatXCoordinate( pointInCanvasCrs ) ); derivedAttributes.insert( tr( "(clicked coordinate Y)" ), formatYCoordinate( pointInCanvasCrs ) ); if ( identifyResult.isValid() ) { QMap<int, QVariant> values = identifyResult.results(); QgsGeometry geometry; if ( format == QgsRaster::IdentifyFormatValue ) { for ( auto it = values.constBegin(); it != values.constEnd(); ++it ) { QString valueString; if ( it.value().isNull() ) { valueString = tr( "no data" ); } else { QVariant value( it.value() ); // The cast is legit. Quoting QT doc : // "Although this function is declared as returning QVariant::Type, // the return value should be interpreted as QMetaType::Type" if ( static_cast<QMetaType::Type>( value.type() ) == QMetaType::Float ) { valueString = QgsRasterBlock::printValue( value.toFloat() ); } else { valueString = QgsRasterBlock::printValue( value.toDouble() ); } } attributes.insert( dprovider->generateBandName( it.key() ), valueString ); } QString label = layer->name(); results->append( IdentifyResult( qobject_cast<QgsMapLayer *>( layer ), label, attributes, derivedAttributes ) ); } else if ( format == QgsRaster::IdentifyFormatFeature ) { for ( auto it = values.constBegin(); it != values.constEnd(); ++it ) { QVariant value = it.value(); if ( value.type() == QVariant::Bool && !value.toBool() ) { // sublayer not visible or not queryable continue; } if ( value.type() == QVariant::String ) { // error // TODO: better error reporting QString label = layer->subLayers().value( it.key() ); attributes.clear(); attributes.insert( tr( "Error" ), value.toString() ); results->append( IdentifyResult( qobject_cast<QgsMapLayer *>( layer ), label, attributes, derivedAttributes ) ); continue; } // list of feature stores for a single sublayer const QgsFeatureStoreList featureStoreList = it.value().value<QgsFeatureStoreList>(); for ( const QgsFeatureStore &featureStore : featureStoreList ) { const QgsFeatureList storeFeatures = featureStore.features(); for ( QgsFeature feature : storeFeatures ) { attributes.clear(); // WMS sublayer and feature type, a sublayer may contain multiple feature types. // Sublayer name may be the same as layer name and feature type name // may be the same as sublayer. We try to avoid duplicities in label. QString sublayer = featureStore.params().value( QStringLiteral( "sublayer" ) ).toString(); QString featureType = featureStore.params().value( QStringLiteral( "featureType" ) ).toString(); // Strip UMN MapServer '_feature' featureType.remove( QStringLiteral( "_feature" ) ); QStringList labels; if ( sublayer.compare( layer->name(), Qt::CaseInsensitive ) != 0 ) { labels << sublayer; } if ( featureType.compare( sublayer, Qt::CaseInsensitive ) != 0 || labels.isEmpty() ) { labels << featureType; } QMap< QString, QString > derAttributes = derivedAttributes; derAttributes.unite( featureDerivedAttributes( &feature, layer ) ); IdentifyResult identifyResult( qobject_cast<QgsMapLayer *>( layer ), labels.join( QStringLiteral( " / " ) ), featureStore.fields(), feature, derAttributes ); identifyResult.mParams.insert( QStringLiteral( "getFeatureInfoUrl" ), featureStore.params().value( QStringLiteral( "getFeatureInfoUrl" ) ) ); results->append( identifyResult ); } } } } else // text or html { QgsDebugMsg( QString( "%1 HTML or text values" ).arg( values.size() ) ); for ( auto it = values.constBegin(); it != values.constEnd(); ++it ) { QString value = it.value().toString(); attributes.clear(); attributes.insert( QLatin1String( "" ), value ); QString label = layer->subLayers().value( it.key() ); results->append( IdentifyResult( qobject_cast<QgsMapLayer *>( layer ), label, attributes, derivedAttributes ) ); } } } else { attributes.clear(); QString value = identifyResult.error().message( QgsErrorMessage::Text ); attributes.insert( tr( "Error" ), value ); QString label = tr( "Identify error" ); results->append( IdentifyResult( qobject_cast<QgsMapLayer *>( layer ), label, attributes, derivedAttributes ) ); } return true; }