The pulldata("@layer") function was introduced with Survey123 version 3.16 (October 2022). It allows you to query an ArcGIS layer and retrieve data from it. In this blog, I will explore common scenarios for this function and describe design best practices and limitations.
A bit of context first
Querying ArcGIS layers using XLSForm expressions is not a new concept. Back in the day, I described how to do it using custom JavaScript functions. Using custom JS functions gives you great flexibility, but you need to know how to write JavaScript, and most importantly, you can't execute a custom JS function if you share a survey publicly.
The new pulldata("@layer") function simplifies the syntax for querying ArcGIS layers, and it can be used in public surveys too!
Point-in-polygon calculations using a map
The animation below shows a typical scenario for pulldata("@layer"). A calculation takes the location set in the map, and triggers a point-in-polygon query to retrieve a value from the intersecting polygon; a parcel's APN (Assessor Parcel Number) in this case.
I can think of a gazillion cases where point-in-polygon queries will be handy... just think of the polygon layers you may have: parcels, soils, fire perimeters, sales territories, evacuation zones, city boundaries...
Point-in-polygon calculations using an address location
Point-in-polygon queries do not strictly require a map in your form: They require a location and a polygon layer. In the example below, the respondent enters an address, and a point-in-polygon query is triggered to automatically calculate the City Code that applies to that location. In this case, the location for your point-in-polygon query does not come from a map, but an address. The City Code comes from the feature layer targeted in your point-in-polygon query.
Point-in-polygon calculations will help you make your forms geographically smarter. You can choose to display the geo-calculated values in the form, or keep them hidden.
Point-in-polygon queries for data validation
You can use the output of pulldata("@layer") to calculate a value, like in the examples above, or to feed a data validation rule. In the next animation, a point-in-polygon query checks if a reported incident falls within a City of Redlands public park. If it does, the report is accepted. If it does not, the user will not be able to submit the form.
Again, we are essentially doing a point-in-polygon query using pulldata("@layer"). In this case, the output of the query is used to check the integrity of the data.
Calculate values using an attribute query
In the example below the respondent enters a customer number and the form automatically populates contact information about that customer. That is all done through pulldata("@layer"): The customer number is used to find a record in the customer layer. If found, the name and email attributes are populated.
Customers, assets, buildings, parts... You can query any ArcGIS layer or table as long as, of course, the user completing the form has access to the layer or table.
Count records in a table or layer
Another common use case is that in which you want a survey to stop accepting responses once a certain number of records are submitted. This is also something pulldata("@layer") can do for you. Check out the next screenshot: Through a query to the signup table, we can tell how many people already signed up. A constraint in the form uses this value to allow (or not allow) another person to submit the survey.
Now that we have reviewed some common scenarios, we are going to get hands on...
Getting started with pulldata("@layer")
If you have read this far, you will be eager to start. The pulldata("@layer") syntax is described in this help topic. Have a good read of this topic as it describes the basics very well.
Point-in-polygon query to retrieve a single attribute: pulldata("@layer", "getValueAt")
This three-minute video shows, step-by-step, how to construct a simple point-in-polygon query using the World Administrative Divisions layer from ArcGIS Online. If you want to follow along, make sure you have Survey123 Connect version 3.16 or newer.
2022-10 GetValueAt Tutorial Connect.mp4
Video Player is loading.
Current Time 0:00
/
Duration 0:00
Loaded: 0%
0:00
Stream Type LIVE
Remaining Time -0:00
1x
Chapters
descriptions off, selected
captions settings, opens captions settings dialog
captions off, selected
This is a modal window.
Beginning of dialog window. Escape will cancel and close the window.
End of dialog window.
This is a modal window. This modal can be closed by pressing the Escape key or activating the close button.
For your reference, you will find the getValueAt.xlsx file attached at the bottom of this post.
The getValueAt_Constraint.xlsx file shows how you can use the calculated value from the point-in-polygon query to ensure the location chosen by the user falls within a polygon.
Point-in-polygon query to retrieve multiple attributes: pulldata("@layer", "getRecordAt")
Another three-minute step-by-step video building on the previous one. In this case, the getRecordAt operation is used to do a point-in-polygon query and get an entire record. Using the pulldata("@json") function, the record is parsed locally to get multiple attributes efficiently.
2022-10 GetRecordAt Tutorial Connect.mp4
Video Player is loading.
Current Time 0:00
/
Duration 0:00
Loaded: 0%
0:00
Stream Type LIVE
Remaining Time -0:00
1x
Chapters
descriptions off, selected
captions settings, opens captions settings dialog
captions off, selected
This is a modal window.
Beginning of dialog window. Escape will cancel and close the window.
End of dialog window.
This is a modal window. This modal can be closed by pressing the Escape key or activating the close button.
Find the getRecordAt.xlsx file in the attachments section below for reference.
Refining point-in-polygon queries using extra parameters
On top of the well defined parameters you can pass to the getRecord and getRecordAt operations, you can also pass extra parameters to the feature layer url to perform some more advanced queries. For example:
distance: This parameter allows you to buffer the input geometry (geopoint) to find polygons within a given distance. By default, the distance units come in meters, but you can also specify other units. For example, the following expression will apply a buffer of 1600 feet to the provided location (geopoint) and find any intersecting polygons in the critical habitats layer.
I highlighted in blue the key part where the distance and units are specified. These extra parameters are added to the URL. You need to add a question mark (?) to start adding extra parameters and then separate them with an ampersand symbol (&).
gdbVersion: This parameter is useful if you want to query a versioned multi-user geodatabase feature layer.
orderByFields: If you expect your query to return more than one value, you can use this parameter to sort the results. For example, say you use getRecord to get all tickets submitted by a user. You can use orderByFields to sort all tickets by date in descending order. This will make your getRecord operation to give you back the latest ticket submitted.
outStatistics and groupByFieldsForStatistics: Ideal when you want to retrieve stats from your layer, rather than a unique record or value. This is how, for example, you will want to get a count of records in a layer.
Survey123 Connect includes a sample called Query a Feature Layer. Have a look at it, as it illustrates some advanced techniques you can leverage to gather statistics and construct dynamic queries.
A few extra tips and things you should know
Public surveys: Unlike custom JS functions, you can use pulldata("@layer") with public surveys.
No offline support: The pulldata("@layer") function only works while online. This makes it particularly useful for web surveys, although if your device is connected, it will also work in the Survey123 field app.
Support starts with version 3.16: If you go into Survey123 Connect and you can't even get the samples to work, check your Connect version. You need 3.16 or newer for this function to work in Connect. All surveys published with version 3.16 or newer will work in the web app. For the field app, ensure users have the latest version of Survey123 installed on their devices. Otherwise, your pulldata("@layer") function will be ignored.
You do not need to add a layer to a map before you query it: I think this one should be obvious already, but I will add it just in case. You do not need to add a layer to your survey map before you can query it. In fact, you do not need a map in your survey to do a query. All you need to do is to pass the URL of your layer to the function.
About tokens: When using pulldata("@layer") you do not need to worry about tokens. Since the token of the signed in user is passed automatically to the function.
concat() is your friend: When composing a SQL WHERE filter, use the concat() function. For example:
Good: concat("MANHOLE_ID='", ${question1},"'")
Bad: MANHOLE_ID='${question1}'
SQL tips: Where statements can sometimes be tricky.
Quote if you work with a text field: concat("MANHOLE_ID='", ${question1},"'")
Do not quote if numeric: concat("MANHOLE_ID=", ${question1})
Layer sharing: Make sure the layers you query are appropriately shared. For example, you cannot expect a private layer to be available in a public survey. Do not dream of Survey123 magically letting you query a layer the signed in user does not have access to. If Survey123 cannot access a layer you reference in pulldata("@layer"), the output of the function will be empty. The user will not see an error or warning.
Layer capabilities: The layers you query need to be queryable. This may seem obvious, but it may catch you out. If the data you are querying is sensitive, consider the use of feature layer views to restrict access to fields and rows as appropriate.
pulldata("@layer") is not for populating a list: The purpose of pulldata("@layer") is to help you query a layer and get back either one record, or one value (attribute) from a record. If you want to query a layer to populate a list, use the search appearance instead as described in this blog post by @BrettS or this other blog post on geolists.
';
}
}
}
catch(e){
}
}
}
if (newSub.getAttribute("slang").toLowerCase() != code_l.toLowerCase()) {
if (trLabelsHtml != "") {
var labelSname = "";
if(labelEle[i].querySelector("ul li:nth-child(1)").getAttribute("aria-hidden")){
labelSname = labelEle[i].querySelector("ul li:nth-child(1)").outerHTML;
}
labelEle[i].innerHTML = "";
labelEle[i].innerHTML = labelSname + trLabelsHtml;
}
}
}
}
}
catch(e){
}
}
}
/* V 2.0:3 = Store not translated reply id */
if(lingoRSXML.snapshotLength == 0){
if($scope.falseReplyID == "") {
$scope.falseReplyID = value;
}
}
/* Get translated Body of Replies/Comments */
var lingoRBXML = doc.evaluate(lingoRBExp, doc, null, XPathResult.UNORDERED_NODE_SNAPSHOT_TYPE, null);
for(var i=0;i 0) {
var attachDiv = rootElement.querySelector('div.lia-quilt-row-main').querySelector('div.custom-attachments');
if (attachDiv) {
attachDiv = attachDiv.outerHTML;
}
else if(rootElement.querySelector('div.lia-quilt-row-main').querySelectorAll('#attachments').length > 0){
if ("BlogArticlePage" == "BlogArticlePage") {
attachDiv = rootElement.querySelector('div.lia-quilt-row-main .lia-message-body-content').querySelector('#attachments');
if (attachDiv) {
attachDiv = attachDiv.outerHTML;
}
else{
attachDiv = "";
}
}else{
attachDiv = rootElement.querySelector('div.lia-quilt-row-main').querySelector('#attachments').outerHTML;
}
}
else {
attachDiv = "";
}
/* Feedback Div */
var feedbackDiv = "";
var feedbackDivs = rootElement.querySelector('div.lia-quilt-row-main').querySelectorAll('div.lia-panel-feedback-banner-safe');
if (feedbackDivs.length > 0) {
for (var k = 0; k < feedbackDivs.length; k++) {
feedbackDiv = feedbackDiv + feedbackDivs[k].outerHTML;
}
}
}
else {
var attachDiv = rootElement.querySelector('div.lia-message-body-content').querySelector('div.Attachments.preview-attachments');
if (attachDiv) {
attachDiv = attachDiv.outerHTML;
} else {
attachDiv = "";
}
/* Everyone tags links */
if (document.querySelectorAll("div.TagList").length > 0){
var everyoneTagslink = document.querySelector('div.lia-quilt-row-main').querySelector(".MessageTagsTaplet .TagList");
if ((everyoneTagslink != null)||(everyoneTagslink != undefined)){
everyoneTagslink = everyoneTagslink.outerHTML;
}
else{
everyoneTagslink = "";
}
}
/* Feedback Div */
var feedbackDiv = "";
var feedbackDivs = rootElement.querySelector('div.lia-message-body-content').querySelectorAll('div.lia-panel-feedback-banner-safe');
if (feedbackDivs.length > 0) {
for (var m = 0; m < feedbackDivs.length; m++) {
feedbackDiv = feedbackDiv + feedbackDivs[m].outerHTML;
}
}
}
}
} catch (e) {
}
if (body_L == "") {
/* V 2.0:7 Replacing translated video data with source video data */
var newBodyVideoData = newBody.querySelectorAll('div[class*="video-embed"]');
angular.forEach($scope.videoData[value], function (sourceVideoElement, index) {
if (index <= (newBodyVideoData.length - 1)) {
newBodyVideoData[index].outerHTML = sourceVideoElement.outerHTML
}
});
/* V 2.0:7 = Replacing translated image data with source data */
var newBodyImageData = newBody.querySelectorAll('[class*="lia-image"]');
angular.forEach($scope.imageData[value], function (sourceImgElement, index) {
if (index <= (newBodyImageData.length - 1)) {
newBodyImageData[index].outerHTML = sourceImgElement.outerHTML;
}
});
/* V 2.0:7 = Replacing translated pre tag data with source data */
var newBodyPreTagData = newBody.querySelectorAll('pre');
angular.forEach($scope.preTagData[value], function (sourcePreTagElement, index) {
if (index <= (newBodyPreTagData.length - 1)) {
newBodyPreTagData[index].outerHTML = sourcePreTagElement.outerHTML;
}
});
}
var copyBodySubject = false;
if (body_L == "") {
copyBodySubject = true;
body_L = newBody.innerHTML;
}
/* This code is written as part of video fix by iTalent */
/* try{
var iframeHTMLText = body_L;
var searchIframeText = "<IFRAME";
var foundiFrameTag;
if (iframeHTMLText.indexOf(searchIframeText) > -1) {
foundiFrameTag = decodeHTMLEntities(iframeHTMLText);
foundiFrameTag = foundiFrameTag.split('src="')[1];
body_L = foundiFrameTag;
}
}
catch(e){
} */
/* This code is placed to remove the extra meta tag adding in the UI*/
try{
body_L = body_L.replace('<meta http-equiv="Content-Type" content="text/html; charset=UTF-8" />','');
}
catch(e){
}
/** We should not replace the source content if user profile language and selected target language matches with source language **/
if(showTrContent) {
var compiled = false;
rootElement.querySelectorAll('div.lia-message-body-content')[0].innerHTML = null
if("BlogArticlePage"=="IdeaPage"){
// var customAttachDiv = '';
rootElement.querySelectorAll('div.lia-message-body-content')[0].innerHTML = body_L + feedbackDiv ;
$compile(rootElement.querySelectorAll('div.lia-message-body-content')[0])($scope);
compiled = true;
/* Attach atttach div */
// document.querySelector("div.translation-attachments-"+value).innerHTML = attachDiv;
rootElement.querySelectorAll('div.lia-message-body-content')[0].insertAdjacentHTML('afterend',attachDiv);
if(rootElement.querySelectorAll('div.lia-quilt-idea-message .lia-message-body .lia-attachments-message').length > 1){
rootElement.querySelectorAll('div.lia-quilt-idea-message .lia-message-body .lia-attachments-message')[1].remove();
}
} else {
if("BlogArticlePage"=="TkbArticlePage"){
rootElement.querySelectorAll('div.lia-message-body-content')[0].innerHTML = body_L + feedbackDiv ;
}else{
rootElement.querySelectorAll('div.lia-message-body-content')[0].innerHTML = body_L + feedbackDiv + attachDiv;
compiled = true;
}
}
/* Destroy and recreate OOyala player videos to restore the videos in target languages which is written by iTalent as part of iTrack LILICON-79 */ /* Destroy and recreate OOyala player videos */
try{
// $scope.videoData[value][0].querySelector("div").getAttribute("id");
for(var vidIndex=0; vidIndex<$scope.videoData[value].length; vidIndex++){
if( $scope.videoData[value][vidIndex].querySelector("div") != null){
var containerId = LITHIUM.OOYALA.players[$scope.videoData[value][vidIndex].querySelector("div").getAttribute("id")].containerId;
videoId = LITHIUM.OOYALA.players[$scope.videoData[value][vidIndex].querySelector("div").getAttribute("id")].videoId;
/** Get the Video object */
vid = OO.Player.create(containerId,videoId);
/** Destroy the video **/
vid.destroy();
/** recreate in the same position */
var vid = OO.Player.create(containerId,videoId);
}
}
}
catch(e){
}
try{
for(var vidIndex=0; vidIndex<($scope.videoData[value].length); vidIndex++){
if($scope.videoData[value][vidIndex].querySelector('video-js') != null){
var data_id = $scope.videoData[value][vidIndex].querySelector('video-js').getAttribute('data-video-id');
var data_account = $scope.videoData[value][vidIndex].querySelector('video-js').getAttribute('data-account');
var data_palyer = $scope.videoData[value][vidIndex].querySelector('video-js').getAttribute('data-player');
var div = document.createElement('div');
div.id = "brightcove";
div.class = "brightcove-player";
div.innerHTML =
'(view in my videos)'
var data = div.getElementsByClassName("video-js");
var script = document.createElement('script');
script.src = "https://players.brightcove.net/" + data_account + "/" + data_palyer + "_default/index.min.js";
for(var i=0;i< data.length;i++){
videodata.push(data[i]);
}
}
}
for(var i=0;i< videodata.length;i++){
document.getElementsByClassName('lia-vid-container')[i].innerHTML = videodata[i].outerHTML;
document.body.appendChild(script);
}
}
catch(e){
}
if(!compiled){
/* Re compile html */
$compile(rootElement.querySelectorAll('div.lia-message-body-content')[0])($scope);
}
}
if (code_l.toLowerCase() != newBody.getAttribute("slang").toLowerCase()) {
/* Adding Translation flag */
var tr_obj = $filter('filter')($scope.sourceLangList, function (obj_l) {
return obj_l.code.toLowerCase() === newBody.getAttribute("slang").toLowerCase()
});
if (tr_obj.length > 0) {
tr_text = "Esri may utilize third parties to translate your data and/or imagery to facilitate communication across different languages.".replace(/lilicon-trans-text/g, tr_obj[0].title);
try {
if ($scope.wootMessages[$rootScope.profLang] != undefined) {
tr_text = $scope.wootMessages[$rootScope.profLang].replace(/lilicon-trans-text/g, tr_obj[0].title);
}
} catch (e) {
}
} else {
//tr_text = "This message was translated for your convenience!";
tr_text = "Esri may utilize third parties to translate your data and/or imagery to facilitate communication across different languages.";
}
try {
if (!document.getElementById("tr-msz-" + value)) {
var tr_para = document.createElement("P");
tr_para.setAttribute("id", "tr-msz-" + value);
tr_para.setAttribute("class", "tr-msz");
tr_para.style.textAlign = 'justify';
var tr_fTag = document.createElement("IMG");
tr_fTag.setAttribute("class", "tFlag");
tr_fTag.setAttribute("src", "/html/assets/langTrFlag.PNG");
tr_fTag.style.marginRight = "5px";
tr_fTag.style.height = "14px";
tr_para.appendChild(tr_fTag);
var tr_textNode = document.createTextNode(tr_text);
tr_para.appendChild(tr_textNode);
/* Woot message only for multi source */
if(rootElement.querySelector(".lia-quilt-forum-message")){
rootElement.querySelector(".lia-quilt-forum-message").appendChild(tr_para);
} else if(rootElement.querySelector(".lia-message-view-blog-topic-message")) {
rootElement.querySelector(".lia-message-view-blog-topic-message").appendChild(tr_para);
} else if(rootElement.querySelector(".lia-quilt-blog-reply-message")){
rootElement.querySelector(".lia-quilt-blog-reply-message").appendChild(tr_para);
} else if(rootElement.querySelector(".lia-quilt-tkb-message")){
rootElement.querySelector(".lia-quilt-tkb-message").appendChild(tr_para);
} else if(rootElement.querySelector(".lia-quilt-tkb-reply-message")){
rootElement.querySelector(".lia-quilt-tkb-reply-message").insertBefore(tr_para,rootElement.querySelector(".lia-quilt-row.lia-quilt-row-footer"));
} else if(rootElement.querySelector(".lia-quilt-idea-message")){
rootElement.querySelector(".lia-quilt-idea-message").appendChild(tr_para);
} else if(rootElement.querySelector('.lia-quilt-occasion-message')){
rootElement.querySelector('.lia-quilt-occasion-message').appendChild(tr_para);
}
else {
if (rootElement.querySelectorAll('div.lia-quilt-row-footer').length > 0) {
rootElement.querySelectorAll('div.lia-quilt-row-footer')[0].appendChild(tr_para);
} else {
rootElement.querySelectorAll('div.lia-quilt-column-message-footer')[0].appendChild(tr_para);
}
}
}
} catch (e) {
}
}
} else {
/* Do not display button for same language */
// syncList.remove(value);
var index = $scope.syncList.indexOf(value);
if (index > -1) {
$scope.syncList.splice(index, 1);
}
}
}
}
});
});
/* V 1.1:2 = Reply Sync button for multi source translation */
} catch(e){
console.log(e);
}
};
if((rContent != undefined) && (rContent != "")) {
drawCanvas(decodeURIComponent(rContent));
/** Update variable with selected language code **/
$scope.previousSelCode = code_l;
}
};
/**
* @function manageTranslation
* @description Managess the translation of given language for the thread
* @param {string} langCode - Language Code
* @param {string} tid - Thread ID
*/
$scope.manageTranslation = function (langCode, tid) {
//debugger;
$scope.showTrText = false;
/* V 2.0:5 = actualStatus variable introduced to indicate detailed connector status on UI. This variable holds the actual translation percentage */
$scope.transPercent = "";
$scope.actualStatus = "";
if (tid != "") {
var bulkTranslation = lithiumPlugin.bulkTranslation(langCode, tid);
bulkTranslation.then(function (trContent) {
if(trContent.body != "") {
$scope.showPreview(trContent.body, $scope.mszList, langCode);
if(langCode != "en-US") {
$scope.showTrText = true;
}
}
if((trContent.status != "NA") && trContent.status != null) {
// $scope.transPercent = String(trContent.status);
$scope.actualStatus = String(trContent.status);
} else {
// $rootScope.errorMsg = "Translation is in progress. Please check again a few minutes."
$rootScope.errorMsg = "Translation is in progress. Please retry in a few minutes."
}
$scope.workbench = trContent.wb;
/* V 2.0:4 = Trigger uncalled or delayed callbacks (documnet uploaded/translation completed from lithium).*/
if(trContent.callback == 'true') {
var trCompletCallback = lithiumPlugin.trCompletCallback(langCode, trContent.docID);
trCompletCallback.then(function (callback){
// $rootScope.errorMsg = "Downloading Translated content in " + langCode + " now. Please check again in a few minutes."
$rootScope.errorMsg = "Uploading content to translate. Please check again in a few minutes."
});
} else if (trContent.callback == 'upload') {
var trCompletUpload = lithiumPlugin.trCompletUpload(langCode, trContent.docID);
trCompletUpload.then(function (callback) {
//$rootScope.errorMsg = "Uploading content to translate. Please check again in a few minutes."
$rootScope.errorMsg = "Uploading content to translate. Please check again in a few minutes."
});
} else if ("many" == "one") {
$scope.updateOOS();
} else if("SmartConx" == "SmartConx"){
if ("many" == "many"){
$scope.updateOOS();
}
}else if ((trContent.status != null) && trContent.status.includes("100")) {
/* If everything fine then only check Out of Sync status */
$scope.updateOOS();
} else {
/* If translation perccent is less than 100 then show the percentage on UI */
$scope.transPercent = $scope.actualStatus;
}
});
}
}
/**
* @function selectThisLang
* @description Called on select dropdown.
* @param {string} lang - Language code
*
*/
$scope.selectThisLang = function (lang, anonymousFlag) {
/* 1.4:3 Update Analytics on language selection */
try {
setTimeout(()=>{
lingoThreadLangSelected(lang, '1224415');
console.log("Language",lang);
},5000)
} catch (e) {
console.log(e);
}
/** Display Translated content **/
var getTranslation = lithiumPlugin.getTranslation(lang, "1224415");
getTranslation.then(function (trContent) {
if (trContent.body != "") {
$scope.showPreview(trContent.body, $scope.mszList, lang);
} else {
//$rootScope.errorMsg = "Translation is in progress. Please check again in a few minutes."
$rootScope.errorMsg = "Translation is in progress. Please retry in a few minutes."
}
});
};
var decodeEntities = (function() {
// this prevents any overhead from creating the object each time
var element = document.createElement('div');
function decodeHTMLEntities (str) {
if(str && typeof str === 'string') {
// strip script/html tags
str = str.replace(/
Auto-suggest helps you quickly narrow down your search results by suggesting possible matches as you type.