layer/layerRec/featureRecord.js

  1. 'use strict';
  2. const attribFC = require('./attribFC.js')();
  3. const attribRecord = require('./attribRecord.js')();
  4. const layerInterface = require('./layerInterface.js')();
  5. const shared = require('./shared.js')();
  6. /**
  7. * @class FeatureRecord
  8. */
  9. class FeatureRecord extends attribRecord.AttribRecord {
  10. // TODO add flags for file based layers?
  11. /**
  12. * Create a layer record with the appropriate geoApi layer type. Layer config
  13. * should be fully merged with all layer options defined (i.e. this constructor
  14. * will not apply any defaults).
  15. * @param {Object} layerClass the ESRI api object for feature layers
  16. * @param {Object} esriRequest the ESRI api object for making web requests with proxy support
  17. * @param {Object} apiRef object pointing to the geoApi. allows us to call other geoApi functions.
  18. * @param {Object} config layer config values
  19. * @param {Object} esriLayer an optional pre-constructed layer
  20. * @param {Function} epsgLookup an optional lookup function for EPSG codes (see geoService for signature)
  21. */
  22. constructor (layerClass, esriRequest, apiRef, config, esriLayer, epsgLookup) {
  23. // TEST STATUS basic
  24. // TODO if we have nothing to add here, delete this constructor
  25. // TODO might need to add a placeholder here with stuff like
  26. // this._defaultFC = '0';
  27. // this._featClasses['0'] = placeholder;
  28. super(layerClass, esriRequest, apiRef, config, esriLayer, epsgLookup);
  29. this._geometryType = undefined;
  30. this._fcount = undefined;
  31. }
  32. // TODO ensure whoever is making layers from config fragments is also setting the feature index.
  33. // remove comment once that is done
  34. makeLayerConfig () {
  35. // TEST STATUS basic
  36. const cfg = super.makeLayerConfig();
  37. cfg.mode = this.config.state.snapshot ? this._layerClass.MODE_SNAPSHOT
  38. : this._layerClass.MODE_ONDEMAND;
  39. // TODO confirm this logic. old code mapped .options.snapshot.value to the button -- meaning if we were in snapshot mode,
  40. // we would want the button disabled. in the refactor, the button may get it's enabled/disabled from a different source.
  41. // this.config.state.snapshot = !this.config.state.snapshot;
  42. this._snapshot = this.config.state.snapshot;
  43. return cfg;
  44. }
  45. getGeomType () {
  46. // TEST STATUS none
  47. // standard case, layer has no geometry. This gets overridden in feature-based Record classes.
  48. return this._geometryType;
  49. }
  50. // returns the proxy interface object for the root of the layer (i.e. main entry in legend, not nested child things)
  51. // TODO docs
  52. getProxy () {
  53. // TEST STATUS basic
  54. // TODO figure out control name arrays from config (specifically disabled stuff)
  55. // updated config schema uses term "enabled" but have a feeling it really means available
  56. // TODO figure out how placeholders work with all this
  57. if (!this._rootProxy) {
  58. this._rootProxy = new layerInterface.LayerInterface(this, this.initialConfig.controls);
  59. this._rootProxy.convertToFeatureLayer(this);
  60. }
  61. return this._rootProxy;
  62. }
  63. /**
  64. * Triggers when the layer loads.
  65. *
  66. * @function onLoad
  67. */
  68. onLoad () {
  69. // TEST STATUS basic
  70. super.onLoad();
  71. // set up attributes, set up children bundles.
  72. const attributeBundle = this._apiRef.attribs.loadLayerAttribs(this._layer);
  73. // feature has only one layer
  74. const idx = attributeBundle.indexes[0];
  75. const aFC = new attribFC.AttribFC(this, idx, attributeBundle[idx], this.config);
  76. aFC.nameField = this.config.nameField;
  77. this._defaultFC = idx;
  78. this._featClasses[idx] = aFC;
  79. this.getSymbology().then(symbolArray => {
  80. // remove anything from the stack, then add new symbols to the stack
  81. this.symbology.stack.splice(0, this.symbology.stack.length, ...symbolArray);
  82. });
  83. // update asynch data
  84. aFC.getLayerData().then(ld => {
  85. this._geometryType = ld.geometryType;
  86. });
  87. this.getFeatureCount().then(fc => {
  88. this._fcount = fc;
  89. });
  90. }
  91. getFeatureCount () {
  92. // TEST STATUS basic
  93. // just use the layer url (or lack of in case of file layer)
  94. return super.getFeatureCount(this._layer.url);
  95. }
  96. isFileLayer () {
  97. // TEST STATUS none
  98. // TODO revisit. is it robust enough?
  99. return this._layer && this._layer.url === '';
  100. }
  101. // TODO determine who is setting this. if we have an internal
  102. // snapshot process, it might become a read-only property
  103. get isSnapshot () { return this._snapshot; }
  104. set isSnapshot (value) { this._snapshot = value; }
  105. get layerType () { return shared.clientLayerType.ESRI_FEATURE; }
  106. get featureCount () { return this._fcount; }
  107. onMouseOver (e) {
  108. // TEST STATUS none
  109. if (this._hoverListeners.length > 0) {
  110. // TODO add in quick lookup for layers that dont have attributes loaded yet
  111. const showBundle = {
  112. type: 'mouseOver',
  113. point: e.screenPoint,
  114. target: e.target
  115. };
  116. // tell anyone listening we moused into something
  117. this._fireEvent(this._hoverListeners, showBundle);
  118. // pull metadata for this layer.
  119. this.getLayerData().then(lInfo => {
  120. // TODO this will change a bit after we add in quick lookup. for now, get all attribs
  121. return Promise.all([Promise.resolve(lInfo), this.getAttribs()]);
  122. }).then(([lInfo, aInfo]) => {
  123. // graphic attributes will only have the OID if layer is server based
  124. const oid = e.graphic.attributes[lInfo.oidField];
  125. // get name via attribs and name field
  126. const featAttribs = aInfo.features[aInfo.oidIndex[oid]].attributes;
  127. const featName = this.getFeatureName(oid, featAttribs);
  128. // get icon via renderer and geoApi call
  129. const svgcode = this._apiRef.symbology.getGraphicIcon(featAttribs, lInfo.renderer);
  130. // duplicate the position so listener can verify this event is same as mouseOver event above
  131. const loadBundle = {
  132. type: 'tipLoaded',
  133. name: featName,
  134. target: e.target,
  135. svgcode
  136. };
  137. // tell anyone listening we moused into something
  138. this._fireEvent(this._hoverListeners, loadBundle);
  139. });
  140. }
  141. }
  142. onMouseOut (e) {
  143. // TEST STATUS none
  144. // tell anyone listening we moused out
  145. const outBundle = {
  146. type: 'mouseOut',
  147. target: e.target
  148. };
  149. this._fireEvent(this._hoverListeners, outBundle);
  150. }
  151. /**
  152. * Run a query on a feature layer, return the result as a promise. Fills the panelData array on resolution. // TODO update
  153. * @function identify
  154. * @param {Object} opts additional argumets like map object, clickEvent, etc.
  155. * @returns {Object} an object with identify results array and identify promise resolving when identify is complete; if an empty object is returned, it will be skipped
  156. */
  157. identify (opts) {
  158. // TEST STATUS none
  159. // TODO add full documentation for options parameter
  160. // TODO fix these params
  161. // TODO legendEntry.name, legendEntry.symbology appear to be fast links to populate the left side of the results
  162. // view. perhaps it should not be in this object anymore?
  163. // TODO see how the client is consuming the internal pointer to layerRecord. this may also now be
  164. // directly available via the legend object.
  165. const identifyResult =
  166. new shared.IdentifyResult('legendEntry.name', 'legendEntry.symbology', 'EsriFeature',
  167. this, this._defaultFC);
  168. // run a spatial query
  169. const qry = new this._apiRef.layer.Query();
  170. qry.outFields = ['*']; // this will result in just objectid fields, as that is all we have in feature layers
  171. // more accurate results without making the buffer if we're dealing with extents
  172. // polygons from added file need buffer
  173. // TODO further investigate why esri is requiring buffer for file-based polygons. logic says it shouldnt
  174. if (this._layer.geometryType === 'esriGeometryPolygon' && !this.isFileLayer()) {
  175. qry.geometry = opts.geometry;
  176. } else {
  177. qry.geometry = this.makeClickBuffer(opts.clickEvent.mapPoint, opts.map, this.clickTolerance);
  178. }
  179. const identifyPromise = Promise.all([
  180. this.getAttributes(),
  181. Promise.resolve(this._layer.queryFeatures(qry)),
  182. this.getLayerData()
  183. ])
  184. .then(([attributes, queryResult, layerData]) => {
  185. // transform attributes of query results into {name,data} objects one object per queried feature
  186. //
  187. // each feature will have its attributes converted into a table
  188. // placeholder for now until we figure out how to signal the panel that
  189. // we want to make a nice table
  190. identifyResult.isLoading = false;
  191. identifyResult.data = queryResult.features.map(
  192. feat => {
  193. // grab the object id of the feature we clicked on.
  194. const objId = feat.attributes[attributes.oidField];
  195. const objIdStr = objId.toString();
  196. // use object id find location of our feature in the feature array, and grab its attributes
  197. const featAttribs = attributes.features[attributes.oidIndex[objIdStr]];
  198. return {
  199. name: this.getFeatureName(objIdStr, featAttribs),
  200. data: this.attributesToDetails(featAttribs, layerData.fields),
  201. oid: objId,
  202. symbology: [
  203. { svgcode: this._apiRef.symbology.getGraphicIcon(featAttribs, layerData.renderer) }
  204. ]
  205. };
  206. });
  207. });
  208. return { identifyResults: [identifyResult], identifyPromise };
  209. }
  210. }
  211. module.exports = () => ({
  212. FeatureRecord
  213. });