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. /**
  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. super(layerClass, esriRequest, apiRef, config, esriLayer, epsgLookup);
  24. // handles placeholder symbol, possibly other things
  25. // if we were passed a pre-loaded layer, we skip this (it will run after the load triggers
  26. // in the super-constructor, thus overwriting our good results)
  27. if (!esriLayer) {
  28. this._defaultFC = '0';
  29. this._featClasses['0'] = new placeholderFC.PlaceholderFC(this, this.name);
  30. this._fcount = undefined;
  31. }
  32. }
  33. get queryUrl () { return `${this.rootUrl}/${this._defaultFC}`; }
  34. /**
  35. * Creates an options object for the map API object
  36. *
  37. * @function makeLayerConfig
  38. * @returns {Object} an object with api options
  39. */
  40. makeLayerConfig () {
  41. const cfg = super.makeLayerConfig();
  42. cfg.mode = this.config.state.snapshot ? this._layerClass.MODE_SNAPSHOT
  43. : this._layerClass.MODE_ONDEMAND;
  44. // if we have a definition at load, apply it here to avoid cancellation errors on
  45. if (this.config.initialFilteredQuery) {
  46. cfg.definitionExpression = this.config.initialFilteredQuery;
  47. }
  48. // TODO confirm this logic. old code mapped .options.snapshot.value to the button -- meaning if we were in snapshot mode,
  49. // we would want the button disabled. in the refactor, the button may get it's enabled/disabled from a different source.
  50. // this.config.state.snapshot = !this.config.state.snapshot;
  51. this._snapshot = this.config.state.snapshot;
  52. return cfg;
  53. }
  54. /**
  55. * Indicates the geometry type of the layer.
  56. *
  57. * @function getGeomType
  58. * @returns {String} the geometry type of the layer
  59. */
  60. getGeomType () {
  61. return this._featClasses[this._defaultFC].geomType;
  62. }
  63. /**
  64. * Provides the proxy interface object to the layer.
  65. *
  66. * @function getProxy
  67. * @returns {Object} the proxy interface for the layer
  68. */
  69. getProxy () {
  70. if (!this._rootProxy) {
  71. this._rootProxy = new layerInterface.LayerInterface(this, this.initialConfig.controls);
  72. this._rootProxy.convertToFeatureLayer(this);
  73. }
  74. return this._rootProxy;
  75. }
  76. /**
  77. * Triggers when the layer loads.
  78. *
  79. * @function onLoad
  80. */
  81. onLoad () {
  82. const loadPromises = super.onLoad();
  83. // get attribute package
  84. let attribPackage;
  85. let featIdx;
  86. if (this.isFileLayer()) {
  87. featIdx = '0';
  88. attribPackage = this._apiRef.attribs.loadFileAttribs(this._layer);
  89. } else {
  90. const splitUrl = shared.parseUrlIndex(this._layer.url);
  91. featIdx = splitUrl.index;
  92. this.rootUrl = splitUrl.rootUrl;
  93. attribPackage = this._apiRef.attribs.loadServerAttribs(splitUrl.rootUrl, featIdx, this.config.outfields);
  94. }
  95. // feature has only one layer
  96. const aFC = new attribFC.AttribFC(this, featIdx, attribPackage, this.config);
  97. this._defaultFC = featIdx;
  98. this._featClasses[featIdx] = aFC;
  99. const pLS = aFC.loadSymbology();
  100. // update asynch data
  101. const pLD = aFC.getLayerData().then(ld => {
  102. aFC.geomType = ld.geometryType;
  103. aFC.nameField = this.config.nameField || ld.nameField || '';
  104. // trickery. file layer can have field names that are bad keys.
  105. // our file loader will have corrected them, but config.nameField will have
  106. // been supplied from the wizard (it pre-fetches fields to present a choice
  107. // to the user). If the nameField was adjusted for bad characters, we need to
  108. // re-synchronize it here.
  109. if (this.isFileLayer() && ld.fields.findIndex(f => f.name === aFC.nameField) === -1) {
  110. const validField = ld.fields.find(f => f.alias === aFC.nameField);
  111. if (validField) {
  112. aFC.nameField = validField.name;
  113. } else {
  114. // give warning. impact is tooltips will have no text, details pane no header
  115. console.warn(`Cannot find name field in layer field list: ${aFC.nameField}`);
  116. }
  117. }
  118. });
  119. const pFC = this.getFeatureCount().then(fc => {
  120. this._fcount = fc;
  121. });
  122. // if file based (or server extent was fried), calculate extent based on geometry
  123. if (!this.extent || !this.extent.xmin) {
  124. this.extent = this._apiRef.proj.graphicsUtils.graphicsExtent(this._layer.graphics);
  125. }
  126. loadPromises.push(pLD, pFC, pLS);
  127. Promise.all(loadPromises).then(() => {
  128. this._stateChange(shared.states.LOADED);
  129. });
  130. }
  131. /**
  132. * Get feature count of this layer.
  133. *
  134. * @function getFeatureCount
  135. * @return {Promise} resolves with an integer indicating the feature count.
  136. */
  137. getFeatureCount () {
  138. // just use the layer url (or lack of in case of file layer)
  139. return super.getFeatureCount(this._layer.url);
  140. }
  141. /**
  142. * Indicates if the layer is file based.
  143. *
  144. * @function isFileLayer
  145. * @returns {Boolean} true if layer is file based
  146. */
  147. isFileLayer () {
  148. // TODO revisit. is it robust enough?
  149. return this._layer && !this._layer.url;
  150. }
  151. /**
  152. * Attempts to abort an attribute load in progress.
  153. * Harmless to call before or after an attribute load.
  154. *
  155. * @function abortAttribLoad
  156. */
  157. abortAttribLoad () {
  158. this._featClasses[this._defaultFC].abortAttribLoad();
  159. }
  160. // TODO determine who is setting this. if we have an internal
  161. // snapshot process, it might become a read-only property
  162. get isSnapshot () { return this._snapshot; }
  163. set isSnapshot (value) { this._snapshot = value; }
  164. get layerType () { return shared.clientLayerType.ESRI_FEATURE; }
  165. get featureCount () { return this._fcount; }
  166. get loadedFeatureCount () { return this._featClasses[this._defaultFC].loadedFeatureCount; }
  167. /**
  168. * Triggers when the mouse enters a feature of the layer.
  169. *
  170. * @function onMouseOver
  171. * @param {Object} standard mouse event object
  172. */
  173. onMouseOver (e) {
  174. if (this._hoverListeners.length > 0) {
  175. const showBundle = {
  176. type: 'mouseOver',
  177. point: e.screenPoint,
  178. target: e.target
  179. };
  180. // tell anyone listening we moused into something
  181. this._fireEvent(this._hoverListeners, showBundle);
  182. // pull metadata for this layer.
  183. let oid;
  184. this.getLayerData().then(lInfo => {
  185. // graphic attributes will only have the OID if layer is server based
  186. oid = e.graphic.attributes[lInfo.oidField];
  187. const graphicPromise = this.fetchGraphic(oid, { attribs: true });
  188. return Promise.all([Promise.resolve(lInfo), graphicPromise]);
  189. }).then(([lInfo, graphicBundle]) => {
  190. const featAttribs = graphicBundle.graphic.attributes;
  191. // get icon via renderer and geoApi call
  192. const svgcode = this._apiRef.symbology.getGraphicIcon(featAttribs, lInfo.renderer);
  193. // duplicate the position so listener can verify this event is same as mouseOver event above
  194. const loadBundle = {
  195. type: 'tipLoaded',
  196. name: this.getFeatureName(oid, featAttribs),
  197. target: e.target,
  198. svgcode
  199. };
  200. // tell anyone listening we moused into something
  201. this._fireEvent(this._hoverListeners, loadBundle);
  202. });
  203. }
  204. }
  205. /**
  206. * Triggers when the mouse leaves a feature of the layer.
  207. *
  208. * @function onMouseOut
  209. * @param {Object} standard mouse event object
  210. */
  211. onMouseOut (e) {
  212. // tell anyone listening we moused out
  213. const outBundle = {
  214. type: 'mouseOut',
  215. target: e.target
  216. };
  217. this._fireEvent(this._hoverListeners, outBundle);
  218. }
  219. /**
  220. * Run a query on a feature layer, return the result as a promise.
  221. * Options:
  222. * - clickEvent {Object} an event object from the mouse click event, where the user wants to identify.
  223. * - map {Object} map object. A geoApi wrapper, such as esriMap, not an actual esri api map
  224. * - geometry {Object} geometry (in map coordinates) to identify against
  225. * - tolerance {Integer} an optional click tolerance for the identify
  226. *
  227. * @function identify
  228. * @param {Object} opts additional arguemets, see above.
  229. * @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
  230. */
  231. identify (opts) {
  232. // TODO add full documentation for options parameter
  233. // early kickout check. not loaded/error; not visible; not queryable; off scale
  234. if (!shared.layerLoaded(this.state) ||
  235. !this.visibility ||
  236. !this.isQueryable() ||
  237. this.isOffScale(opts.map.getScale()).offScale) {
  238. // TODO verifiy this is correct result format if layer should be excluded from the identify process
  239. return { identifyResults: [], identifyPromise: Promise.resolve() };
  240. }
  241. const identifyResult = new shared.IdentifyResult(this.getProxy());
  242. const tolerance = opts.tolerance || this.clickTolerance;
  243. // run a spatial query
  244. const qry = new this._apiRef.layer.Query();
  245. qry.outFields = ['*']; // this will result in just objectid fields, as that is all we have in feature layers
  246. // more accurate results without making the buffer if we're dealing with extents
  247. // polygons from added file need buffer
  248. // TODO further investigate why esri is requiring buffer for file-based polygons. logic says it shouldnt
  249. if (this.getGeomType() === 'esriGeometryPolygon' && !this.isFileLayer()) {
  250. qry.geometry = opts.geometry;
  251. } else {
  252. // TODO investigate why we are using opts.clickEvent.mapPoint and not opts.geometry
  253. qry.geometry = this.makeClickBuffer(opts.clickEvent.mapPoint, opts.map, tolerance);
  254. }
  255. const identifyPromise = Promise.all([
  256. this.getAttribs(),
  257. Promise.resolve(this._layer.queryFeatures(qry)),
  258. this.getLayerData()
  259. ])
  260. .then(([attributes, queryResult, layerData]) => {
  261. // transform attributes of query results into {name,data} objects one object per queried feature
  262. //
  263. // each feature will have its attributes converted into a table
  264. // placeholder for now until we figure out how to signal the panel that
  265. // we want to make a nice table
  266. identifyResult.isLoading = false;
  267. identifyResult.data = queryResult.features.map(
  268. feat => {
  269. // grab the object id of the feature we clicked on.
  270. const objId = feat.attributes[layerData.oidField];
  271. const objIdStr = objId.toString();
  272. // use object id find location of our feature in the feature array, and grab its attributes
  273. const featAttribs = attributes.features[attributes.oidIndex[objIdStr]].attributes;
  274. return {
  275. name: this.getFeatureName(objIdStr, featAttribs),
  276. data: this.attributesToDetails(featAttribs, layerData.fields),
  277. oid: objId,
  278. symbology: [
  279. { svgcode: this._apiRef.symbology.getGraphicIcon(featAttribs, layerData.renderer) }
  280. ]
  281. };
  282. });
  283. });
  284. return { identifyResults: [identifyResult], identifyPromise };
  285. }
  286. /**
  287. * Applies a definition query to the layer.
  288. *
  289. * @function setDefinitionQuery
  290. * @param {String} query a valid definition query
  291. */
  292. setDefinitionQuery (query) {
  293. // very difficult.
  294. this._layer.setDefinitionExpression(query);
  295. }
  296. }
  297. module.exports = () => ({
  298. FeatureRecord
  299. });