layer/layerRec/dynamicRecord.js

  1. 'use strict';
  2. const attribRecord = require('./attribRecord.js')();
  3. const shared = require('./shared.js')();
  4. const placeholderFC = require('./placeholderFC.js')();
  5. const layerInterface = require('./layerInterface.js')();
  6. const dynamicFC = require('./dynamicFC.js')();
  7. const attribFC = require('./attribFC.js')();
  8. /**
  9. * @class DynamicRecord
  10. */
  11. class DynamicRecord extends attribRecord.AttribRecord {
  12. // TODO are we still using passthrough stuff?
  13. get _layerPassthroughBindings () {
  14. // TEST STATUS none
  15. return ['setOpacity', 'setVisibility', 'setVisibleLayers', 'setLayerDrawingOptions'];
  16. }
  17. get _layerPassthroughProperties () {
  18. // TEST STATUS none
  19. return ['visibleAtMapScale', 'visible', 'spatialReference', 'layerInfos', 'supportsDynamicLayers'];
  20. }
  21. get layerType () { return shared.clientLayerType.ESRI_DYNAMIC; }
  22. /**
  23. * Create a layer record with the appropriate geoApi layer type. Layer config
  24. * should be fully merged with all layer options defined (i.e. this constructor
  25. * will not apply any defaults).
  26. * @param {Object} layerClass the ESRI api object for dynamic layers
  27. * @param {Object} esriRequest the ESRI api object for making web requests with proxy support
  28. * @param {Object} apiRef object pointing to the geoApi. allows us to call other geoApi functions
  29. * @param {Object} config layer config values
  30. * @param {Object} esriLayer an optional pre-constructed layer
  31. * @param {Function} epsgLookup an optional lookup function for EPSG codes (see geoService for signature)
  32. */
  33. constructor (layerClass, esriRequest, apiRef, config, esriLayer, epsgLookup) {
  34. // TEST STATUS basic
  35. super(layerClass, esriRequest, apiRef, config, esriLayer, epsgLookup);
  36. this.ArcGISDynamicMapServiceLayer = layerClass;
  37. // TODO what is the case where we have dynamic layer already prepared
  38. // and passed in? Generally this only applies to file layers (which
  39. // are feature layers).
  40. // TODO figure out controls on config
  41. // TODO worry about placeholders. WORRY. how does that even work here?
  42. this._proxies = {};
  43. }
  44. /**
  45. * Return a proxy interface for a child layer
  46. *
  47. * @param {Integer} featureIdx index of child entry (leaf or group)
  48. * @return {Object} proxy interface for given child
  49. */
  50. getChildProxy (featureIdx) {
  51. // TEST STATUS basic
  52. // TODO verify we have integer coming in and not a string
  53. // in this case, featureIdx can also be a group index
  54. if (this._proxies[featureIdx.toString()]) {
  55. return this._proxies[featureIdx.toString()];
  56. } else {
  57. // throw new Error(`attempt to get non-existing child proxy. Index ${featureIdx}`);
  58. // to handle the case of a structured legend needing a proxy for a child prior to the
  59. // layer loading, we treat an unknown proxy request as that case and return
  60. // a proxy loaded with a placeholder.
  61. // TODO how to pass in a name? add an optional second parameter? expose a "set name" on the proxy?
  62. const pfc = new placeholderFC.PlaceholderFC(this, '');
  63. const tProxy = new layerInterface.LayerInterface(pfc); // specificially no controls at this point.
  64. tProxy.convertToPlaceholder(pfc);
  65. this._proxies[featureIdx.toString()] = tProxy;
  66. return tProxy;
  67. }
  68. }
  69. // TODO I think we need to override getProxy to return a special Dynamic proxy.
  70. // Need to figure out how visibility works (i.e. layer is invisible or just empty visibleChildren array)
  71. // Might also need to manage the root children somehow (i.e. the layerEntries from the config)
  72. // TODO docs
  73. getFeatureCount (featureIdx) {
  74. // TEST STATUS basic
  75. // point url to sub-index we want
  76. // TODO might change how we manage index and url
  77. return super.getFeatureCount(this._layer.url + '/' + featureIdx);
  78. }
  79. /**
  80. * Triggers when the layer loads.
  81. *
  82. * @function onLoad
  83. */
  84. onLoad () {
  85. // TEST STATUS basic
  86. super.onLoad();
  87. const supportsDynamic = this._layer.supportsDynamicLayers;
  88. const controlBanlist = ['reload', 'snapshot', 'boundingBox'];
  89. if (!supportsDynamic) {
  90. controlBanlist.push('opacity');
  91. }
  92. // strip any banned controls from a controls array
  93. // array is modified
  94. const banControls = controls => {
  95. controlBanlist.forEach(bc => {
  96. const idx = controls.indexOf(bc);
  97. if (idx > -1) {
  98. controls.splice(idx, 1);
  99. }
  100. });
  101. // a bit redundant. useful if we are passing in an anonymous array.
  102. return controls;
  103. };
  104. // don't worry about structured legend. the legend part is separate from
  105. // the layers part. we just load what we are told to. the legend module
  106. // will handle the structured part.
  107. // NOTE for now, the only relevant properties to be propagated
  108. // from parent to child are .state and .controls .
  109. // .outfields does not make sense as chilren can have different fields.
  110. // We assume the objects at the layer level (index -1) are fully defaulted.
  111. // All other missing items assigned from parent item.
  112. // subconfig lookup. initialize with the layer root (-1), then add
  113. // in anything provided in the initial config.
  114. const subConfigs = {
  115. '-1': {
  116. config: {
  117. state: this.config.state,
  118. controls: banControls(this.config.controls.concat())
  119. },
  120. defaulted: true
  121. }
  122. };
  123. this.config.layerEntries.forEach(le => {
  124. subConfigs[le.index.toString()] = {
  125. config: le,
  126. defaulted: false
  127. };
  128. });
  129. // subfunction to either return a stored sub-config, or
  130. // derive a new subconfig from the parent config.
  131. // both params integers in string format.
  132. const fetchSubConfig = (id, parentId) => {
  133. if (subConfigs[id]) {
  134. const subC = subConfigs[id];
  135. if (!subC.defaulted) {
  136. // get any missing properties from parent
  137. const parent = subConfigs[parentId].config;
  138. // TODO verify if we need to check for controls array of .length === 0.
  139. // I am assuming an empty array a valid setting (i.e. no controls should be shown)
  140. if (!subC.config.controls) {
  141. // we can assume parent.controls has already been ban-scraped
  142. subC.config.controls = parent.controls.concat();
  143. } else {
  144. // ensure we dont have any bad controls lurking
  145. banControls(subC.config.controls);
  146. }
  147. if (!subC.config.state) {
  148. // copy all
  149. subC.config.state = Object.assign({}, parent.state);
  150. } else {
  151. // selective inheritance
  152. Object.keys(parent.state).forEach(stateKey => {
  153. // be aware of falsey logic here.
  154. if (!subC.config.state.hasOwnProperty(stateKey)) {
  155. subC.config.state[stateKey] = parent.state[stateKey];
  156. }
  157. });
  158. }
  159. if (!subC.config.hasOwnProperty('outfields')) {
  160. subC.config.outfields = '*';
  161. }
  162. subC.defaulted = true;
  163. }
  164. return subC.config;
  165. } else {
  166. // no config at all. direct copy properties from parent
  167. // we can assume parent.controls has already been ban-scraped
  168. const newConfig = {
  169. state: Object.assign({}, subConfigs[parentId].config.state),
  170. controls: subConfigs[parentId].config.controls.concat(),
  171. outfields: '*'
  172. };
  173. subConfigs[id] = {
  174. config: newConfig,
  175. defaulted: true
  176. };
  177. return newConfig;
  178. }
  179. };
  180. // this subfunction will recursively crawl a dynamic layerInfo structure.
  181. // it will generate proxy objects for all groups and leafs under the
  182. // input layerInfo.
  183. // it also collects and returns an array of leaf nodes so each group
  184. // can store it and have fast access to all leaves under it.
  185. const processLayerInfo = (layerInfo, treeArray, parentId) => {
  186. const sId = layerInfo.id.toString();
  187. const subConfig = fetchSubConfig(sId, parentId.toString());
  188. if (layerInfo.subLayerIds && layerInfo.subLayerIds.length > 0) {
  189. // group
  190. // TODO probably need some placeholder magic going on here too
  191. // TODO do we need to apply any config state?
  192. // TODO figure out control lists, whats available, whats disabled.
  193. // supply on second and third parameters
  194. let group;
  195. if (this._proxies[sId]) {
  196. // we have a pre-made proxy (structured legend)
  197. // TODO might need to pass controls array into group proxy
  198. group = this._proxies[sId];
  199. } else {
  200. // set up new proxy
  201. group = new layerInterface.LayerInterface(this, subConfig.controls);
  202. this._proxies[sId] = group;
  203. }
  204. group.convertToDynamicGroup(this, sId, subConfig.name || layerInfo.name || '');
  205. const treeGroup = { id: layerInfo.id, childs: [] };
  206. treeArray.push(treeGroup);
  207. // process the kids in the group.
  208. // store the child leaves in the internal variable
  209. layerInfo.subLayerIds.forEach(slid => {
  210. group._childLeafs = group._childLeafs.concat(
  211. processLayerInfo(this._layer.layerInfos[slid], treeGroup.childs, sId));
  212. });
  213. return group._childLeafs;
  214. } else {
  215. // leaf
  216. // TODO figure out control lists, whats available, whats disabled.
  217. // supply on second and third parameters.
  218. // might need to steal from parent, since auto-gen may not have explicit
  219. // config settings.
  220. // TODO since we are doing placeholder, might want to not provide controls array yet.
  221. let leaf;
  222. const pfc = new placeholderFC.PlaceholderFC(this, layerInfo.name);
  223. if (this._proxies[sId]) {
  224. // we have a pre-made proxy (structured legend)
  225. // TODO might need to pass controls array into leaf proxy
  226. leaf = this._proxies[sId];
  227. leaf.updateSource(pfc);
  228. } else {
  229. // set up new proxy
  230. leaf = new layerInterface.LayerInterface(null, subConfig.controls);
  231. leaf.convertToPlaceholder(pfc);
  232. this._proxies[sId] = leaf;
  233. }
  234. treeArray.push({ id: layerInfo.id });
  235. return [leaf];
  236. }
  237. };
  238. this._childTree = []; // public structure describing the tree
  239. if (this.config.layerEntries) {
  240. this.config.layerEntries.forEach(le => {
  241. if (!le.stateOnly) {
  242. processLayerInfo(this._layer.layerInfos[le.index], this._childTree, -1);
  243. }
  244. });
  245. }
  246. // trigger attribute load and set up children bundles.
  247. // TODO do we need an options object, with .skip set for sub-layers we are not dealing with?
  248. // we currently (sort-of) have the list of things included -- the keys of the
  249. // subConfigs object. we would need to iterate layerInfos again and find keys
  250. // not in subConfigs.
  251. // Alternate: add new option that is opposite of .skip. Will be more of a
  252. // .only, and we won't have to derive a "skip" set from our inclusive
  253. // list
  254. // Furthermore: skipping / being effecient might not really matter here anymore.
  255. // back in the day, loadLayerAttribs would actually load everything.
  256. // now it just sets up promises that dont trigger until someone asks for
  257. // the information.
  258. const attributeBundle = this._apiRef.attribs.loadLayerAttribs(this._layer);
  259. const initVis = [];
  260. // converts server type string to client type string
  261. const serverLayerTypeToClientLayerType = serverType => {
  262. switch (serverType) {
  263. case 'Feature Layer':
  264. return shared.clientLayerType.ESRI_FEATURE;
  265. case 'Raster Layer':
  266. return shared.clientLayerType.ESRI_RASTER;
  267. default:
  268. throw new Error('Unexpected layer type in serverLayerTypeToClientLayerType', serverType);
  269. }
  270. };
  271. // idx is a string
  272. attributeBundle.indexes.forEach(idx => {
  273. // if we don't have a defaulted sub-config, it means the attribute leaf is not present
  274. // in our visible tree structure.
  275. const subC = subConfigs[idx];
  276. if (subC && subC.defaulted) {
  277. // TODO need to worry about Raster Layers here. DynamicFC is based off of
  278. // attribute things.
  279. const dFC = new dynamicFC.DynamicFC(this, idx, attributeBundle[idx], subC.config);
  280. this._featClasses[idx] = dFC;
  281. if (subC.config.state.visibility) {
  282. initVis.push(parseInt(idx)); // store for initial visibility
  283. }
  284. // if we have a proxy watching this leaf, replace its placeholder with the real data
  285. const leafProxy = this._proxies[idx];
  286. if (leafProxy) {
  287. // TODO update controls array?
  288. // trickery involving symbology.
  289. // the UI is binding to the object that was set up in the leaf placeholder.
  290. // so we cannot just make a new one.
  291. // we need to inject the placeholder symbology object into our new DynamicFC.
  292. // then we can aysnch update it with real symbols, and the UI is still
  293. // pointing at the same array in memory.
  294. dFC._symbolBundle = leafProxy.symbology;
  295. leafProxy.convertToDynamicLeaf(dFC);
  296. }
  297. // load real symbols into our source
  298. dFC.loadSymbology();
  299. // update asynchronous values
  300. dFC.getLayerData().then(ld => {
  301. dFC.layerType = serverLayerTypeToClientLayerType(ld.layerType);
  302. if (dFC.layerType === shared.clientLayerType.ESRI_FEATURE) {
  303. dFC.geomType = ld.geometryType;
  304. }
  305. });
  306. this.getFeatureCount(idx).then(fc => {
  307. dFC.featureCount = fc;
  308. });
  309. }
  310. });
  311. // need to do a post ban-sweep on control arrays. dynamic groups are not allowed
  312. // to have opacity. if we had removed above, children of groups would have also
  313. // lost opacity.
  314. // a lovely pyramid of doom.
  315. Object.keys(this._proxies).forEach(sId => {
  316. const proxy = this._proxies[sId];
  317. if (!proxy.isPlaceholder && proxy.layerType === shared.clientLayerType.ESRI_GROUP) {
  318. const poIdx = proxy.availableControls.indexOf('opacity');
  319. if (poIdx > -1) {
  320. proxy.availableControls.splice(poIdx, 1);
  321. // TODO test if we need to adjust subconfigs, or if it's all the same pointer
  322. if (subConfigs[sId].config.controls.indexOf('opacity') > -1) {
  323. console.log('HEEEY HEYYY WE HAVE A CONFIG OPACITY GROUP, ADD CODE TO REMOVE IT');
  324. }
  325. }
  326. }
  327. });
  328. if (initVis.length === 0) {
  329. initVis.push(-1); // esri code for set all to invisible
  330. }
  331. this._layer.setVisibleLayers(initVis);
  332. }
  333. // override to add child index parameter
  334. zoomToScale (childIdx, map, lods, zoomIn, zoomGraphic = false) {
  335. // TEST STATUS none
  336. // get scale set from child, then execute zoom
  337. return this._featClasses[childIdx].getScaleSet().then(scaleSet => {
  338. return this._zoomToScaleSet(map, lods, zoomIn, scaleSet, zoomGraphic);
  339. });
  340. }
  341. isOffScale (childIdx, mapScale) {
  342. // TEST STATUS none
  343. return this._featClasses[childIdx].isOffScale(mapScale);
  344. }
  345. isQueryable (childIdx) {
  346. // TEST STATUS none
  347. return this._featClasses[childIdx].queryable;
  348. }
  349. // TODO if we need this back, may need to implement as getChildGeomType.
  350. // appears this ovverrides the LayerRecord.getGeomType function, which returns
  351. // undefined, and that is what we want on the DynamicRecord level (as dynamic layer)
  352. // has no geometry.
  353. // Currently, all child requests for geometry go through the proxy,
  354. // so could be this child-targeting version is irrelevant.
  355. /*
  356. getGeomType (childIdx) {
  357. // TEST STATUS none
  358. return this._featClasses[childIdx].geomType;
  359. }
  360. */
  361. getChildTree () {
  362. if (this._childTree) {
  363. return this._childTree;
  364. } else {
  365. throw new Error('Called getChildTree before layer is loaded');
  366. }
  367. }
  368. /**
  369. * Get the best user-friendly name of a field. Uses alias if alias is defined, else uses the system attribute name.
  370. *
  371. * @param {String} attribName the attribute name we want a nice name for
  372. * @param {String} childIndex index of the child layer whos attributes we are looking at
  373. * @return {Promise} resolves to the best available user friendly attribute name
  374. */
  375. aliasedFieldName (attribName, childIndex) {
  376. // TEST STATUS none
  377. return this._featClasses[childIndex].aliasedFieldName(attribName);
  378. }
  379. /**
  380. * Retrieves attributes from a layer for a specified feature index
  381. * @param {String} childIndex index of the child layer to get attributes for
  382. * @return {Promise} promise resolving with formatted attributes to be consumed by the datagrid and esri feature identify
  383. */
  384. getFormattedAttributes (childIndex) {
  385. // TEST STATUS none
  386. return this._featClasses[childIndex].getFormattedAttributes();
  387. }
  388. /**
  389. * Check to see if the attribute in question is an esriFieldTypeDate type.
  390. *
  391. * @param {String} attribName the attribute name we want to check if it's a date or not
  392. * @param {String} childIndex index of the child layer whos attributes we are looking at
  393. * @return {Promise} resolves to true or false based on the attribName type being esriFieldTypeDate
  394. */
  395. checkDateType (attribName, childIndex) {
  396. // TEST STATUS none
  397. return this._featClasses[childIndex].checkDateType(attribName);
  398. }
  399. /**
  400. * Returns attribute data for a child layer.
  401. *
  402. * @function getAttribs
  403. * @param {String} childIndex the index of the child layer
  404. * @returns {Promise} resolves with a layer attribute data object
  405. */
  406. getAttribs (childIndex) {
  407. // TEST STATUS none
  408. return this._featClasses[childIndex].getAttribs();
  409. }
  410. /**
  411. * Returns layer-specific data for a child layer
  412. *
  413. * @function getLayerData
  414. * @param {String} childIndex the index of the child layer
  415. * @returns {Promise} resolves with a layer data object
  416. */
  417. getLayerData (childIndex) {
  418. // TEST STATUS none
  419. return this._featClasses[childIndex].getLayerData();
  420. }
  421. getFeatureName (childIndex, objId, attribs) {
  422. // TEST STATUS none
  423. return this._featClasses[childIndex].getFeatureName(objId, attribs);
  424. }
  425. getSymbology (childIndex) {
  426. // TEST STATUS basic
  427. return this._featClasses[childIndex].getSymbology();
  428. }
  429. /**
  430. * Run a query on a dynamic layer, return the result as a promise.
  431. * @function identify
  432. * @param {Object} opts additional argumets like map object, clickEvent, etc.
  433. * @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
  434. */
  435. identify (opts) {
  436. // TEST STATUS none
  437. // TODO caller must pass in layer ids to interrogate. geoApi wont know what is toggled in the legend.
  438. // param is opts.layerIds, array of integer for every leaf to interrogate.
  439. // TODO add full documentation for options parameter
  440. // bundles results from all leaf layers
  441. const identifyResults = [];
  442. // create an results object for every leaf layer we are inspecting
  443. opts.layerIds.forEach(leafIndex => {
  444. // TODO fix these params
  445. // TODO legendEntry.name, legendEntry.symbology appear to be fast links to populate the left side of the results
  446. // view. perhaps it should not be in this object anymore?
  447. // TODO see how the client is consuming the internal pointer to layerRecord. this may also now be
  448. // directly available via the legend object.
  449. const identifyResult =
  450. new shared.IdentifyResult('legendEntry.name', 'legendEntry.symbology', 'EsriFeature', this,
  451. leafIndex, 'legendEntry.master.name'); // provide name of the master group as caption
  452. identifyResults[leafIndex] = identifyResult;
  453. });
  454. opts.tolerance = this.clickTolerance;
  455. const identifyPromise = this._apiRef.layer.serverLayerIdentify(this._layer, opts)
  456. .then(clickResults => {
  457. const hitIndexes = []; // sublayers that we got results for
  458. // transform attributes of click results into {name,data} objects
  459. // one object per identified feature
  460. //
  461. // each feature will have its attributes converted into a table
  462. // placeholder for now until we figure out how to signal the panel that
  463. // we want to make a nice table
  464. clickResults.forEach(ele => {
  465. // NOTE: the identify service returns aliased field names, so no need to look them up here.
  466. // however, this means we need to un-alias the data when doing field lookups.
  467. // NOTE: ele.layerId is what we would call featureIdx
  468. hitIndexes.push(ele.layerId);
  469. // get metadata about this sublayer
  470. this.getLayerData(ele.layerId).then(lData => {
  471. const identifyResult = identifyResults[ele.layerId];
  472. if (lData.supportsFeatures) {
  473. const unAliasAtt = attribFC.AttribFC.unAliasAttribs(ele.feature.attributes, lData.fields);
  474. // TODO traditionally, we did not pass fields into attributesToDetails as data was
  475. // already aliased from the server. now, since we are extracting field type as
  476. // well, this means things like date formatting might not be applied to
  477. // identify results. examine the impact of providing the fields parameter
  478. // to data that is already aliased.
  479. identifyResult.data.push({
  480. name: ele.value,
  481. data: this.attributesToDetails(ele.feature.attributes),
  482. oid: unAliasAtt[lData.oidField],
  483. symbology: [{
  484. svgcode: this._apiRef.symbology.getGraphicIcon(unAliasAtt, lData.renderer)
  485. }]
  486. });
  487. }
  488. identifyResult.isLoading = false;
  489. });
  490. });
  491. // set the rest of the entries to loading false
  492. identifyResults.forEach(identifyResult => {
  493. if (hitIndexes.indexOf(identifyResult.requester.featureIdx) === -1) {
  494. identifyResult.isLoading = false;
  495. }
  496. });
  497. });
  498. return {
  499. identifyResults: identifyResults.filter(identifyResult => identifyResult), // collapse sparse array
  500. identifyPromise
  501. };
  502. }
  503. // TODO docs
  504. getChildName (index) {
  505. // TEST STATUS none
  506. // TODO revisit logic. is this the best way to do this? what are the needs of the consuming code?
  507. // TODO restructure so WMS can use this too?
  508. // will not use FC classes, as we also need group names
  509. return this._layer.layerInfos[index].name;
  510. }
  511. }
  512. module.exports = () => ({
  513. DynamicRecord
  514. });