layer/layerRec/attribFC.js

  1. 'use strict';
  2. const shared = require('./shared.js')();
  3. const basicFC = require('./basicFC.js')();
  4. /**
  5. * @class AttribFC
  6. */
  7. class AttribFC extends basicFC.BasicFC {
  8. // attribute-specific variant for feature class object.
  9. // deals with stuff specific to a feature class that has attributes
  10. /**
  11. * Create an attribute specific feature class object
  12. * @param {Object} parent the Record object that this Feature Class belongs to
  13. * @param {String} idx the service index of this Feature Class. an integer in string format. use '0' for non-indexed sources.
  14. * @param {Object} layerPackage a layer package object from the attribute module for this feature class
  15. * @param {Object} config the config object for this sublayer
  16. */
  17. constructor (parent, idx, layerPackage, config) {
  18. super(parent, idx, config);
  19. this._layerPackage = layerPackage;
  20. this._geometryType = undefined; // this indicates unknown to the ui.
  21. this._fcount = undefined;
  22. // moar?
  23. }
  24. /**
  25. * Returns attribute data for this FC.
  26. *
  27. * @function getAttribs
  28. * @returns {Promise} resolves with a layer attribute data object
  29. */
  30. getAttribs () {
  31. return this._layerPackage.getAttribs();
  32. }
  33. /**
  34. * Returns layer-specific data for this FC.
  35. *
  36. * @function getLayerData
  37. * @returns {Promise} resolves with a layer data object
  38. */
  39. getLayerData () {
  40. return this._layerPackage.layerData;
  41. }
  42. // this will actively download / refresh the internal symbology
  43. loadSymbology () {
  44. return this.getLayerData().then(lData => {
  45. if (lData.layerType === 'Feature Layer') {
  46. // feature always has a single item, so index 0
  47. this.symbology = shared.makeSymbologyArray(lData.legend.layers[0].legend);
  48. } else {
  49. // non-feature source. use legend server
  50. return super.loadSymbology();
  51. }
  52. });
  53. }
  54. /**
  55. * Extract the feature name from a feature as best we can.
  56. *
  57. * @function getFeatureName
  58. * @param {String} objId the object id of the attribute
  59. * @param {Object} attribs the dictionary of attributes for the feature.
  60. * @returns {String} the name of the feature
  61. */
  62. getFeatureName (objId, attribs) {
  63. // TODO revisit the objId parameter. Do we actually need this fallback anymore?
  64. // NOTE: we used to have fallback logic here that would use layer settings
  65. // if this.nameField had no value. Logic has changed to now push
  66. // layer settings to this.nameField during the load event of the
  67. // Record.
  68. if (this.nameField && attribs) {
  69. // extract name
  70. return attribs[this.nameField];
  71. } else {
  72. // FIXME wire in "feature" to translation service
  73. return 'Feature ' + objId;
  74. }
  75. }
  76. /**
  77. * Retrieves attributes from a layer for a specified feature index
  78. * @return {Promise} promise resolving with formatted attributes to be consumed by the datagrid and esri feature identify
  79. */
  80. getFormattedAttributes () {
  81. if (this._formattedAttributes) {
  82. return this._formattedAttributes;
  83. }
  84. // TODO after refactor, consider changing this to a warning and just return some dummy value
  85. if (this.layerType === shared.clientLayerType.ESRI_RASTER) {
  86. throw new Error('Attempting to get attributes on a raster layer.');
  87. }
  88. this._formattedAttributes = Promise.all([this.getAttribs(), this.getLayerData()])
  89. .then(([aData, lData]) => {
  90. // create columns array consumable by datables
  91. const columns = lData.fields
  92. .filter(field =>
  93. // assuming there is at least one attribute - empty attribute budnle promises should be rejected, so it never even gets this far
  94. // filter out fields where there is no corresponding attribute data
  95. aData.features[0].attributes.hasOwnProperty(field.name))
  96. .map(field => ({
  97. data: field.name,
  98. title: field.alias || field.name
  99. }));
  100. return {
  101. columns,
  102. rows: aData.features.map(feature => feature.attributes),
  103. fields: lData.fields, // keep fields for reference ...
  104. oidField: lData.oidField, // ... keep a reference to id field ...
  105. oidIndex: aData.oidIndex, // ... and keep id mapping array
  106. renderer: lData.renderer
  107. };
  108. })
  109. .catch(() => {
  110. delete this._formattedAttributes; // delete cached promise when the geoApi `getAttribs` call fails, so it will be requested again next time `getAttributes` is called;
  111. throw new Error('Attrib loading failed');
  112. });
  113. return this._formattedAttributes;
  114. }
  115. /**
  116. * Check to see if the attribute in question is an esriFieldTypeDate type.
  117. *
  118. * @param {String} attribName the attribute name we want to check if it's a date or not
  119. * @return {Promise} resolves to true or false based on the attribName type being esriFieldTypeDate
  120. */
  121. checkDateType (attribName) {
  122. // TEST STATUS none
  123. // grab attribute info (waiting for it it finish loading)
  124. return this.getLayerData().then(lData => {
  125. // inspect attribute fields
  126. if (lData.fields) {
  127. const attribField = lData.fields.find(field => {
  128. return field.name === attribName;
  129. });
  130. if (attribField && attribField.type) {
  131. return attribField.type === 'esriFieldTypeDate';
  132. }
  133. }
  134. return false;
  135. });
  136. }
  137. /**
  138. * Get the best user-friendly name of a field. Uses alias if alias is defined, else uses the system attribute name.
  139. *
  140. * @param {String} attribName the attribute name we want a nice name for
  141. * @return {Promise} resolves to the best available user friendly attribute name
  142. */
  143. aliasedFieldName (attribName) {
  144. // TEST STATUS none
  145. // grab attribute info (waiting for it it finish loading)
  146. return this.getLayerData().then(lData => {
  147. return AttribFC.aliasedFieldNameDirect(attribName, lData.fields);
  148. });
  149. }
  150. static aliasedFieldNameDirect (attribName, fields) {
  151. // TEST STATUS none
  152. let fName = attribName;
  153. // search for aliases
  154. if (fields) {
  155. const attribField = fields.find(field => {
  156. return field.name === attribName;
  157. });
  158. if (attribField && attribField.alias && attribField.alias.length > 0) {
  159. fName = attribField.alias;
  160. }
  161. }
  162. return fName;
  163. }
  164. /**
  165. * Convert an attribute set so that any keys using aliases are converted to proper fields
  166. *
  167. * @param {Object} attribs attribute key-value mapping, potentially with aliases as keys
  168. * @param {Array} fields fields definition array for layer
  169. * @return {Object} attribute key-value mapping with fields as keys
  170. */
  171. static unAliasAttribs (attribs, fields) {
  172. const newA = {};
  173. fields.forEach(field => {
  174. // attempt to extract on name. if not found, attempt to extract on alias
  175. // dump value into the result
  176. newA[field.name] = attribs.hasOwnProperty(field.name) ? attribs[field.name] : attribs[field.alias];
  177. });
  178. return newA;
  179. }
  180. // TODO perhaps a splitting of server url and layer index to make things consistent between feature and dynamic?
  181. // could be on constructor, then parent can easily feed in the treats.
  182. }
  183. module.exports = () => ({
  184. AttribFC
  185. });