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