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