Edit feature attributes which are linked to annotation through an expression.

Use case
Annotation is useful for displaying text that you don’t want to move or resize when the map is panned or zoomed (unlike labels which will move and resize). Feature-linked annotation will update when a feature attribute referenced by the annotation expression is also updated. Additionally, the position of the annotation will transform to match any transformation to the linked feature’s geometry.
How to use the sample
Pan and zoom the map to see that the text on the map is annotation, not labels. Tap one of the address points to update the house number (AD_ADDRESS) and street name (ST_STR_NAM). Tap one of the dashed parcel polylines and tap another location to change its geometry. NOTE: Selection is only enabled for points and straight (single segment) polylines.
The feature-linked annotation will update accordingly.
How it works
- Load the geodatabase. NOTE: Read/write geodatabases should normally come from a
GeodatabaseSyncTask, but this has been omitted here. That functionality is covered in the sample Generate geodatabase. - Create
FeatureLayers from geodatabase feature tables found in the geodatabase withGeodatabase::geodatabaseFeatureTable. - Create
AnnotationLayers from geodatabase feature tables found in the geodatabase withGeodatabase::geodatabaseAnnotationTable. - Add the feature layers and annotation layers to the map’s operational layers.
- Connect to
MapQuickView::mouseClickedto capture clicks on the map to either select address points or parcel polyline features. NOTE: Selection is only enabled for points and straight (single segment) polylines.- For the address points, a dialog will opened to allow editing of the address number (AD_ADDRESS) and street name (ST_STR_NAM) attributes. A second tap will change the location of the point.
- For the parcel lines, a second tap will change one of the polyline’s vertices.
Both expressions were defined by the data author in ArcGIS Pro using the Arcade expression language.
Relevant API
- AnnotationLayer
- Feature
- FeatureLayer
- Geodatabase
Offline data
| Link | Local Location |
|---|---|
| Loudoun geodatabase | <userhome>/ArcGIS/Runtime/Data/geodatabase/loudoun_anno.geodatabase |
About the data
This sample uses data derived from the Loudoun GeoHub.
The annotation linked to the point data in this sample is defined by arcade expression $feature.AD_ADDRESS + " " + $feature.ST_STR_NAM. The annotation linked to the parcel polyline data is defined by Round(Length(Geometry($feature), 'feet'), 2).
Tags
annotation, attributes, feature-linked annotation, features, fields
Sample Code
// [WriteFile Name=EditFeaturesWithFeatureLinkedAnnotation, Category=EditData]// [Legal]// Copyright 2020 Esri.//// Licensed under the Apache License, Version 2.0 (the "License");// you may not use this file except in compliance with the License.// You may obtain a copy of the License at// http://www.apache.org/licenses/LICENSE-2.0//// Unless required by applicable law or agreed to in writing, software// distributed under the License is distributed on an "AS IS" BASIS,// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.// See the License for the specific language governing permissions and// limitations under the License.// [Legal]
#ifdef PCH_BUILD#include "pch.hpp"#endif // PCH_BUILD
// sample headers#include "EditFeaturesWithFeatureLinkedAnnotation.h"
// ArcGIS Maps SDK headers#include "AnnotationLayer.h"#include "AttributeListModel.h"#include "Feature.h"#include "FeatureLayer.h"#include "GeoElement.h"#include "Geodatabase.h"#include "GeodatabaseFeatureTable.h"#include "GeometryEngine.h"#include "IdentifyLayerResult.h"#include "ImmutablePart.h"#include "ImmutablePartCollection.h"#include "LayerListModel.h"#include "Map.h"#include "MapQuickView.h"#include "MapTypes.h"#include "Part.h"#include "PartCollection.h"#include "Polyline.h"#include "PolylineBuilder.h"#include "ProximityResult.h"#include "SpatialReference.h"#include "Viewpoint.h"
// Qt headers#include <QFile>#include <QFuture>#include <QStandardPaths>#include <QString>#include <QTimer>#include <QUuid>#include <QtCore/qglobal.h>
// STL headers#include <memory>
using namespace Esri::ArcGISRuntime;
// helper method to get cross platform data pathnamespace{ QString defaultDataPath() { QString dataPath;
#ifdef Q_OS_IOS dataPath = QStandardPaths::writableLocation(QStandardPaths::DocumentsLocation);#else dataPath = QStandardPaths::writableLocation(QStandardPaths::HomeLocation);#endif
return dataPath; }} // namespace
using namespace Esri::ArcGISRuntime;
namespace{ // Convenience RAII struct that deletes all pointers in given container. struct IdentifyLayerResultsScopedCleanup { IdentifyLayerResultsScopedCleanup(const QList<IdentifyLayerResult*>& list) : results(list) { }
~IdentifyLayerResultsScopedCleanup() { qDeleteAll(results); }
const QList<IdentifyLayerResult*>& results; };} // namespace
EditFeaturesWithFeatureLinkedAnnotation::EditFeaturesWithFeatureLinkedAnnotation(QObject* parent /* = nullptr */) : QObject(parent), s_ad_address(QStringLiteral("AD_ADDRESS")), s_st_str_nam(QStringLiteral("ST_STR_NAM")){ constexpr double lat = 39.0204; constexpr double lon = -77.4159; constexpr double scale = 2256.994353; m_map = new Map(BasemapStyle::ArcGISLightGray, this); m_map->setInitialViewpoint(Viewpoint(lat, lon, scale));
const QString dataPath = defaultDataPath() + "/ArcGIS/Runtime/Data/geodatabase/loudoun_anno.geodatabase";
m_geodatabase = new Geodatabase(dataPath, this); connect(m_geodatabase, &Geodatabase::doneLoading, this, &EditFeaturesWithFeatureLinkedAnnotation::onGeodatabaseDoneLoading_);
m_geodatabase->load();}
EditFeaturesWithFeatureLinkedAnnotation::~EditFeaturesWithFeatureLinkedAnnotation() = default;
void EditFeaturesWithFeatureLinkedAnnotation::init(){ // Register the map view for QML qmlRegisterType<MapQuickView>("Esri.Samples", 1, 0, "MapView"); qmlRegisterType<EditFeaturesWithFeatureLinkedAnnotation>("Esri.Samples", 1, 0, "EditFeaturesWithFeatureLinkedAnnotationSample");}
MapQuickView* EditFeaturesWithFeatureLinkedAnnotation::mapView() const{ return m_mapView;}
// Set the view (created in QML)void EditFeaturesWithFeatureLinkedAnnotation::setMapView(MapQuickView* mapView){ if (!mapView || mapView == m_mapView) { return; }
m_mapView = mapView; m_mapView->setMap(m_map);
connect(m_mapView, &MapQuickView::mouseClicked, this, &EditFeaturesWithFeatureLinkedAnnotation::onMouseClicked_);
emit mapViewChanged();}
void EditFeaturesWithFeatureLinkedAnnotation::onGeodatabaseDoneLoading_(const Error& error){ if (!error.isEmpty()) { return; }
GeodatabaseFeatureTable* pointFeatureTable = m_geodatabase->geodatabaseFeatureTable("Loudoun_Address_Points_1"); GeodatabaseFeatureTable* parcelLinesFeatureTable = m_geodatabase->geodatabaseFeatureTable("ParcelLines_1");
GeodatabaseFeatureTable* addressPointsAnnotationTable = m_geodatabase->geodatabaseAnnotationTable("Loudoun_Address_PointsAnno_1"); GeodatabaseFeatureTable* parcelLinesAnnotationTable = m_geodatabase->geodatabaseAnnotationTable("ParcelLinesAnno_1");
// create feature layers from tables in the geodatabase m_pointFeatureLayer = new FeatureLayer(pointFeatureTable, this); m_parcelLinesFeatureLayer = new FeatureLayer(parcelLinesFeatureTable, this);
// create annotation layers from tables in the geodatabase m_addressPointsAnnotationLayer = new AnnotationLayer(addressPointsAnnotationTable, this); m_parcelLinesAnnotationLayer = new AnnotationLayer(parcelLinesAnnotationTable, this);
// add feature layers to map m_map->operationalLayers()->append(m_pointFeatureLayer); m_map->operationalLayers()->append(m_parcelLinesFeatureLayer);
// add annotation layers to map m_map->operationalLayers()->append(m_addressPointsAnnotationLayer); m_map->operationalLayers()->append(m_parcelLinesAnnotationLayer);}
void EditFeaturesWithFeatureLinkedAnnotation::onMouseClicked_(QMouseEvent& mouseEvent){ clearSelection();
if (m_selectedFeature) { // move feature to clicked locaiton if already selected const Point clickedPoint = m_mapView->screenToLocation(mouseEvent.position().x(), mouseEvent.position().y()); moveFeature(clickedPoint); } else { // identify and select feature m_mapView->identifyLayersAsync(mouseEvent.position(), 10, false) .then(this, [this](const QList<IdentifyLayerResult*>& identifyResults) { onIdentifyLayersCompleted_(identifyResults); }); }}
void EditFeaturesWithFeatureLinkedAnnotation::onIdentifyLayersCompleted_(const QList<IdentifyLayerResult*>& identifyResults){ // A convenience wrapper that deletes the contents of identifyResults when we leave scope. IdentifyLayerResultsScopedCleanup identifyResultsScopedCleanup(identifyResults);
for (IdentifyLayerResult* identifyResult : identifyResults) { if (!identifyResult->geoElements().isEmpty()) { // only perform selections on Feature Layers FeatureLayer* featureLayer = dynamic_cast<FeatureLayer*>(identifyResult->layerContent());
// if cast was successful select features. if (featureLayer) { m_selectedFeature = static_cast<Feature*>(identifyResult->geoElements().at(0));
// Prevent the feature from being deleted when identifyResultsLock goes out of scope and cleans up identifyResults m_selectedFeature->setParent(this);
const GeometryType selectedFeatureGeomType = m_selectedFeature->geometry().geometryType();
if (selectedFeatureGeomType == GeometryType::Polyline) { const Polyline geom = geometry_cast<Polyline>(m_selectedFeature->geometry()); const PolylineBuilder polylineBuilder(geom);
// if the selected feature is a polyline with any part containing more than one segment // (i.e. a curve) do not select it if (polylineBuilder.toPolyline().parts().part(0).segmentCount() > 1) { clearSelection(); delete m_selectedFeature; m_selectedFeature = nullptr; return; } } else if (selectedFeatureGeomType == GeometryType::Point) { // clear string list with attribute values. m_addressAndStreetText.clear();
// update QML text fields with the attributes for the selected point feature const QString addressText = m_selectedFeature->attributes()->attributeValue(s_ad_address).toString(); m_addressAndStreetText.append(addressText); const QString streetNameText = m_selectedFeature->attributes()->attributeValue(s_st_str_nam).toString(); m_addressAndStreetText.append(streetNameText);
emit addressAndStreetTextChanged(); } featureLayer->selectFeature(m_selectedFeature); return; } } }}
void EditFeaturesWithFeatureLinkedAnnotation::clearSelection(){ for (Layer* layer : *m_map->operationalLayers()) { FeatureLayer* featureLayer = dynamic_cast<FeatureLayer*>(layer); if (featureLayer) { featureLayer->clearSelection(); } }}
void EditFeaturesWithFeatureLinkedAnnotation::moveFeature(Point mapPoint){ const Geometry geom = m_selectedFeature->geometry();
const Point projectedMapPoint = geometry_cast<Point>(GeometryEngine::project(mapPoint, geom.spatialReference())); if (geom.geometryType() == GeometryType::Polyline) { // get nearest vertex to the map point on the selected polyline const ProximityResult nearestVertex = GeometryEngine::nearestVertex(geom, projectedMapPoint); const PolylineBuilder polylineBuilder(geometry_cast<Polyline>(geom));
// get part of polyline nearest to map point Part* part = polylineBuilder.parts()->part(nearestVertex.partIndex());
// remove nearest point to map point part->removePoint(nearestVertex.pointIndex());
// add the map point as a new point part->addPoint(projectedMapPoint);
// update the selected feature with the new geometry m_selectedFeature->setGeometry(polylineBuilder.toGeometry()); } else if (geom.geometryType() == GeometryType::Point) { // if the selected feature is a point, change the geometry with the map point m_selectedFeature->setGeometry(mapPoint); }
// update the selected feature with the new geometry auto future = m_selectedFeature->featureTable()->updateFeatureAsync(m_selectedFeature); Q_UNUSED(future) clearSelection(); delete m_selectedFeature; m_selectedFeature = nullptr;}
void EditFeaturesWithFeatureLinkedAnnotation::updateSelectedFeature(const QString& address, const QString& streetName){ if (!m_selectedFeature) { return; }
// update the two attributes with the inputted text. m_selectedFeature->attributes()->replaceAttribute(s_ad_address, address); m_selectedFeature->attributes()->replaceAttribute(s_st_str_nam, streetName); auto future = m_selectedFeature->featureTable()->updateFeatureAsync(m_selectedFeature); Q_UNUSED(future)}// [WriteFile Name=EditFeaturesWithFeatureLinkedAnnotation, Category=EditData]// [Legal]// Copyright 2020 Esri.//// Licensed under the Apache License, Version 2.0 (the "License");// you may not use this file except in compliance with the License.// You may obtain a copy of the License at// http://www.apache.org/licenses/LICENSE-2.0//// Unless required by applicable law or agreed to in writing, software// distributed under the License is distributed on an "AS IS" BASIS,// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.// See the License for the specific language governing permissions and// limitations under the License.// [Legal]
#ifndef EDITFEATURESWITHFEATURELINKEDANNOTATION_H#define EDITFEATURESWITHFEATURELINKEDANNOTATION_H
// ArcGIS Maps SDK headers#include "Error.h"#include "Point.h"
// Qt headers#include <QDir>#include <QMouseEvent>#include <QObject>#include <QUuid>
namespace Esri::ArcGISRuntime{ class AnnotationLayer; class Feature; class FeatureLayer; class Geodatabase; class IdentifyLayerResult; class Map; class MapQuickView;} // namespace Esri::ArcGISRuntime
Q_MOC_INCLUDE("MapQuickView.h")Q_MOC_INCLUDE("IdentifyLayerResult.h")
class EditFeaturesWithFeatureLinkedAnnotation : public QObject{ Q_OBJECT
Q_PROPERTY(Esri::ArcGISRuntime::MapQuickView* mapView READ mapView WRITE setMapView NOTIFY mapViewChanged) Q_PROPERTY(QStringList addressAndStreetText MEMBER m_addressAndStreetText NOTIFY addressAndStreetTextChanged)
public: explicit EditFeaturesWithFeatureLinkedAnnotation(QObject* parent = nullptr); ~EditFeaturesWithFeatureLinkedAnnotation();
static void init();
void clearSelection(); void moveFeature(Esri::ArcGISRuntime::Point mapPoint); Q_INVOKABLE void updateSelectedFeature(const QString& address, const QString& streetName);
signals: void mapViewChanged(); void addressAndStreetTextChanged();
private: void onIdentifyLayersCompleted_(const QList<Esri::ArcGISRuntime::IdentifyLayerResult*>& identifyResults); void onMouseClicked_(QMouseEvent& mouseEvent); void onGeodatabaseDoneLoading_(const Esri::ArcGISRuntime::Error& error);
Esri::ArcGISRuntime::MapQuickView* mapView() const; void setMapView(Esri::ArcGISRuntime::MapQuickView* mapView);
Esri::ArcGISRuntime::Map* m_map = nullptr; Esri::ArcGISRuntime::MapQuickView* m_mapView = nullptr; Esri::ArcGISRuntime::Geodatabase* m_geodatabase = nullptr; Esri::ArcGISRuntime::FeatureLayer* m_pointFeatureLayer = nullptr; Esri::ArcGISRuntime::FeatureLayer* m_parcelLinesFeatureLayer = nullptr; Esri::ArcGISRuntime::AnnotationLayer* m_addressPointsAnnotationLayer = nullptr; Esri::ArcGISRuntime::AnnotationLayer* m_parcelLinesAnnotationLayer = nullptr; Esri::ArcGISRuntime::Feature* m_selectedFeature = nullptr; Esri::ArcGISRuntime::Point m_screenClickPoint; QStringList m_addressAndStreetText; const QString s_ad_address; const QString s_st_str_nam;};
#endif // EDITFEATURESWITHFEATURELINKEDANNOTATION_H// [WriteFile Name=EditFeaturesWithFeatureLinkedAnnotation, Category=EditData]// [Legal]// Copyright 2020 Esri.//// Licensed under the Apache License, Version 2.0 (the "License");// you may not use this file except in compliance with the License.// You may obtain a copy of the License at// http://www.apache.org/licenses/LICENSE-2.0//// Unless required by applicable law or agreed to in writing, software// distributed under the License is distributed on an "AS IS" BASIS,// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.// See the License for the specific language governing permissions and// limitations under the License.// [Legal]
import QtQuickimport QtQuick.Controlsimport QtQuick.Layoutsimport Esri.Samples
Item {
// add a mapView component MapView { id: view anchors.fill: parent
Component.onCompleted: { // Set the focus on MapView to initially enable keyboard navigation forceActiveFocus(); }
onMouseClicked: { if (updateWindow.visible) updateWindow.visible = false; } }
// Update Window Rectangle { id: updateWindow anchors.centerIn: parent width: gridLayout.implicitWidth + 10 height: gridLayout.implicitHeight + 10 radius: 10 visible: false color: palette.base
MouseArea { anchors.fill: parent acceptedButtons: Qt.LeftButton | Qt.RightButton onClicked: mouse => mouse.accepted = true; onDoubleClicked: mouse => mouse.accepted = true onWheel: wheel => wheel.accepted = true; }
GridLayout { id: gridLayout columns: 2 anchors.margins: 5
Label { Layout.columnSpan: 2 Layout.margins: 5 Layout.alignment: Qt.AlignHCenter text: qsTr("Update Attributes") font.pixelSize: 16 }
Label { text: qsTr("AD_ADDRESS:") Layout.margins: 5 }
TextField { id: attAddressTextField selectByMouse: true }
Label { text: qsTr("ST_STR_NAM:") Layout.margins: 5 }
TextField { id: attStreetTextField selectByMouse: true }
Row { Layout.alignment: Qt.AlignRight Layout.columnSpan: 2 height: childrenRect.height spacing: 5
Button { Layout.margins: 5 Layout.alignment: Qt.AlignRight text: qsTr("Update") // once the update button is clicked, hide the windows, and fetch the currently selected features onClicked: { updateWindow.visible = false; model.updateSelectedFeature(attAddressTextField.text, attStreetTextField.text); } }
Button { Layout.alignment: Qt.AlignRight Layout.margins: 5 text: qsTr("Cancel") // once the cancel button is clicked, hide the window onClicked: updateWindow.visible = false; } } } }
// Declare the C++ instance which creates the map etc. and supply the view EditFeaturesWithFeatureLinkedAnnotationSample { id: model mapView: view
onAddressAndStreetTextChanged: { updateWindow.visible = true;
attAddressTextField.text = model.addressAndStreetText[0]; attStreetTextField.text = model.addressAndStreetText[1]; } }}// [WriteFile Name=EditFeaturesWithFeatureLinkedAnnotation, Category=EditData]// [Legal]// Copyright 2020 Esri.//// Licensed under the Apache License, Version 2.0 (the "License");// you may not use this file except in compliance with the License.// You may obtain a copy of the License at// http://www.apache.org/licenses/LICENSE-2.0//// Unless required by applicable law or agreed to in writing, software// distributed under the License is distributed on an "AS IS" BASIS,// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.// See the License for the specific language governing permissions and// limitations under the License.// [Legal]
// sample headers#include "EditFeaturesWithFeatureLinkedAnnotation.h"
// ArcGIS Maps SDK headers#include "ArcGISRuntimeEnvironment.h"
// Qt headers#include <QDir>#include <QGuiApplication>#include <QQmlApplicationEngine>
// Platform specific headers#ifdef Q_OS_WIN#include <Windows.h>#endif
int main(int argc, char* argv[]){ QGuiApplication app(argc, argv); app.setApplicationName(QString("EditFeaturesWithFeatureLinkedAnnotation"));
// Use of ArcGIS location services, such as basemap styles, geocoding, and routing services, // requires an access token. For more information see // https://links.esri.com/arcgis-runtime-security-auth.
// The following methods grant an access token:
// 1. User authentication: Grants a temporary access token associated with a user's ArcGIS account. // To generate a token, a user logs in to the app with an ArcGIS account that is part of an // organization in ArcGIS Online or ArcGIS Enterprise.
// 2. API key authentication: Get a long-lived access token that gives your application access to // ArcGIS location services. Go to the tutorial at https://links.esri.com/create-an-api-key. // Copy the API Key access token.
const QString accessToken = QString("");
if (accessToken.isEmpty()) { qWarning() << "Use of ArcGIS location services, such as the basemap styles service, requires" << "you to authenticate with an ArcGIS account or set the API Key property."; } else { Esri::ArcGISRuntime::ArcGISRuntimeEnvironment::setApiKey(accessToken); }
// Initialize the sample EditFeaturesWithFeatureLinkedAnnotation::init();
// Initialize application view QQmlApplicationEngine engine; // Add the import Path engine.addImportPath(QDir(QCoreApplication::applicationDirPath()).filePath("qml"));
#ifdef ARCGIS_RUNTIME_IMPORT_PATH_2 engine.addImportPath(ARCGIS_RUNTIME_IMPORT_PATH_2);#endif
// Set the source engine.load(QUrl("qrc:/Samples/EditData/EditFeaturesWithFeatureLinkedAnnotation/main.qml"));
return app.exec();}// Copyright 2020 Esri.
// Licensed under the Apache License, Version 2.0 (the "License");// you may not use this file except in compliance with the License.// You may obtain a copy of the License at// http://www.apache.org/licenses/LICENSE-2.0
// Unless required by applicable law or agreed to in writing, software// distributed under the License is distributed on an "AS IS" BASIS,// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.// See the License for the specific language governing permissions and// limitations under the License.
import QtQuick.Controlsimport Esri.Samples
ApplicationWindow { visible: true width: 800 height: 600
EditFeaturesWithFeatureLinkedAnnotation { anchors.fill: parent }}