define([ 'dojo/_base/array', 'dojo/_base/lang', 'dojo/Deferred', 'dojo/promise/all', 'dojo/string', 'esri/arcade/arcade', 'esri/arcade/Feature', 'esri/tasks/RelationshipQuery', 'jimu/utils', 'jimu/LayerStructure' ], function( array, lang, Deferred, all, string, Arcade, ArcadeFeature, RelationshipQuery, jimuUtils, LayerStructure) { var mo = {}; /*------------------------------------------------------------------------------------------------------------------*/ /********read arcade expr info*********/ mo.readExprInfo = {}; //public functions /** Get arcade profiles by type. * @param {string} type, arcade profile types, it could be three types:render, label, infoTemplate. it's optional, if no type or wrong type, it retruns all the profiles. * @return {object} when type is one of the three strings above. format: {render:[],label:[],infoTemplate:[]} {array} when type is none or others, every item in array is a object. item's format depends on the type of arcade profile's type. eg: infoTemplate's format is {layer:'layerId',expressionInfos:[]} //https://developers.arcgis.com/javascript/3/jshelp/arcade.html#popups */ mo.readExprInfo.getArcadeProfilesByType = function(map, operlayer, /* optional */ type){ var args = { map: map, layer: operlayer }; if(type === 'render'){ return mo.readExprInfo._getArcadeRender(args); }else if(type === 'label'){ return mo.readExprInfo._getArcadeLabel(args); }else if(type === 'infoTemplate'){ return mo.readExprInfo._getArcadeInfoTemplate(args); }else{ return mo.readExprInfo._getAllArcade(args); } }; //private functions mo.readExprInfo._getArcadeRender = function(args) { return mo.readExprInfo._lookupArcadeRender({layers: mo.readExprInfo._checkPassInLayer(args)}); }; mo.readExprInfo._getArcadeInfoTemplate = function(args) { return mo.readExprInfo._lookupArcadeInfoTemplate({layers: mo.readExprInfo._checkPassInLayer(args)}); }; mo.readExprInfo._getArcadeLabel = function(args) { return mo.readExprInfo._lookupArcadeLabel({layers: mo.readExprInfo._checkPassInLayer(args)}); }; mo.readExprInfo._getAllArcade = function(args) { var render = mo.readExprInfo._getArcadeRender(args); var infoTemplate = mo.readExprInfo._getArcadeInfoTemplate(args); var label = mo.readExprInfo._getArcadeLabel(args); // var arcadeObj = Object.assign(infoTemplate, render, label); // return arcadeObj; return { render: render, infoTemplate: infoTemplate, label: label }; }; mo.readExprInfo._checkPassInLayer = function(args) { var layerList = []; if(typeof(args) !== 'undefined') { if(typeof(args.layer) !== 'undefined') { if((args.layer !== null) && (args.layer !== '')) { //layerList.push(args.layer); layerList = mo.readExprInfo._getAllMapLayers(args); } else { layerList = mo.readExprInfo._getAllMapLayers(); } } else { layerList = mo.readExprInfo._getAllMapLayers(); } } else { layerList = mo.readExprInfo._getAllMapLayers(); } return layerList; }; //This gets all the operational layers and gets the info and places it in a custom data object. mo.readExprInfo._getAllMapLayers = function(args) { var lookupId = ""; var layerList = []; var layerStructure = LayerStructure.getInstance(); // if(typeof(args) !== 'undefined') { if(args && args.layer) { lookupId = args.layer.id; } if(lookupId !== "") { var foundLayer = layerStructure.getNodeById(lookupId); // if(foundLayer !== 'undefined') { if(foundLayer) { layerList.push(foundLayer._layerInfo.layerObject); } } else { //No layer ID passed in, just get all layers. layerStructure.traversal(function(layerNode) { //check to see if type exist and if it's not any tiles if(typeof(layerNode._layerInfo.layerObject.type) !== 'undefined') { if((layerNode._layerInfo.layerObject.type).indexOf("tile") === -1) { layerList.push(layerNode._layerInfo.layerObject); } } else { //It is a grouped Map service layerList.push(layerNode._layerInfo.layerObject); } }); } return layerList; }; mo.readExprInfo._lookupArcadeRender = function(args) { var lyrArcadeExpr = []; if(args.layers.length > 0) { array.forEach(args.layers, lang.hitch(this, function(layer) { var expObj = {}; //Only add if there is a renderer object, maps services do not have this. if(typeof(layer.renderer) !== 'undefined') { var lyrObjRender = layer.renderer; if(typeof(lyrObjRender.valueExpression) !== 'undefined') { expObj.layer = layer.id; expObj.valueExpression = lyrObjRender.valueExpression; expObj.valueExpressionTitle = lyrObjRender.valueExpressionTitle; if(typeof(lyrObjRender.values) !== 'undefined') { expObj.values = lyrObjRender.values; } if(typeof(lyrObjRender.visualVariables) !== 'undefined') { expObj.visualVariables = lyrObjRender.visualVariables; } } if(Object.keys(expObj).length > 1 && expObj.constructor === Object) { lyrArcadeExpr.push(expObj); }else{ //if layer has no arcade expressionInfos lyrArcadeExpr.push({ layer: layer.id, valueExpression: null, valueExpressionTitle: null, values: null, visualVariables: null }); } } })); } return lyrArcadeExpr; }; mo.readExprInfo._lookupArcadeInfoTemplate = function(args) { var lyrArcadeExpr = []; if(args.layers.length > 0) { array.forEach(args.layers, lang.hitch(this, function(layer) { var expObj = {}; //InfoTemplate handling for feature layers if(typeof(layer.infoTemplate) !== 'undefined') { if(typeof(layer.infoTemplate.info.expressionInfos) !== 'undefined') { if(layer.infoTemplate.info.expressionInfos.length > 0) { expObj.layer = layer.id; expObj.expressionInfos = layer.infoTemplate.info.expressionInfos; } } } else { //feature handling for map service if(typeof(layer.infoTemplates) !== 'undefined') { //check if it template is arcade and only store it if it is. //Arcade is stored in the group level, not sublayers for (var key in layer.infoTemplates) { if((layer.infoTemplates).hasOwnProperty(key)) { if(typeof(layer.infoTemplates[key].infoTemplate.info.expressionInfos) !== 'undefined') { var arcadeNode = layer.infoTemplates[key].infoTemplate; if(arcadeNode.info.expressionInfos.length > 0) { expObj.layer = layer.id; expObj.expressionInfos = arcadeNode.info.expressionInfos; } } } } } else { //no infoTemplates defined } } if(Object.keys(expObj).length > 1 && expObj.constructor === Object) { lyrArcadeExpr.push(expObj); }else{ //if layer has no arcade expressionInfos lyrArcadeExpr.push({ layer: layer.id, expressionInfos: null }); } })); } return lyrArcadeExpr; }; mo.readExprInfo._lookupArcadeLabel = function(args) { var lyrArcadeExpr = []; if(args.layers.length > 0) { array.forEach(args.layers, lang.hitch(this, function(layer) { var expObj = {}; var lyrObj = layer; if(typeof(lyrObj.labelingInfo) !== 'undefined') { array.forEach(lyrObj.labelingInfo, lang.hitch(this, function(labelInfo) { //check if 'name' exists, it is present when Arcade is used if(typeof(labelInfo.name) !== 'undefined') { expObj.layer = layer.id; expObj.name = labelInfo.name; expObj.labelingInfo = labelInfo; expObj.expression = labelInfo.labelExpressionInfo.expression; } })); } if(Object.keys(expObj).length > 1 && expObj.constructor === Object) { lyrArcadeExpr.push(expObj); }else{ //if layer has no arcade expressionInfos lyrArcadeExpr.push({ layer: layer.id, name: null, labelingInfo: null, expression: null }); } })); } return lyrArcadeExpr; }; /********************custom arcade expression***********************/ mo.customExpr = {}; /** * Get one graphic's all attributes by custom expressionInfos. * @param {array} expressionInfos, every item is an object, likes: {name,title,expression} * @param {object} graphic, it contains geometry, a symbol, attributes, or an infoTemplate * @return {object} attributes object, including orginal attributes and custom arcade attributes */ mo.customExpr.getAttributesFromCustomArcadeExpr = function (expressionInfos, graphic, layerDefinition) { var parsedExprs = mo.InfoTemplate._parseArcadeExpressions(expressionInfos); var graphicAttrs = mo.InfoTemplate._combineFeatureAttributesAndExpressionResolutions(graphic, parsedExprs, layerDefinition); return graphicAttrs; }; /** * Get Graphics's attributes value list by custom expressionInfos. * @param {array} expressionInfos, every item is an object, likes: {name,title,expression} * @param {array} graphicList, graphic object array. * @return {array} every item in array is an object, it contains the attributes's values by custom expressionInfos. */ mo.customExpr.getAttributesValueFromCustomArcadeExprBatch = function (expressionInfos, graphicList, layerDefinition) { var features = []; for(var key in graphicList){ features.push(graphicList[key]); } var parsedExprs = mo.InfoTemplate._parseArcadeExpressions(expressionInfos); var customExprTemplates = mo.InfoTemplate._getExprNamesArrayFromPopupExprInfos(expressionInfos); customExprTemplates.relationships = null; customExprTemplates.parsedExpressions = parsedExprs; var def = new Deferred(); var labelDef = mo.InfoTemplate._getPopupFieldsValueFromArcadeFeatures(features, customExprTemplates, layerDefinition); labelDef.then(function(customFieldsValueArray){ def.resolve(customFieldsValueArray); }); return def; }; /********************Render***********************/ /********************Label***********************/ /********************InfoTemplate***********************/ mo.InfoTemplate = {}; /** * Get one graphic's all attributes. * @param {operlayer} layer that need to have id, popupInfo, layerObject(has fields). * @param {object} graphic, it contains geometry, a symbol, attributes, or an infoTemplate * @return {object} attributes object, including orginal attributes and arcade attributes format:{'OBJECTID':1, 'expression/expr1':"394-Waterous", 'relationships/0/CHAINS':123} */ mo.InfoTemplate.getAttributesFromInfoTemplate = function (map, operlayer, graphic) { var layerDefinition = {fields: []}; if(operlayer.layerObject && operlayer.layerObject.fields){ layerDefinition.fields = operlayer.layerObject.fields; } var arcadeTemplate = mo.readExprInfo.getArcadeProfilesByType(map, operlayer, 'infoTemplate'); var expressionInfos = arcadeTemplate[0].expressionInfos; if(!expressionInfos){ return graphic.attributes; } var parsedExprs = mo.InfoTemplate._parseArcadeExpressions(expressionInfos); var graphicAttrs = mo.InfoTemplate._combineFeatureAttributesAndExpressionResolutions(graphic, parsedExprs, layerDefinition); return graphicAttrs; }; /** * Get Graphics's attributes value list which configurate in InfoTemplate. * @param {operlayer} layer that need to have id, popupInfo, layerObject(has fields). * @param {array} graphicList, graphic object array. * @return {array} every item in array is an object it contains the attributes's values which configurate in InfoTemplate. format:[["278-Waterous","Waterous---str"],["279-Clow Corporation","Clow Corporation---str"]] */ mo.InfoTemplate.getAttributesValueFromInfoTemplateBatch = function (map, operlayer, graphicList) { var layerDefinition = {fields: []}; if(operlayer.layerObject && operlayer.layerObject.fields){ layerDefinition.fields = operlayer.layerObject.fields; } var features = []; for(var key in graphicList){ features.push(graphicList[key]); } var arcadeTemplate = mo.readExprInfo.getArcadeProfilesByType(map, operlayer, 'infoTemplate'); var expressionInfos = arcadeTemplate[0].expressionInfos; var def = new Deferred(); if(!expressionInfos){//layer has no arcade infotemplate def.resolve([]); return def; } var parsedExprs = mo.InfoTemplate._parseArcadeExpressions(expressionInfos); var popupExprTemplates = null; var popupDesc = operlayer.popupInfo.description; //from popupDesc if(popupDesc !== null){ popupExprTemplates = mo.InfoTemplate._convertPopupDescToFieldNamesArray(popupDesc); popupExprTemplates.relationships = mo.InfoTemplate._getRelationshipQueries(operlayer); }else{ //from expressionInfos popupExprTemplates = mo.InfoTemplate._getExprNamesArrayFromPopupExprInfos(expressionInfos); popupExprTemplates.relationships = null; //doesn't support relationships in this case. } //add parsedExpr popupExprTemplates.parsedExpressions = parsedExprs; var labelDef = mo.InfoTemplate._getPopupFieldsValueFromArcadeFeatures(features, popupExprTemplates, layerDefinition); labelDef.then(function(popupFieldsValueArray){ def.resolve(popupFieldsValueArray); }); return def; }; // Get relationshipQueries from a layer. mo.InfoTemplate._getRelationshipQueries = function (operLayer) { var hasRelationships = false, relationships = {}, relationshipFieldPattern = /\{relationships\/\d+\//gm, relationshipIdPattern = /\d+/, matches; matches = operLayer.popupInfo.description.match(relationshipFieldPattern); if (matches) { hasRelationships = true; array.forEach(matches, function (match) { var relatedQuery, id = match.match(relationshipIdPattern)[0]; if (!relationships.hasOwnProperty(id)) { relatedQuery = new RelationshipQuery(); relatedQuery.outFields = ['*']; relatedQuery.relationshipId = id; relatedQuery.returnGeometry = false; relationships[id] = { operLayer: operLayer, relatedQuery: relatedQuery }; } }); } return hasRelationships? relationships : null; }; /** * Converts the arcade exprs obj of a custom popup into a multiline label specification; * @param {array} popupExprInfos, an arry of objects by customing popup arcade exprs in configuration. every object, likes: {name,title,expression} * @return {array} an array of exprs's name, format: ["${expression/expr1}"] */ mo.InfoTemplate._getExprNamesArrayFromPopupExprInfos = function (popupExprInfos) { var exprNames = []; for(var key in popupExprInfos){ var exprName = popupExprInfos[key].name; exprNames.push('${expression/' + exprName + '}'); } return exprNames; }; /** * Converts the text of a custom popup into an array of fields's name. * conversion splits text into lines on
s, * removes HTML tags, and changes field tags from popup style to string.substitute form. * @param {string} popupDesc, popup's descriptions. * format: "{OBJECTID}
{relationships/0/Creator}
{expression/expr0}
" * @return {array} an array of fields' name * format: ["${OBJECTID}","${relationships/0/CreationDate}","${expression/expr0}"] */ mo.InfoTemplate._convertPopupDescToFieldNamesArray = function (popupDesc) { // e.g., Occupant
{FULLADDR}
{MUNICIPALITY}, IL {PSTLZIP5} // Sanitize the description after converting
s and

s to line breaks var descEOLs1 = mo.InfoTemplate._convertEndParasToEOLs(popupDesc); var descEOLs2 = mo.InfoTemplate._convertBreaksToEOLs(descEOLs1); var desc = mo.InfoTemplate._sanitizeNoTags(descEOLs2).trim(); // Change the open brace used for attribute names to "${" so that we can use dojo/string's substitute() desc = desc.replace(/\{/gi, '${'); // Remove non-breaking spaces, which aren't rendered correctly in CSV output and aren't needed in PDF output desc = desc.replace(/\u00a0/gi, ' '); // Split on the line breaks desc = desc.split('\n'); // Trim each line, since HTML pretty much does that desc = array.map(desc, function (line) { return line.trim(); }); return desc; }; /** * Removes HTML tags from string. * @param {string} taggedText String to clean * @return {string} Cleaned string */ mo.InfoTemplate._sanitizeNoTags = function (taggedText) { var cleanedText, tagBody, tagOrComment; cleanedText = jimuUtils.sanitizeHTML(taggedText); // Remove remaining tags // From Stack Overflow https://stackoverflow.com/a/430240/5090610 // CC BY-SA 3.0 (https://creativecommons.org/licenses/by-sa/3.0/) // Mike Samuel (https://stackoverflow.com/users/20394/mike-samuel) tagBody = '(?:[^"\'>]|"[^"]*"|\'[^\']*\')*'; tagOrComment = new RegExp( '<(?:' + // Comment body. '!--(?:(?:-*[^->])*--+|-?)' + // Special "raw text" elements whose content should be elided. '|script\\b' + tagBody + '>[\\s\\S]*?[\\s\\S]*?', 'gi'); function removeTags(html) { var oldHtml; do { oldHtml = html; html = html.replace(tagOrComment, ''); } while (html !== oldHtml); return html.replace(//" to match popup attributes = feature.attributes; prefix = 'relationships/' + relationshipId + '/'; mo.InfoTemplate._objEach(relatedFeature.attributes, function (value, key) { newKey = prefix + key; attributes[newKey] = value; }); // Substitute attribute values and resolved Arcade expressions into label lines array.forEach(popupExprTemplates, function (labelLineRule) { labelLines.push(string.substitute(labelLineRule, mo.InfoTemplate._combineFeatureAttributesAndExpressionResolutions( feature, popupExprTemplates.parsedExpressions, layerDefinition), mo.InfoTemplate._useEmptyStringForNull)); }); labels.push(labelLines); }); } promise.resolve(labels); }); }); }); all(promises).then(function (results) { // Look at the related records for each found address array.forEach(results, function (labels) { // For each found address, save its labels array.forEach(labels, function (labelLines) { ++relatedRecordsFound; content.push(labelLines); }); }); console.log(relatedRecordsFound + ' related address features found'); deferred.resolve(content); }); }else { array.forEach(features, function (feature) { var labelLines = []; // Substitute attribute values and resolved Arcade expressions into label lines array.forEach(popupExprTemplates, function (labelLineRule) { labelLines.push(string.substitute(labelLineRule, mo.InfoTemplate._combineFeatureAttributesAndExpressionResolutions( feature, popupExprTemplates.parsedExpressions, layerDefinition), mo.InfoTemplate._useEmptyStringForNull)); }); content.push(labelLines); }); deferred.resolve(content); } return deferred; }; /** * Parses Arcade expression infos. * @param {array} expressionInfos Expression info list from popupInfo * @return {object} List of parsed expressions keyed by the expression name */ mo.InfoTemplate._parseArcadeExpressions = function (expressionInfos) { var parsedExpressions; if (Array.isArray(expressionInfos) && expressionInfos.length > 0) { parsedExpressions = {}; array.forEach(expressionInfos, function (info) { parsedExpressions[info.name] = Arcade.parseScript(info.expression); }); } return parsedExpressions; }; /** * Initializes the Arcade context with an ArcadeFeature. * @param {object} feature Feature to convert into an ArcadeFeature and to add to the context's vars object * @return {object} Arcade context object */ mo.InfoTemplate._initArcadeContext = function (feature, layerDefinition) { var context = { vars: {} }; context.vars.$feature = ArcadeFeature.createFromGraphicLikeObject(feature.geometry, feature.attributes, layerDefinition); return context; }; /** * Creates a list of feature attributes and resolved Arcade expressions for the feature. * @param {object} feature Feature whose attributes are to be used * @param {object} parsedExpressions Parsed Arcade expressions keyed by the expression name; each expression * is resolved and added to the output object keyed by 'expression/' + the expression's name * @return {object} Combination of feature attributes and resolved expressions */ mo.InfoTemplate._combineFeatureAttributesAndExpressionResolutions = function (feature, parsedExpressions, layerDefinition) { var attributesAndExpressionResolutions, arcadeContext, name, resolvedExpression; // Resolve Arcade expressions for this feature; convert to string in case its return type is not already string attributesAndExpressionResolutions = lang.mixin({}, feature.attributes); if (parsedExpressions) { arcadeContext = mo.InfoTemplate._initArcadeContext(feature, layerDefinition); for (name in parsedExpressions) { resolvedExpression = Arcade.executeScript(parsedExpressions[name], arcadeContext); attributesAndExpressionResolutions['expression/' + name] = resolvedExpression ? resolvedExpression.toString() : ''; } } return attributesAndExpressionResolutions; }; /** * Converts

s into CRLFs. * @param {string} html Text to scan * @return {string} Converted text */ mo.InfoTemplate._convertEndParasToEOLs = function (html) { var html2 = html, matches = html.match(/<\/p>/gi); array.forEach(matches, function (match) { html2 = html2.replace(match, '\n'); }); return html2; }; /** * Converts
s into CRLFs. * @param {string} html Text to scan * @return {string} Converted text */ mo.InfoTemplate._convertBreaksToEOLs = function (html) { var html2 = html, matches = html.match(//gi); array.forEach(matches, function (match) { html2 = html2.replace(match, '\n'); }); return html2; }; /** * Insures that a string is not undefined or null. * @param {string} str String to check * @return {string} str, or '' if str is undefined, null, or empty */ mo.InfoTemplate._useEmptyStringForNull = function (str) { return str ? str : ''; }; /** * Interates over the items in an associative array (forIn). * @param {object} obj Object to interate over * @param {function} fcn Function to call for each item * @param {object} scope Scope to apply to function call */ mo.InfoTemplate._objEach = function (obj, fcn, scope){ var key; for (key in obj){ if(obj.hasOwnProperty(key)){ fcn.call(scope, obj[key], key); } } }; return mo; });