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. get isTrueDynamic () { return this._isTrueDynamic; }
  23. /**
  24. * Create a layer record with the appropriate geoApi layer type.
  25. * Regarding configuration -- in the standard case, the incoming config object
  26. * will be incomplete with regards to child state. It may not even have entries for all possible
  27. * child sub-layers. Given our config defaulting for children happens AFTER the layer loads,
  28. * it means what is passed in at the constructor is generally unreliable except for any child names,
  29. * and the class will treat that information as unreliable (the UI will set values after config defaulting
  30. * happens). In the rare case where the config is fully formed and we want to take advantage of that,
  31. * set the configIsComplete param to true. Be aware that if the config is not actually complete you may
  32. * get a layer in an undesired initial state.
  33. *
  34. * @param {Object} layerClass the ESRI api object for dynamic layers
  35. * @param {Object} esriRequest the ESRI api object for making web requests with proxy support
  36. * @param {Object} apiRef object pointing to the geoApi. allows us to call other geoApi functions
  37. * @param {Object} config layer config values
  38. * @param {Object} esriLayer an optional pre-constructed layer
  39. * @param {Function} epsgLookup an optional lookup function for EPSG codes (see geoService for signature)
  40. * @param {Boolean} configIsComplete an optional flag to indicate if the config is fully flushed out (i.e. things defined for all children). Defaults to false.
  41. */
  42. constructor (layerClass, esriRequest, apiRef, config, esriLayer, epsgLookup, configIsComplete = false) {
  43. // TODO might need some nonsense here. if not configIsComplete, and layer is set to visible in config,
  44. // we may need to hack the process so that the esri layer object is initialized as invisible,
  45. // but the config is still marked as visible so the UI knows to do the proper defaulting.
  46. // As is right now, the layer might start to pull an image from the server while our onLoad
  47. // event handler is running and shutting off visibilities.
  48. super(layerClass, esriRequest, apiRef, config, esriLayer, epsgLookup);
  49. this.ArcGISDynamicMapServiceLayer = layerClass;
  50. this._configIsComplete = configIsComplete;
  51. // TODO what is the case where we have dynamic layer already prepared
  52. // and passed in? Generally this only applies to file layers (which
  53. // are feature layers).
  54. this._proxies = {};
  55. // marks if layer supports dynamic capabilities, like child opacity, renderer change, layer reorder
  56. // TODO ensure false is best default (what is better for UI)
  57. this._isTrueDynamic = false;
  58. }
  59. /**
  60. * Return a proxy interface for a child layer
  61. *
  62. * @param {Integer} featureIdx index of child entry (leaf or group)
  63. * @return {Object} proxy interface for given child
  64. */
  65. getChildProxy (featureIdx) {
  66. // TODO verify we have integer coming in and not a string
  67. // NOTE we no longer have group proxies. Since it is possible for a proxy to
  68. // be requested prior to a dynamic layer being loaded (and thus have no
  69. // idea of the index is valid or the index is a group), we always give
  70. // a proxy and depend on the caller to be smart about it.
  71. const strIdx = featureIdx.toString();
  72. if (this._proxies[strIdx]) {
  73. return this._proxies[strIdx];
  74. } else {
  75. // throw new Error(`attempt to get non-existing child proxy. Index ${featureIdx}`);
  76. // to handle the case of a structured legend needing a proxy for a child prior to the
  77. // layer loading, we treat an unknown proxy request as that case and return
  78. // a proxy loaded with a placeholder.
  79. // TODO how to pass in a name? add an optional second parameter? expose a "set name" on the proxy?
  80. const pfc = new placeholderFC.PlaceholderFC(this, '');
  81. const tProxy = new layerInterface.LayerInterface(pfc);
  82. tProxy.convertToPlaceholder(pfc);
  83. this._proxies[strIdx] = tProxy;
  84. return tProxy;
  85. }
  86. }
  87. // TODO docs
  88. getFeatureCount (featureIdx) {
  89. // point url to sub-index we want
  90. // TODO might change how we manage index and url
  91. return super.getFeatureCount(this._layer.url + '/' + featureIdx);
  92. }
  93. // TODO docs
  94. synchOpacity (opacity) {
  95. // in the case where a dynamic layer does not support child opacity, if a user
  96. // changes the opacity of a child, it actually just adjusts the opacity of the layer.
  97. // this means that all other children of the layer need to have their opacity set
  98. // to the same value. but we dont want to trigger a number of opacity change requests,
  99. // so we do some trickery here.
  100. Object.keys(this._featClasses).forEach(idx => {
  101. const fc = this._featClasses[idx];
  102. if (fc) {
  103. // important: must use the private ._opacity property here,
  104. // as we want to avoid the logic on the .opacity setter.
  105. fc._opacity = opacity;
  106. }
  107. });
  108. // update the layer itself.
  109. this.opacity = opacity;
  110. }
  111. /**
  112. * Triggers when the layer loads.
  113. *
  114. * @function onLoad
  115. */
  116. onLoad () {
  117. const loadPromises = super.onLoad();
  118. this._isTrueDynamic = this._layer.supportsDynamicLayers;
  119. // don't worry about structured legend. the legend part is separate from
  120. // the layers part. we just load what we are told to. the legend module
  121. // will handle the structured part.
  122. // see comments on the constructor to learn about _configIsComplete and
  123. // what type of scenarios you can expect for incoming configs
  124. // snapshot doesn't apply to child layers
  125. // we don't include bounding box / extent, as we are inheriting it.
  126. // a lack of the property means we use the layer definition
  127. const dummyState = {
  128. opacity: 1,
  129. visibility: false,
  130. query: false
  131. };
  132. // subfunction to clone a layerEntries config object.
  133. // since we are using typed objects with getters and setters,
  134. // our usual easy ways of cloning an object don't work (e.g. using
  135. // JSON.parse(JSON.stringify(x))). This is not a great solution (understatement),
  136. // but is being done as a quick n dirty workaround. At a later time,
  137. // the guts of this function can be re-examined for a better,
  138. // less hardcoded solution.
  139. const cloneConfig = origConfig => {
  140. const clone = {};
  141. // direct copies, no defaulting
  142. clone.name = origConfig.name;
  143. clone.index = origConfig.index;
  144. clone.stateOnly = origConfig.stateOnly;
  145. // an empty string is a valid property, so be wary of falsy logic
  146. clone.outfields = origConfig.hasOwnProperty('outfields') ? origConfig.outfields : '*';
  147. // with state, we are either complete, or pure defaults.
  148. // in the non-complete case, we treat our state as unreliable and
  149. // expect the client to assign properties as it does parent-child inheritance
  150. // defaulting (which occurs after this onLoad function has completed)
  151. if (this._configIsComplete) {
  152. clone.state = {
  153. visiblity: origConfig.visiblity,
  154. opacity: origConfig.opacity,
  155. query: origConfig.query
  156. };
  157. } else {
  158. clone.state = Object.assign({}, dummyState);
  159. }
  160. // if extent is present, we assume it is fully defined.
  161. // TODO living dangerously for now. clarify if extents are strongly typed or just classic json
  162. clone.extent = origConfig.extent;
  163. /*
  164. if (origConfig.extent) {
  165. clone.extent = {
  166. }
  167. }
  168. */
  169. return clone;
  170. };
  171. // collate any relevant overrides from the config.
  172. const subConfigs = {};
  173. this.config.layerEntries.forEach(le => {
  174. subConfigs[le.index.toString()] = {
  175. config: cloneConfig(le),
  176. defaulted: this._configIsComplete
  177. };
  178. });
  179. // subfunction to return a subconfig object.
  180. // if it does not exist or is not defaulted, will do that first
  181. // id param is an integer in string format
  182. const fetchSubConfig = (id, serverName = '') => {
  183. if (subConfigs[id]) {
  184. const subC = subConfigs[id];
  185. if (!subC.defaulted) {
  186. // config is incomplete, fill in blanks
  187. // we will never hit this code block a complete config was passed in
  188. // apply a server name if no name exists
  189. if (!subC.config.name) {
  190. subC.config.name = serverName;
  191. }
  192. // mark as defaulted so we don't do this again
  193. subC.defaulted = true;
  194. }
  195. return subC.config;
  196. } else {
  197. // no config at all. we apply defaults, and a name from the server if available
  198. const configSeed = {
  199. name: serverName,
  200. index: parseInt(id),
  201. stateOnly: true
  202. };
  203. const newConfig = cloneConfig(configSeed);
  204. subConfigs[id] = {
  205. config: newConfig,
  206. defaulted: true
  207. };
  208. return newConfig;
  209. }
  210. };
  211. // this subfunction will recursively crawl a dynamic layerInfo structure.
  212. // it will generate proxy objects for all groups and leafs under the
  213. // input layerInfo.
  214. // we also generate a tree structure of layerInfos that is in a format
  215. // that makes the client happy
  216. const processLayerInfo = (layerInfo, treeArray) => {
  217. const sId = layerInfo.id.toString();
  218. const subC = fetchSubConfig(sId, layerInfo.name);
  219. if (layerInfo.subLayerIds && layerInfo.subLayerIds.length > 0) {
  220. // group sublayer. set up our tree for the client, then crawl childs.
  221. const treeGroup = {
  222. entryIndex: layerInfo.id,
  223. name: subC.name,
  224. childs: []
  225. };
  226. treeArray.push(treeGroup);
  227. // process the kids in the group.
  228. // store the child leaves in the internal variable
  229. layerInfo.subLayerIds.forEach(slid => {
  230. processLayerInfo(this._layer.layerInfos[slid], treeGroup.childs);
  231. });
  232. } else {
  233. // leaf sublayer. make placeholders, add leaf to the tree
  234. const pfc = new placeholderFC.PlaceholderFC(this, subC.name);
  235. if (this._proxies[sId]) {
  236. // we have a pre-made proxy (structured legend). update it.
  237. this._proxies[sId].updateSource(pfc);
  238. } else {
  239. // set up new proxy
  240. const leafProxy = new layerInterface.LayerInterface(null);
  241. leafProxy.convertToPlaceholder(pfc);
  242. this._proxies[sId] = leafProxy;
  243. }
  244. treeArray.push({ entryIndex: layerInfo.id });
  245. }
  246. };
  247. this._childTree = []; // public structure describing the tree
  248. // process the child layers our config is interested in, and all their children.
  249. if (this.config.layerEntries) {
  250. this.config.layerEntries.forEach(le => {
  251. if (!le.stateOnly) {
  252. processLayerInfo(this._layer.layerInfos[le.index], this._childTree);
  253. }
  254. });
  255. }
  256. // trigger attribute load and set up children bundles.
  257. // do we need an options object, with .skip set for sub-layers we are not dealing with?
  258. // Alternate: add new option that is opposite of .skip. Will be more of a
  259. // .only, and we won't have to derive a "skip" set from our inclusive
  260. // list
  261. // Furthermore: skipping / being effecient might not really matter here anymore.
  262. // back in the day, loadLayerAttribs would actually load everything.
  263. // now it just sets up promises that dont trigger until someone asks for
  264. // the information. <-- for now we are doing this approach.
  265. const attributeBundle = this._apiRef.attribs.loadLayerAttribs(this._layer);
  266. // converts server layer type string to client layer type string
  267. const serverLayerTypeToClientLayerType = serverType => {
  268. switch (serverType) {
  269. case 'Feature Layer':
  270. return shared.clientLayerType.ESRI_FEATURE;
  271. case 'Raster Layer':
  272. return shared.clientLayerType.ESRI_RASTER;
  273. default:
  274. console.warn('Unexpected layer type in serverLayerTypeToClientLayerType', serverType);
  275. return shared.clientLayerType.UNKNOWN;
  276. }
  277. };
  278. // idx is a string
  279. attributeBundle.indexes.forEach(idx => {
  280. // if we don't have a defaulted sub-config, it means the attribute leaf is not present
  281. // in our visible tree structure.
  282. const subC = subConfigs[idx];
  283. if (subC && subC.defaulted) {
  284. // TODO need to worry about Raster Layers here. DynamicFC is based off of
  285. // attribute things.
  286. const dFC = new dynamicFC.DynamicFC(this, idx, attributeBundle[idx], subC.config);
  287. this._featClasses[idx] = dFC;
  288. // if we have a proxy watching this leaf, replace its placeholder with the real data
  289. const leafProxy = this._proxies[idx];
  290. if (leafProxy) {
  291. leafProxy.convertToDynamicLeaf(dFC);
  292. }
  293. // load real symbols into our source
  294. loadPromises.push(dFC.loadSymbology());
  295. // update asynchronous values
  296. const pLD = dFC.getLayerData()
  297. .then(ld => {
  298. dFC.layerType = serverLayerTypeToClientLayerType(ld.layerType);
  299. // if we didn't have an extent defined on the config, use the layer extent
  300. if (!dFC.extent) {
  301. dFC.extent = ld.extent;
  302. }
  303. // skip a number of things if it is a raster layer
  304. // either way, return a promise so our loadPromises have a good
  305. // value to wait on.
  306. if (dFC.layerType === shared.clientLayerType.ESRI_FEATURE) {
  307. dFC.geomType = ld.geometryType;
  308. return this.getFeatureCount(idx).then(fc => {
  309. dFC.featureCount = fc;
  310. });
  311. } else {
  312. return Promise.resolve();
  313. }
  314. })
  315. .catch(() => {
  316. dFC.layerType = shared.clientLayerType.UNRESOLVED;
  317. });
  318. loadPromises.push(pLD);
  319. }
  320. });
  321. // TODO careful now, as the dynamicFC.DynamicFC constructor also appears to be setting visibility on the parent.
  322. if (this._configIsComplete) {
  323. // if we have a complete config, want to set layer visibility
  324. // get an array of leaf ids that are visible.
  325. // use _featClasses as it contains keys that exist on the server and are
  326. // potentially visible in the client.
  327. const initVis = Object.keys(this._featClasses)
  328. .filter(fcId => {return fetchSubConfig(fcId).config.state.visibility; })
  329. .map(fcId => { return parseInt(fcId); });
  330. if (initVis.length === 0) {
  331. initVis.push(-1); // esri code for set all to invisible
  332. }
  333. this._layer.setVisibleLayers(initVis);
  334. } else {
  335. // default configuration for non-complete config.
  336. this._layer.setVisibility(false);
  337. this._layer.setVisibleLayers([-1]);
  338. }
  339. Promise.all(loadPromises).then(() => {
  340. this._stateChange(shared.states.LOADED);
  341. });
  342. }
  343. // override to add child index parameter
  344. zoomToScale (childIdx, map, lods, zoomIn, zoomGraphic = false) {
  345. // get scale set from child, then execute zoom
  346. return this._featClasses[childIdx].getScaleSet().then(scaleSet => {
  347. return this._zoomToScaleSet(map, lods, zoomIn, scaleSet, zoomGraphic);
  348. });
  349. }
  350. isOffScale (childIdx, mapScale) {
  351. return this._featClasses[childIdx].isOffScale(mapScale);
  352. }
  353. isQueryable (childIdx) {
  354. return this._featClasses[childIdx].queryable;
  355. }
  356. // TODO if we need this back, may need to implement as getChildGeomType.
  357. // appears this ovverrides the LayerRecord.getGeomType function, which returns
  358. // undefined, and that is what we want on the DynamicRecord level (as dynamic layer)
  359. // has no geometry.
  360. // Currently, all child requests for geometry go through the proxy,
  361. // so could be this child-targeting version is irrelevant.
  362. /*
  363. getGeomType (childIdx) {
  364. // TEST STATUS none
  365. return this._featClasses[childIdx].geomType;
  366. }
  367. */
  368. getChildTree () {
  369. if (this._childTree) {
  370. return this._childTree;
  371. } else {
  372. throw new Error('Called getChildTree before layer is loaded');
  373. }
  374. }
  375. /**
  376. * Get the best user-friendly name of a field. Uses alias if alias is defined, else uses the system attribute name.
  377. *
  378. * @param {String} attribName the attribute name we want a nice name for
  379. * @param {String} childIndex index of the child layer whos attributes we are looking at
  380. * @return {Promise} resolves to the best available user friendly attribute name
  381. */
  382. aliasedFieldName (attribName, childIndex) {
  383. // TEST STATUS none
  384. return this._featClasses[childIndex].aliasedFieldName(attribName);
  385. }
  386. /**
  387. * Retrieves attributes from a layer for a specified feature index
  388. * @param {String} childIndex index of the child layer to get attributes for
  389. * @return {Promise} promise resolving with formatted attributes to be consumed by the datagrid and esri feature identify
  390. */
  391. getFormattedAttributes (childIndex) {
  392. // TEST STATUS none
  393. return this._featClasses[childIndex].getFormattedAttributes();
  394. }
  395. /**
  396. * Check to see if the attribute in question is an esriFieldTypeDate type.
  397. *
  398. * @param {String} attribName the attribute name we want to check if it's a date or not
  399. * @param {String} childIndex index of the child layer whos attributes we are looking at
  400. * @return {Promise} resolves to true or false based on the attribName type being esriFieldTypeDate
  401. */
  402. checkDateType (attribName, childIndex) {
  403. // TEST STATUS none
  404. return this._featClasses[childIndex].checkDateType(attribName);
  405. }
  406. /**
  407. * Returns attribute data for a child layer.
  408. *
  409. * @function getAttribs
  410. * @param {String} childIndex the index of the child layer
  411. * @returns {Promise} resolves with a layer attribute data object
  412. */
  413. getAttribs (childIndex) {
  414. // TEST STATUS none
  415. return this._featClasses[childIndex].getAttribs();
  416. }
  417. /**
  418. * Returns layer-specific data for a child layer
  419. *
  420. * @function getLayerData
  421. * @param {String} childIndex the index of the child layer
  422. * @returns {Promise} resolves with a layer data object
  423. */
  424. getLayerData (childIndex) {
  425. return this._featClasses[childIndex].getLayerData();
  426. }
  427. getFeatureName (childIndex, objId, attribs) {
  428. return this._featClasses[childIndex].getFeatureName(objId, attribs);
  429. }
  430. getSymbology (childIndex) {
  431. return this._featClasses[childIndex].symbology;
  432. }
  433. /**
  434. * Run a query on a dynamic layer, return the result as a promise.
  435. * @function identify
  436. * @param {Object} opts additional argumets like map object, clickEvent, etc.
  437. * @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
  438. */
  439. identify (opts) {
  440. // TODO add full documentation for options parameter
  441. // bundles results from all leaf layers
  442. const identifyResults = [];
  443. // TODO add scale check to our filterting logic. might require a scale to be included on the opts param
  444. opts.layerIds = this._layer.visibleLayers
  445. .filter(leafIndex => {
  446. if (leafIndex === -1) {
  447. // this is marker for nothing is visible. get rid of it
  448. return false;
  449. } else {
  450. const fc = this._featClasses[leafIndex];
  451. if (fc) {
  452. return fc.queryable;
  453. } else {
  454. // we dont have a feature class for this id.
  455. // it is likely a a group or something visible but not active
  456. return false;
  457. }
  458. }
  459. });
  460. if (opts.layerIds.length === 0) {
  461. return {};
  462. }
  463. opts.layerIds.forEach(leafIndex => {
  464. // TODO fix these params
  465. // TODO legendEntry.name, legendEntry.symbology appear to be fast links to populate the left side of the results
  466. // view. perhaps it should not be in this object anymore?
  467. // TODO see how the client is consuming the internal pointer to layerRecord. this may also now be
  468. // directly available via the legend object.
  469. const identifyResult =
  470. new shared.IdentifyResult('legendEntry.name', 'legendEntry.symbology', 'EsriFeature', this,
  471. leafIndex, 'legendEntry.master.name'); // provide name of the master group as caption
  472. identifyResults[leafIndex] = identifyResult;
  473. });
  474. opts.tolerance = this.clickTolerance;
  475. const identifyPromise = this._apiRef.layer.serverLayerIdentify(this._layer, opts)
  476. .then(clickResults => {
  477. const hitIndexes = []; // sublayers that we got results for
  478. // transform attributes of click results into {name,data} objects
  479. // one object per identified feature
  480. //
  481. // each feature will have its attributes converted into a table
  482. // placeholder for now until we figure out how to signal the panel that
  483. // we want to make a nice table
  484. clickResults.forEach(ele => {
  485. // NOTE: the identify service returns aliased field names, so no need to look them up here.
  486. // however, this means we need to un-alias the data when doing field lookups.
  487. // NOTE: ele.layerId is what we would call featureIdx
  488. hitIndexes.push(ele.layerId);
  489. // get metadata about this sublayer
  490. this.getLayerData(ele.layerId).then(lData => {
  491. const identifyResult = identifyResults[ele.layerId];
  492. if (lData.supportsFeatures) {
  493. const unAliasAtt = attribFC.AttribFC.unAliasAttribs(ele.feature.attributes, lData.fields);
  494. // TODO traditionally, we did not pass fields into attributesToDetails as data was
  495. // already aliased from the server. now, since we are extracting field type as
  496. // well, this means things like date formatting might not be applied to
  497. // identify results. examine the impact of providing the fields parameter
  498. // to data that is already aliased.
  499. identifyResult.data.push({
  500. name: ele.value,
  501. data: this.attributesToDetails(ele.feature.attributes),
  502. oid: unAliasAtt[lData.oidField],
  503. symbology: [{
  504. svgcode: this._apiRef.symbology.getGraphicIcon(unAliasAtt, lData.renderer)
  505. }]
  506. });
  507. }
  508. identifyResult.isLoading = false;
  509. });
  510. });
  511. // set the rest of the entries to loading false
  512. identifyResults.forEach(identifyResult => {
  513. if (hitIndexes.indexOf(identifyResult.requester.featureIdx) === -1) {
  514. identifyResult.isLoading = false;
  515. }
  516. });
  517. });
  518. return {
  519. identifyResults: identifyResults.filter(identifyResult => identifyResult), // collapse sparse array
  520. identifyPromise
  521. };
  522. }
  523. // TODO docs
  524. getChildName (index) {
  525. // TEST STATUS none
  526. // TODO revisit logic. is this the best way to do this? what are the needs of the consuming code?
  527. // TODO restructure so WMS can use this too?
  528. // will not use FC classes, as we also need group names
  529. return this._layer.layerInfos[index].name;
  530. }
  531. // TODO we may want version of layerRecord.zoomToBoundary that targets a child index.
  532. // alternately this might go on the proxy and then we go direct from there.
  533. }
  534. module.exports = () => ({
  535. DynamicRecord
  536. });