layer/layerRec/featureRecord.js

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