layer/layerRecord.js

  1. 'use strict';
  2. // TODO bump version.
  3. // TODO look at ripping out esriBundle, and passing specific classes as needed
  4. // TODO consider splitting out into one-file-per-class. Remember the class must be available at compile time
  5. // edit: for real, this file is getting silly-big
  6. // Classes for handling different types of layers
  7. /*
  8. Class heirarchy overview:
  9. We have FC, Record, and Interface classes
  10. FC represents a logical layer. Think of a feature class (gis term, not programming term)
  11. or a raster source. It is one atomic layer.
  12. Record represents a physical layer. Think of a layer in the ESRI map stack. Think of
  13. something represented by an ESRI API layer object.
  14. Interfac is a classs that presents information to the UI and facilitates bindings.
  15. It also exposes calls to perform actions on the layer (e.g. the action a UI button
  16. would execute).
  17. FC classes are contained within Record classes.
  18. If a property or function applies to a logical layer (e.g. min and max scale levels),
  19. it should reside in an FC class. If it applies to a physical layer (e.g. loading
  20. state), it should reside in a Record.
  21. E.g.
  22. A feature layer is implemented with one Record and one FC, because by nature,
  23. a feature layer can only contain data from one feature class.
  24. A dynamic layer is implemented with one Record, and a FC for every
  25. leaf child layer.
  26. An interface object should exist for every layer-bound entry in the legend.
  27. Most Records will have one interface, as they just have one legend entry.
  28. Dynamic Records will also have interfaces for children. This can include
  29. group items, which don't have FC objects. Tricky, eh!
  30. */
  31. // TODO revisit if we still need rv- in these constants.
  32. const states = { // these are used as css classes; hence the `rv` prefix
  33. NEW: 'rv-new',
  34. REFRESH: 'rv-refresh',
  35. LOADING: 'rv-loading',
  36. LOADED: 'rv-loaded', // TODO maybe loaded and default are the same?
  37. DEFAULT: 'rv-default',
  38. ERROR: 'rv-error'
  39. };
  40. // TODO crazy idea. instead of having attribute .layerInfo as a promise,
  41. // we pair that promise with the layer's load event. Essentially, don't
  42. // change our state to loaded until both the layer is loaded AND the .layerInfo
  43. // is loaded. Then we store the result in a not-promise var, and everything else
  44. // can access it synchronously.
  45. // Risk: need to make sure we never need to use .layerInfo prior to the layer loading.
  46. // Risk: layer needs to wait until it has pulled additional info prior to being active (negligible?)
  47. // TODO full review of use of object id, specificly the type -- is it string or integer
  48. // TODO ditto for featureIdx.
  49. // Controls Interface class is used to provide something to the UI that it can bind to.
  50. // It helps the UI keep in line with the layer state.
  51. // Due to bindings, we cannot destroy & recreate an interface when a legend item
  52. // goes from 'Unknown Placeholder' to 'Specific Layer Type'. This means we cannot
  53. // do object heirarchies, as to go from PlaceholderInterface to FeatureLayerInterface
  54. // would require a new object. Instead, we have a class that exposes all possible
  55. // methods and properties as error throwing stubs. Then we replace those functions
  56. // with real ones once we know the flavour of interface we want.
  57. // TODO rename this? A legend entry that is just text will use this to bind content. So the word Layer might be wrong
  58. class LayerInterface {
  59. /**
  60. * @param {Object} source object that provides info to the interface. usually a LayerRecord or FeatureClass
  61. * @param {Array} availableControls [optional=[]] an array or controls names that are displayed inside the legendEntry
  62. * @param {Array} disabledControls [optional=[]] an array or controls names that are disabled and cannot be interacted wiht by a user
  63. */
  64. constructor (source, availableControls = [], disabledControls = []) {
  65. this._source = source;
  66. this._availableControls = availableControls;
  67. this._disabledConrols = disabledControls;
  68. }
  69. // shortcut function for throwing errors on unimplemented functions.
  70. _iAmError () {
  71. throw new Error('Call not supported.');
  72. }
  73. // these expose ui controls available on the interface and indicate which ones are disabled
  74. get availableControls () { return this._availableControls; }
  75. get disabledControls () { return this._disabledControls; }
  76. get symbology () { this._iAmError(); }
  77. // can be group or node name
  78. get name () { this._iAmError(); }
  79. // these are needed for the type flag
  80. get layerType () { this._iAmError(); }
  81. get geometryType () { this._iAmError(); }
  82. get featureCount () { this._iAmError(); }
  83. // layer states
  84. get layerState () { this._iAmError(); }
  85. get isRefreshing () { this._iAmError(); }
  86. // these return the current values of the corresponding controls
  87. get visibility () { this._iAmError(); }
  88. get opacity () { this._iAmError(); }
  89. get boundingBox () { this._iAmError(); }
  90. get query () { this._iAmError(); }
  91. get snapshot () { this._iAmError(); }
  92. // fetches attributes for use in the datatable
  93. get formattedAttributes () { this._iAmError(); }
  94. // content for static legend entires (non-layer/non-group)
  95. get infoType () { this._iAmError(); }
  96. get infoContent () { this._iAmError(); }
  97. // these set values to the corresponding controls
  98. setVisibility () { this._iAmError(); }
  99. setOpacity () { this._iAmError(); }
  100. setBoundingBox () { this._iAmError(); }
  101. setQuery () { this._iAmError(); }
  102. setSnapshot () { this._iAmError(); }
  103. // updates what this interface is pointing to, in terms of layer data source.
  104. // often, the interface starts with a placeholder to avoid errors and return
  105. // defaults. This update happens after a layer has loaded, and new now want
  106. // the interface reading off the real FC.
  107. // TODO docs
  108. updateSource (newSource) {
  109. this._source = newSource;
  110. }
  111. convertToSingleLayer (layerRecord) {
  112. this._source = layerRecord;
  113. newProp(this, 'symbology', standardGetSymbology);
  114. newProp(this, 'layerState', standardGetLayerState);
  115. newProp(this, 'isRefreshing', standardGetIsRefreshing);
  116. newProp(this, 'visibility', standardGetVisibility);
  117. newProp(this, 'opacity', standardGetOpacity);
  118. newProp(this, 'boundingBox', standardGetBoundingBox);
  119. newProp(this, 'query', standardGetQuery);
  120. newProp(this, 'geometryType', standardGetGeometryType);
  121. newProp(this, 'featureCount', standardGetFeatureCount);
  122. this.setVisibility = standardSetVisibility;
  123. this.setOpacity = standardSetOpacity;
  124. this.setBoundingBox = standardSetBoundingBox;
  125. this.setQuery = standardSetQuery;
  126. }
  127. convertToFeatureLayer (layerRecord) {
  128. this.convertToSingleLayer(layerRecord);
  129. newProp(this, 'snapshot', featureGetSnapshot);
  130. newProp(this, 'formattedAttributes', standardGetFormattedAttributes);
  131. this.setSnapshot = featureSetSnapshot;
  132. }
  133. convertToDynamicLeaf (dynamicFC) {
  134. this._source = dynamicFC;
  135. newProp(this, 'symbology', dynamicLeafGetSymbology);
  136. newProp(this, 'visibility', dynamicLeafGetVisibility);
  137. newProp(this, 'opacity', dynamicLeafGetOpacity);
  138. newProp(this, 'query', dynamicLeafGetQuery);
  139. newProp(this, 'formattedAttributes', dynamicLeafGetFormattedAttributes);
  140. newProp(this, 'geometryType', dynamicLeafGetGeometryType);
  141. newProp(this, 'featureCount', dynamicLeafGetFeatureCount);
  142. this.setVisibility = dynamicLeafSetVisibility;
  143. this.setOpacity = dynamicLeafSetOpacity;
  144. this.setQuery = dynamicLeafSetQuery;
  145. }
  146. convertToDynamicGroup (layerRecord, groupId) {
  147. this._source = layerRecord;
  148. this._groupId = groupId;
  149. // contains a list of all child leaves for fast access
  150. this._childLeafs = [];
  151. newProp(this, 'visibility', dynamicGroupGetVisibility);
  152. newProp(this, 'opacity', dynamicGroupGetOpacity);
  153. this.setVisibility = dynamicGroupSetVisibility;
  154. this.setOpacity = dynamicGroupSetOpacity;
  155. }
  156. convertToStatic () {
  157. // TODO figure out what is involved here.
  158. }
  159. }
  160. /**
  161. * Worker function to add or override a get property on an object
  162. *
  163. * @function newProp
  164. * @private
  165. * @param {Object} target the object that will receive the new property
  166. * @param {String} propName name of the get property
  167. * @param {Function} getter the function defining the guts of the get property.
  168. */
  169. function newProp(target, propName, getter) {
  170. Object.defineProperty(target, propName, {
  171. get: getter
  172. });
  173. }
  174. // these functions are upgrades to the duds above.
  175. // we don't use arrow notation, as we want the `this` to point at the object
  176. // that these functions get smashed into.
  177. function standardGetLayerState() {
  178. /* jshint validthis: true */
  179. // returns one of Loading, Loaded, Error
  180. // TODO verify what DEFAULT actually is
  181. switch (this._source.state) {
  182. case states.NEW:
  183. case states.LOADING:
  184. return states.LOADING;
  185. case states.LOADED:
  186. case states.REFRESH:
  187. case states.DEFAULT:
  188. return states.LOADED;
  189. case states.ERROR:
  190. return states.ERROR;
  191. }
  192. }
  193. function standardGetIsRefreshing() {
  194. /* jshint validthis: true */
  195. return this._source.state === states.REFRESH;
  196. }
  197. function standardGetVisibility() {
  198. /* jshint validthis: true */
  199. // TODO should we make interface on _source (a layer record) for this and other properties?
  200. // e.g. _source.getVisiblility() ?
  201. // or too much overkill on fancy abstractions?
  202. return this._source._layer.visibile;
  203. }
  204. function dynamicLeafGetVisibility() {
  205. /* jshint validthis: true */
  206. return this._source.getVisibility();
  207. }
  208. function dynamicGroupGetVisibility() {
  209. /* jshint validthis: true */
  210. // check visibility of all children.
  211. // only return false if all children are invisible
  212. return this._childLeafs.some(leaf => { return leaf.visibility; });
  213. }
  214. function standardGetOpacity() {
  215. /* jshint validthis: true */
  216. return this._source._layer.opacity;
  217. }
  218. function dynamicLeafGetOpacity() {
  219. /* jshint validthis: true */
  220. // TODO figure out how to handle layers that don't support this.
  221. // possibly crosscheck against disabled settings
  222. // might not be an issue if, since there will be no control, nothing will call this
  223. // Alternative: convertToDynamicLeaf() will check and make decisions then?
  224. // TODO ensure .opacity is implemented.
  225. return this._sourceFC.opacity;
  226. }
  227. function dynamicGroupGetOpacity() {
  228. // TODO validate if we really need this?
  229. // currently changing opacity on a group will do nothing.
  230. // see AAFC AGRI Environmental Indicators layer in index-one sample.
  231. // could be we should not show opacity for groups?
  232. return 1;
  233. }
  234. function standardGetBoundingBox() {
  235. /* jshint validthis: true */
  236. // dont be fooled by function/prop name, we are returning bbox visibility,
  237. // not the box itself
  238. return this._source.isBBoxVisible();
  239. }
  240. function standardGetQuery() {
  241. /* jshint validthis: true */
  242. return this._source.isQueryable();
  243. }
  244. // TODO do we have group-level queryable settings?
  245. // e.g. click a control on dynamic root, all childs get setting?
  246. function dynamicLeafGetQuery() {
  247. /* jshint validthis: true */
  248. return this._source.queryable();
  249. }
  250. function standardGetFormattedAttributes() {
  251. /* jshint validthis: true */
  252. return this._source.getFormattedAttributes();
  253. }
  254. function dynamicLeafGetFormattedAttributes() {
  255. /* jshint validthis: true */
  256. // TODO code-wise this looks identical to standardGetFormattedAttributes.
  257. // however in this case, ._source is a DynamicFC, not a LayerRecord.
  258. // This is safer. Deleting this would avoid the duplication. Decide.
  259. return this._source.getFormattedAttributes();
  260. }
  261. function standardGetSymbology() {
  262. /* jshint validthis: true */
  263. return this._source.getSymbology();
  264. }
  265. function dynamicLeafGetSymbology() {
  266. /* jshint validthis: true */
  267. // TODO code-wise this looks identical to standardGetSymbology.
  268. // however in this case, ._source is a DynamicFC, not a LayerRecord.
  269. // This is safer. Deleting this would avoid the duplication. Decide.
  270. return this._source.getSymbology();
  271. }
  272. function standardGetGeometryType() {
  273. /* jshint validthis: true */
  274. return this._source.getGeomType();
  275. }
  276. function dynamicLeafGetGeometryType() {
  277. /* jshint validthis: true */
  278. return this._source.geomType;
  279. }
  280. function standardGetFeatureCount() {
  281. /* jshint validthis: true */
  282. return this._source.getFeatureCount();
  283. }
  284. function dynamicLeafGetFeatureCount() {
  285. /* jshint validthis: true */
  286. return this._source._parent.getFeatureCount(this._source._idx);
  287. }
  288. function standardSetVisibility(value) {
  289. /* jshint validthis: true */
  290. this._source._layer.visibile = value;
  291. }
  292. function dynamicLeafSetVisibility(value) {
  293. /* jshint validthis: true */
  294. this._source.setVisibility(value);
  295. // TODO see if we need to trigger any refresh of parents.
  296. // it may be that the bindings automatically work.
  297. }
  298. function dynamicGroupSetVisibility(value) {
  299. /* jshint validthis: true */
  300. // TODO be aware of cycles of updates. may need a force / dont broadcast flag.
  301. // since we are only hitting leaves and skipping child-groups, should be ok.
  302. this._childLeafs.forEach(leaf => {
  303. leaf.setVisibility(value);
  304. });
  305. }
  306. function standardSetOpacity(value) {
  307. /* jshint validthis: true */
  308. this._source._layer.opacity = value;
  309. }
  310. function dynamicLeafSetOpacity(value) {
  311. /* jshint validthis: true */
  312. this._source.opacity = value;
  313. // TODO call something in this._parent that will update
  314. // this._source._parent._layer.layerDrawingOptions[this._source.idx].transparency
  315. // being careful to remember that transparency is opacity * -1 (good job!)
  316. }
  317. function dynamicGroupSetOpacity(value) {
  318. // TODO see comments on dynamicGroupSetVisibility
  319. console.log('enhance group opacity', value);
  320. }
  321. function standardSetBoundingBox(value) {
  322. /* jshint validthis: true */
  323. // TODO test if object exists? Is it possible to have control without bbox layer?
  324. this._source.bbox.visible = value;
  325. }
  326. function standardSetQuery(value) {
  327. /* jshint validthis: true */
  328. this._source.setQueryable(value);
  329. }
  330. function dynamicLeafSetQuery(value) {
  331. /* jshint validthis: true */
  332. this._source.queryable = value;
  333. }
  334. function featureGetSnapshot() {
  335. /* jshint validthis: true */
  336. return this._source.isSnapshot;
  337. }
  338. function featureSetSnapshot() {
  339. // TODO trigger the snapshot process. need the big picture on how this orchestrates.
  340. // it involves a layer reload so possible this function is irrelevant, as the record
  341. // will likely get nuked
  342. console.log('MOCKING THE SNAPSHOT PROCESS');
  343. }
  344. // TODO implement function to get .name
  345. // where does it come from in single-layer? config? verify new schema
  346. // group node? a config entry? a layer property in auto-gen?
  347. // deal with unbound information-only case (static entry)?
  348. // TODO implement infoType / infoContent for static entry.
  349. // who supplies this? how does it get passed in.
  350. /* jshint validthis: false */
  351. // The FC classes are meant to be internal to this module. They help manage differences between single-type layers
  352. // like feature layers, image layers, and composite layers like dynamic layers.
  353. // Can toy with alternate approaches. E.g. have a convertToPlaceholder function in the interface.
  354. // simple object packager for client symbology package
  355. // TODO proper docs
  356. function makeSymbologyOutput(symbolArray, style) {
  357. return {
  358. stack: symbolArray,
  359. renderStyle: style
  360. };
  361. }
  362. // legend data is our modified legend structure.
  363. // it is similar to esri's server output, but all individual
  364. // items are promises.
  365. // TODO proper docs
  366. function makeSymbologyArray(legendData) {
  367. return legendData.map(item => {
  368. const symbologyItem = {
  369. svgcode: null,
  370. name: null
  371. };
  372. // file-based layers don't have symbology labels, default to ''
  373. // legend items are promises
  374. item.then(data => {
  375. symbologyItem.svgcode = data.svgcode;
  376. symbologyItem.name = data.label || '';
  377. });
  378. return symbologyItem;
  379. });
  380. }
  381. class PlaceholderFC {
  382. // contains dummy stuff to stop placeholder states from freaking out
  383. // prior to a layer being loaded.
  384. constructor (parent, name) {
  385. this._parent = parent;
  386. this._name = name;
  387. }
  388. // TODO probably need more stuff
  389. getVisibility () {
  390. // TODO enhance to have some default value, assigned in constructor?
  391. // TODO can a user toggle placeholders? does state need to be updated?
  392. return true;
  393. }
  394. // TODO same questions as visibility
  395. get opacity () { return 1; }
  396. getSymbology () {
  397. if (!this._symbology) {
  398. // TODO deal with random colours
  399. this._symbology = this._parent._api.symbology.generatePlaceholderSymbology(this._name, '#16bf27')
  400. .then(symbologyItem => {
  401. return makeSymbologyOutput([symbologyItem], 'icons');
  402. });
  403. }
  404. return this._symbology;
  405. }
  406. }
  407. /**
  408. * @class BasicFC
  409. */
  410. class BasicFC {
  411. // base class for feature class object. deals with stuff specific to a feature class (or raster equivalent)
  412. // TODO determine who is setting this. LayerRecord constructor & dynamic child generator?
  413. get queryable () { return this._queryable; }
  414. set queryable (value) { this._queryable = value; }
  415. // TODO determine who is setting this. LayerRecord constructor & dynamic child generator?
  416. get geomType () { return this._geomType; }
  417. set geomType (value) { this._geomType = value; }
  418. /**
  419. * @param {Object} parent the Record object that this Feature Class belongs to
  420. * @param {String} idx the service index of this Feature Class. an integer in string format. use '0' for non-indexed sources.
  421. */
  422. constructor (parent, idx) {
  423. this._parent = parent;
  424. this._idx = idx;
  425. }
  426. // returns a promise of an object with minScale and maxScale values for the feature class
  427. // TODO we may be able to make scale stuff non-asynch. scales are stored in dynamiclayer.layerInfos[idx]
  428. getScaleSet () {
  429. // basic case - we get it from the esri layer
  430. const l = this._parent._layer;
  431. return Promise.resolve({
  432. minScale: l.minScale,
  433. maxScale: l.maxScale
  434. });
  435. }
  436. isOffScale (mapScale) {
  437. return this.getScaleSet().then(scaleSet => {
  438. // GIS for dummies.
  439. // scale increases as you zoom out, decreases as you zoom in
  440. // minScale means if you zoom out beyond this number, hide the layer
  441. // maxScale means if you zoom in past this number, hide the layer
  442. // 0 value for min or max scale means there is no hiding in effect
  443. const result = {
  444. offScale: false,
  445. zoomIn: false
  446. };
  447. // check if out of scale and set zoom direction to scaleSet
  448. if (mapScale < scaleSet.maxScale && scaleSet.maxScale !== 0) {
  449. result.offScale = true;
  450. result.zoomIn = false;
  451. } else if (mapScale > scaleSet.minScale && scaleSet.minScale !== 0) {
  452. result.offScale = true;
  453. result.zoomIn = true;
  454. }
  455. return result;
  456. });
  457. }
  458. // TODO docs
  459. getVisibility () {
  460. return this._parent._layer.visible;
  461. }
  462. // TODO docs
  463. setVisibility (val) {
  464. // basic case - set layer visibility
  465. this._parent._layer.visible = val;
  466. }
  467. getSymbology () {
  468. if (!this._symbology) {
  469. // get symbology from service legend.
  470. // this is used for non-feature based sources (tiles, image, raster).
  471. // wms will override with own special logic.
  472. const url = this._parent._layer.url;
  473. if (url) {
  474. // fetch legend from server, convert to local format, process local format
  475. this._symbology = this._parent._api.symbology.mapServerToLocalLegend(url, this._idx)
  476. .then(legendData => {
  477. return makeSymbologyOutput(makeSymbologyArray(legendData.layers[0]), 'icons');
  478. });
  479. } else {
  480. // this shouldn't happen. non-url layers should be files, which are features,
  481. // which will have a basic renderer and will use FeatureFC override.
  482. throw new Error('encountered layer with no renderer and no url');
  483. }
  484. }
  485. return this._symbology;
  486. }
  487. }
  488. /**
  489. * @class AttribFC
  490. */
  491. class AttribFC extends BasicFC {
  492. // attribute-specific variant for feature class object.
  493. // deals with stuff specific to a feature class that has attributes
  494. // TODO add attribute and layer info promises
  495. /**
  496. * Create an attribute specific feature class object
  497. * @param {Object} parent the Record object that this Feature Class belongs to
  498. * @param {String} idx the service index of this Feature Class. an integer in string format. use '0' for non-indexed sources.
  499. * @param {Object} layerPackage a layer package object from the attribute module for this feature class
  500. */
  501. constructor (parent, idx, layerPackage) {
  502. super(parent, idx);
  503. this._layerPackage = layerPackage;
  504. // moar?
  505. }
  506. /**
  507. * Returns attribute data for this FC.
  508. *
  509. * @function getAttribs
  510. * @returns {Promise} resolves with a layer attribute data object
  511. */
  512. getAttribs () {
  513. return this._layerPackage.getAttribs();
  514. }
  515. /**
  516. * Returns layer-specific data for this FC.
  517. *
  518. * @function getLayerData
  519. * @returns {Promise} resolves with a layer data object
  520. */
  521. getLayerData () {
  522. return this._layerPackage.layerData;
  523. }
  524. getSymbology () {
  525. if (!this._symbology) {
  526. this._symbology = this.getLayerData().then(lData => {
  527. if (lData.layerType === 'Feature Layer') {
  528. // feature always has a single item, so index 0
  529. return makeSymbologyOutput(makeSymbologyArray(lData.legend.layers[0].legend), 'icons');
  530. } else {
  531. // non-feature source. use legend server
  532. return super.getSymbology();
  533. }
  534. });
  535. }
  536. return this._symbology;
  537. }
  538. /**
  539. * Extract the feature name from a feature as best we can.
  540. * Support for dynamic layers is limited at the moment. // TODO explain this comment
  541. *
  542. * @function getFeatureName
  543. * @param {String} objId the object id of the attribute
  544. * @param {Object} attribs optional. the dictionary of attributes for the feature. uses internal attributes if not provided.
  545. * @returns {Promise} resolves with the name of the feature
  546. */
  547. getFeatureName (objId, attribs) {
  548. let nameField = '';
  549. if (this.nameField) {
  550. nameField = this.nameField;
  551. } else if (this.parent._layer && this.parent._layer.displayField) {
  552. nameField = this.parent._layer.displayField;
  553. }
  554. if (nameField) {
  555. // determine if we have been given a set of attributes, or need to use our own
  556. let attribPromise;
  557. if (attribs) {
  558. attribPromise = Promise.resolve(attribs);
  559. } else {
  560. attribPromise = this.getAttribs().then(layerAttribs => {
  561. return layerAttribs.features[layerAttribs.oidIndex[objId]].attributes;
  562. });
  563. }
  564. // after attributes are loaded, extract name
  565. return attribPromise.then(finalAttribs => {
  566. return finalAttribs[nameField];
  567. });
  568. } else {
  569. // FIXME wire in "feature" to translation service
  570. return Promise.resolve('Feature ' + objId);
  571. }
  572. }
  573. /**
  574. * Retrieves attributes from a layer for a specified feature index
  575. * @return {Promise} promise resolving with formatted attributes to be consumed by the datagrid and esri feature identify
  576. */
  577. getFormattedAttributes () {
  578. if (this._formattedAttributes) {
  579. return this._formattedAttributes;
  580. }
  581. this._formattedAttributes = Promise.all([this.getAttribs(), this.getLayerData()])
  582. .then(([aData, lData]) => {
  583. // create columns array consumable by datables
  584. const columns = lData.fields
  585. .filter(field =>
  586. // assuming there is at least one attribute - empty attribute budnle promises should be rejected, so it never even gets this far
  587. // filter out fields where there is no corresponding attribute data
  588. aData.features[0].attributes.hasOwnProperty(field.name))
  589. .map(field => ({
  590. data: field.name,
  591. title: field.alias || field.name
  592. }));
  593. return {
  594. columns,
  595. rows: aData.features.map(feature => feature.attributes),
  596. fields: lData.fields, // keep fields for reference ...
  597. oidField: lData.oidField, // ... keep a reference to id field ...
  598. oidIndex: aData.oidIndex, // ... and keep id mapping array
  599. renderer: lData.renderer
  600. };
  601. })
  602. .catch(() => {
  603. delete this._formattedAttributes; // delete cached promise when the geoApi `getAttribs` call fails, so it will be requested again next time `getAttributes` is called;
  604. throw new Error('Attrib loading failed');
  605. });
  606. return this._formattedAttributes;
  607. }
  608. /**
  609. * Check to see if the attribute in question is an esriFieldTypeDate type.
  610. *
  611. * @param {String} attribName the attribute name we want to check if it's a date or not
  612. * @return {Promise} resolves to true or false based on the attribName type being esriFieldTypeDate
  613. */
  614. checkDateType (attribName) {
  615. // grab attribute info (waiting for it it finish loading)
  616. return this.getLayerData().then(lData => {
  617. // inspect attribute fields
  618. if (lData.fields) {
  619. const attribField = lData.fields.find(field => {
  620. return field.name === attribName;
  621. });
  622. if (attribField && attribField.type) {
  623. return attribField.type === 'esriFieldTypeDate';
  624. }
  625. }
  626. return false;
  627. });
  628. }
  629. /**
  630. * Get the best user-friendly name of a field. Uses alias if alias is defined, else uses the system attribute name.
  631. *
  632. * @param {String} attribName the attribute name we want a nice name for
  633. * @return {Promise} resolves to the best available user friendly attribute name
  634. */
  635. aliasedFieldName (attribName) {
  636. // grab attribute info (waiting for it it finish loading)
  637. return this.getLayerData().then(lData => {
  638. return AttribFC.aliasedFieldNameDirect(attribName, lData.fields);
  639. });
  640. }
  641. static aliasedFieldNameDirect (attribName, fields) {
  642. let fName = attribName;
  643. // search for aliases
  644. if (fields) {
  645. const attribField = fields.find(field => {
  646. return field.name === attribName;
  647. });
  648. if (attribField && attribField.alias && attribField.alias.length > 0) {
  649. fName = attribField.alias;
  650. }
  651. }
  652. return fName;
  653. }
  654. /**
  655. * Convert an attribute set so that any keys using aliases are converted to proper fields
  656. *
  657. * @param {Object} attribs attribute key-value mapping, potentially with aliases as keys
  658. * @param {Array} fields fields definition array for layer
  659. * @return {Object} attribute key-value mapping with fields as keys
  660. */
  661. static unAliasAttribs (attribs, fields) {
  662. const newA = {};
  663. fields.forEach(field => {
  664. // attempt to extract on name. if not found, attempt to extract on alias
  665. // dump value into the result
  666. newA[field.name] = attribs.hasOwnProperty(field.name) ? attribs[field.name] : attribs[field.alias];
  667. });
  668. return newA;
  669. }
  670. // TODO perhaps a splitting of server url and layer index to make things consistent between feature and dynamic?
  671. // could be on constructor, then parent can easily feed in the treats.
  672. }
  673. /**
  674. * @class BasicFC
  675. */
  676. class DynamicFC extends AttribFC {
  677. // dynamic child variant for feature class object.
  678. // deals with stuff specific to dynamic children (i.e. virtual layer on client)
  679. /**
  680. * Create an feature class object for a feature class that is a child of a dynamic layer
  681. * @param {Object} parent the Record object that this Feature Class belongs to
  682. * @param {String} idx the service index of this Feature Class. an integer in string format. use '0' for non-indexed sources.
  683. * @param {Object} layerPackage a layer package object from the attribute module for this feature class
  684. */
  685. constructor (parent, idx, layerPackage) {
  686. super(parent, idx, layerPackage);
  687. // store pointer to the layerinfo for this FC.
  688. // while most information here can also be gleaned from the layer object,
  689. // we cannot know the type (e.g. Feature Layer, Raster Layer), so this object
  690. // is required.
  691. this._layerInfo = parent._layer.layerInfos[idx];
  692. // TODO this may be obsolete now.
  693. this._visible = true; // TODO should be config value or some type of default if auto-gen
  694. }
  695. // returns an object with minScale and maxScale values for the feature class
  696. getScaleSet () {
  697. // get the layerData promise for this FC, wait for it to load,
  698. // then return the scale data
  699. return this.getLayerData().then(lData => {
  700. return {
  701. minScale: lData.minScale,
  702. maxScale: lData.maxScale
  703. };
  704. });
  705. }
  706. // TODO we may need to override some of the methods in AttribFC
  707. // and have logic like
  708. // if this._layerInfo.then(l.layerType === 'Feature Layer') then super(xxx) else non-attrib response
  709. //
  710. // could be tricky, as it is promised based, thus wrecking the override of any synchronous function
  711. setVisibility (val) {
  712. // update visible layers array
  713. const vLayers = this._parent.visibleLayers;
  714. const intIdx = parseInt(this._idx);
  715. const vIdx = vLayers.indexOf(intIdx);
  716. if (val && vIdx === -1) {
  717. // was invisible, now visible
  718. vLayers.push(intIdx);
  719. } else if (!val && vIdx > -1) {
  720. // was visible, now invisible
  721. vLayers.splice(vIdx, 1);
  722. }
  723. }
  724. // TODO extend this function to other FC's? do they need it?
  725. getVisibility () {
  726. return this._parent.visibleLayers.indexOf(parseInt(this._idx)) > -1;
  727. }
  728. }
  729. /**
  730. * Searches for a layer title defined by a wms.
  731. * @function getWMSLayerTitle
  732. * @private
  733. * @param {Object} wmsLayer esri layer object for the wms
  734. * @param {String} wmsLayerId layers id as defined in the wms (i.e. not wmsLayer.id)
  735. * @return {String} layer title as defined on the service, '' if no title defined
  736. */
  737. function getWMSLayerTitle(wmsLayer, wmsLayerId) {
  738. // TODO move this to ogc.js module?
  739. // crawl esri layerInfos (which is a nested structure),
  740. // returns sublayer that has matching id or null if not found.
  741. // written as function to allow recursion
  742. const crawlSubLayers = (subLayerInfos, wmsLayerId) => {
  743. let targetEntry = null;
  744. // we use .some to allow the search to stop when we find something
  745. subLayerInfos.some(layerInfo => {
  746. // wms ids are stored in .name
  747. if (layerInfo.name === wmsLayerId) {
  748. // found it. save it and exit the search
  749. targetEntry = layerInfo;
  750. return true;
  751. } else if (layerInfo.subLayers) {
  752. // search children. if in children, will exit search, else will continue
  753. return crawlSubLayers(layerInfo.subLayers, wmsLayerId);
  754. } else {
  755. // continue search
  756. return false;
  757. }
  758. });
  759. return targetEntry;
  760. };
  761. // init search on root layerInfos, then process result
  762. const match = crawlSubLayers(wmsLayer.layerInfos, wmsLayerId);
  763. if (match && match.title) {
  764. return match.title;
  765. } else {
  766. return ''; // falsy!
  767. }
  768. }
  769. /**
  770. * @class WmsFC
  771. */
  772. class WmsFC extends BasicFC {
  773. getSymbology () {
  774. if (!this._symbology) {
  775. const configLayerEntries = this._parent.config.layerEntries;
  776. const gApi = this._parent._api;
  777. const legendArray = gApi.layer.ogc
  778. .getLegendUrls(this._parent._layer, configLayerEntries.map(le => le.id))
  779. .map((imageUri, idx) => {
  780. const symbologyItem = {
  781. name: null,
  782. svgcode: null
  783. };
  784. // config specified name || server specified name || config id
  785. const name = configLayerEntries[idx].name ||
  786. getWMSLayerTitle(this._parent._layer, configLayerEntries[idx].id) ||
  787. configLayerEntries[idx].id;
  788. gApi.symbology.generateWMSSymbology(name, imageUri).then(data => {
  789. symbologyItem.name = data.name;
  790. symbologyItem.svgcode = data.svgcode;
  791. });
  792. return symbologyItem;
  793. });
  794. this._symbology = Promise.resolve(makeSymbologyOutput(legendArray, 'images'));
  795. }
  796. return this._symbology;
  797. }
  798. }
  799. /**
  800. * @class IdentifyResult
  801. */
  802. class IdentifyResult {
  803. /**
  804. * @param {String} name layer name of the queried layer
  805. * @param {Array} symbology array of layer symbology to be displayed in details panel
  806. * @param {String} format indicates data formating template
  807. * @param {Object} layerRec layer record for the queried layer
  808. * @param {Integer} featureIdx optional feature index of queried layer (should be provided for attribute based layers)
  809. * @param {String} caption optional captions to be displayed along with the name
  810. */
  811. constructor (name, symbology, format, layerRec, featureIdx, caption) {
  812. // TODO revisit what should be in this class, and what belongs in the app
  813. // also what can be abstacted to come from layerRec
  814. this.isLoading = true;
  815. this.requestId = -1;
  816. this.requester = {
  817. name,
  818. symbology,
  819. format,
  820. caption,
  821. layerRec,
  822. featureIdx
  823. };
  824. this.data = [];
  825. }
  826. }
  827. // the Record classes are meant to be public facing and consumed by other modules and the client.
  828. /**
  829. * @class LayerRecord
  830. */
  831. class LayerRecord {
  832. // NOTE: we used to override layerClass in each specific class.
  833. // since we require the class in the generic constructor,
  834. // and since it was requested that the esri class be passed in
  835. // as a constructor parameter instead of holding a ref to the esriBundle,
  836. // and since you must call `super` first in a constructor,
  837. // it was impossible to assign the specific class before the generic
  838. // constructor executed, resulting in null-dereferences.
  839. // this approach solves the problem.
  840. get layerClass () { return this._layerClass; }
  841. get config () { return this.initialConfig; } // TODO: add a live config reference if needed
  842. get legendEntry () { return this._legendEntry; } // legend entry class corresponding to those defined in legend entry service
  843. set legendEntry (value) { this._legendEntry = value; } // TTODO: determine if we still link legends inside this class
  844. get bbox () { return this._bbox; } // bounding box layer
  845. get state () { return this._state; }
  846. set state (value) { this._state = value; }
  847. get layerId () { return this.config.id; }
  848. get _layerPassthroughBindings () { return ['setOpacity', 'setVisibility']; } // TODO when jshint parses instance fields properly we can change this from a property to a field
  849. get _layerPassthroughProperties () { return ['visibleAtMapScale', 'visible', 'spatialReference']; } // TODO when jshint parses instance fields properly we can change this from a property to a field
  850. get userLayer () { return this._user; } // indicates if layer was added by a user
  851. set userLayer (value) { this._user = value; }
  852. get layerName () { return this._name; } // the top level layer name
  853. set layerName (value) { this._name = value; }
  854. /**
  855. * Generate a bounding box for the layer on the given map.
  856. */
  857. createBbox (map) {
  858. if (this._bbox) {
  859. throw new Error('Bbox is already setup');
  860. }
  861. this._bbox = this._apiRef.layer.bbox.makeBoundingBox(`bbox_${this._layer.id}`,
  862. this._layer.fullExtent,
  863. map.extent.spatialReference);
  864. map.addLayer(this._bbox);
  865. }
  866. /**
  867. * Destroy bounding box
  868. */
  869. destroyBbox (map) {
  870. map.removeLayer(this._bbox);
  871. this._bbox = undefined;
  872. }
  873. /**
  874. * Attach event handlers to layer events
  875. */
  876. bindEvents (layer) {
  877. // TODO optional refactor. Rather than making the events object in the parameter,
  878. // do it as a variable, and only add mouse-over, mouse-out events if we are
  879. // in an app configuration that will use it. May save a bit of processing
  880. // by not having unused events being handled and ignored.
  881. // Second optional thing. Call a separate wrapEvents in FeatuerRecord class
  882. this._apiRef.events.wrapEvents(layer, {
  883. // wrapping the function calls to keep `this` bound correctly
  884. load: () => this.onLoad(),
  885. error: e => this.onError(e),
  886. 'update-start': () => this.onUpdateStart(),
  887. 'update-end': () => this.onUpdateEnd(),
  888. 'mouse-over': e => this.onMouseOver(e),
  889. 'mouse-out': e => this.onMouseOut(e)
  890. });
  891. }
  892. /**
  893. * Perform layer initialization tasks
  894. */
  895. constructLayer () {
  896. this._layer = this.layerClass(this.config.url, this.makeLayerConfig());
  897. this.bindEvents(this._layer);
  898. return this._layer;
  899. }
  900. /**
  901. * Handle a change in layer state
  902. */
  903. _stateChange (newState) {
  904. this._state = newState;
  905. console.log(`State change for ${this.layerId} to ${newState}`);
  906. // if we don't copy the array we could be looping on an array
  907. // that is being modified as it is being read
  908. this._fireEvent(this._stateListeners, this._state);
  909. }
  910. /**
  911. * Wire up state change listener
  912. */
  913. addStateListener (listenerCallback) {
  914. this._stateListeners.push(listenerCallback);
  915. return listenerCallback;
  916. }
  917. /**
  918. * Remove a state change listener
  919. */
  920. removeStateListener (listenerCallback) {
  921. const idx = this._stateListeners.indexOf(listenerCallback);
  922. if (idx < 0) {
  923. throw new Error('Attempting to remove a listener which is not registered.');
  924. }
  925. this._stateListeners.splice(idx, 1);
  926. }
  927. /**
  928. * Wire up mouse hover listener
  929. */
  930. addHoverListener (listenerCallback) {
  931. this._hoverListeners.push(listenerCallback);
  932. return listenerCallback;
  933. }
  934. /**
  935. * Remove a mouse hover listener
  936. */
  937. removeHoverListener (listenerCallback) {
  938. const idx = this._hoverListeners.indexOf(listenerCallback);
  939. if (idx < 0) {
  940. throw new Error('Attempting to remove a listener which is not registered.');
  941. }
  942. this._hoverListeners.splice(idx, 1);
  943. }
  944. /**
  945. * Triggers when the layer loads.
  946. *
  947. * @function onLoad
  948. */
  949. onLoad () {
  950. if (this.legendEntry && this.legendEntry.removed) { return; }
  951. console.info(`Layer loaded: ${this._layer.id}`);
  952. let lookupPromise = Promise.resolve();
  953. if (this._epsgLookup) {
  954. const check = this._apiRef.proj.checkProj(this.spatialReference, this._epsgLookup);
  955. if (check.lookupPromise) {
  956. lookupPromise = check.lookupPromise;
  957. }
  958. // TODO if we don't find a projection, the app will show the layer loading forever.
  959. // might need to handle the fail case and show something to the user.
  960. }
  961. lookupPromise.then(() => this._stateChange(states.LOADED));
  962. }
  963. /**
  964. * Handles when the layer has an error
  965. */
  966. onError (e) {
  967. console.warn(`Layer error: ${e}`);
  968. console.warn(e);
  969. this._stateChange(states.ERROR);
  970. }
  971. /**
  972. * Handles when the layer starts to update
  973. */
  974. onUpdateStart () {
  975. this._stateChange(states.REFRESH);
  976. }
  977. /**
  978. * Handles when the layer finishes updating
  979. */
  980. onUpdateEnd () {
  981. this._stateChange(states.LOADED);
  982. }
  983. /**
  984. * Handles when the mouse enters a layer
  985. */
  986. onMouseOver () {
  987. // do nothing in baseclass
  988. }
  989. /**
  990. * Handles when the mouse leaves a layer
  991. */
  992. onMouseOut () {
  993. // do nothing in baseclass
  994. }
  995. /**
  996. * Utility for triggering an event and giving it to the listeners
  997. */
  998. _fireEvent (handlerArray, ...eventParams) {
  999. handlerArray.slice(0).forEach(l => l(...eventParams));
  1000. }
  1001. /**
  1002. * Creates an options object for the physical layer
  1003. */
  1004. makeLayerConfig () {
  1005. return {
  1006. id: this.config.id,
  1007. opacity: this.config.state.opacity,
  1008. visible: this.config.state.visibility
  1009. };
  1010. }
  1011. /**
  1012. * Indicates if the bounding box is visible
  1013. *
  1014. * @returns {Boolean} indicates if the bounding box is visible
  1015. */
  1016. isBBoxVisible () {
  1017. if (this._bbox) {
  1018. return this._bbox.visible;
  1019. } else {
  1020. return false;
  1021. }
  1022. }
  1023. /**
  1024. * Figure out visibility scale. Will use layer minScale/maxScale
  1025. * and map levels of detail to determine scale boundaries.
  1026. *
  1027. * @param {Array} lods array of valid levels of detail for the map
  1028. * @param {Object} scaleSet contains .minScale and .maxScale for valid viewing scales
  1029. * @param {Boolean} zoomIn the zoom to scale direction; true need to zoom in; false need to zoom out
  1030. * @param {Boolean} zoomGraphic an optional value when zoomToScale is use to zoom to a graphic element;
  1031. * true used to zoom to a graphic element; false not used to zoom to a graphic element
  1032. * @returns {Object} a level of detail (lod) object for the appropriate scale to zoom to
  1033. */
  1034. findZoomScale (lods, scaleSet, zoomIn, zoomGraphic = false) {
  1035. // TODO rename function to getZoomScale?
  1036. // TODO take a second look at parameters zoomIn and zoomGraphic. how are they derived (in the caller code)?
  1037. // seems weird to me to do it this way
  1038. // TODO naming of "zoomIn" is very misleading and confusing. in practice, we are often
  1039. // setting the value to false when we are zooming down close to the ground.
  1040. // Need full analysis of usage, possibly rename parameter or update param docs.
  1041. // TODO update function parameters once things are working
  1042. // if the function is used to zoom to a graphic element and the layer is out of scale we always want
  1043. // the layer to zoom to the maximum scale allowed for the layer. In this case, zoomIn must be
  1044. // always false
  1045. zoomIn = (zoomGraphic) ? false : zoomIn;
  1046. // TODO double-check where lods are coming from in old code
  1047. // change search order of lods depending if we are zooming in or out
  1048. const modLods = zoomIn ? lods : [...lods].reverse();
  1049. return modLods.find(currentLod => zoomIn ? currentLod.scale < scaleSet.minScale :
  1050. currentLod.scale > scaleSet.maxScale);
  1051. }
  1052. /**
  1053. * Set map scale depending on zooming in or zooming out of layer visibility scale
  1054. *
  1055. * @param {Object} map layer to zoom to scale to for feature layers; parent layer for dynamic layers
  1056. * @param {Object} lod scale object the map will be set to
  1057. * @param {Boolean} zoomIn the zoom to scale direction; true need to zoom in; false need to zoom out
  1058. * @returns {Promise} resolves after map is done changing its extent
  1059. */
  1060. setMapScale (map, lod, zoomIn) {
  1061. // TODO possible this would live in the map manager in a bigger refactor.
  1062. // NOTE because we utilize the layer object's full extent (and not child feature class extents),
  1063. // this function stays in this class.
  1064. // if zoom in is needed; must find center of layer's full extent and perform center&zoom
  1065. if (zoomIn) {
  1066. // need to reproject in case full extent in a different sr than basemap
  1067. const gextent = this._apiRef.proj.localProjectExtent(this._layer.fullExtent, map.spatialReference);
  1068. const reprojLayerFullExt = this._apiRef.mapManager.Extent(gextent.x0, gextent.y0,
  1069. gextent.x1, gextent.y1, gextent.sr);
  1070. // check if current map extent already in layer extent
  1071. return map.setScale(lod.scale).then(() => {
  1072. // if map extent not in layer extent, zoom to center of layer extent
  1073. // don't need to return Deferred otherwise because setScale already resolved here
  1074. if (!reprojLayerFullExt.intersects(map.extent)) {
  1075. return map.centerAt(reprojLayerFullExt.getCenter());
  1076. }
  1077. });
  1078. } else {
  1079. return map.setScale(lod.scale);
  1080. }
  1081. }
  1082. /**
  1083. * Figure out visibility scale and zoom to it. Will use layer minScale/maxScale
  1084. * and map levels of detail to determine scale boundaries.
  1085. *
  1086. * @private
  1087. * @param {Object} map the map object
  1088. * @param {Array} lods level of details array for basemap
  1089. * @param {Boolean} zoomIn the zoom to scale direction; true need to zoom in; false need to zoom out
  1090. * @param {Object} scaleSet contains min and max scales for the layer.
  1091. * @param {Boolean} zoomGraphic an optional value when zoomToScale is use to zoom to a graphic element;
  1092. * true used to zoom to a graphic element; false not used to zoom to a graphic element
  1093. */
  1094. _zoomToScaleSet (map, lods, zoomIn, scaleSet, zoomGraphic = false) {
  1095. // TODO update function parameters once things are working
  1096. // if the function is used to zoom to a graphic element and the layer is out of scale we always want
  1097. // the layer to zoom to the maximum scale allowed for the layer. In this case, zoomIn must be
  1098. // always false
  1099. zoomIn = (zoomGraphic) ? false : zoomIn;
  1100. // NOTE we use lods provided by config rather that system-ish map.__tileInfo.lods
  1101. const zoomLod = this.findZoomScale(lods, scaleSet, zoomIn, zoomGraphic = false);
  1102. // TODO ponder on the implementation of this
  1103. return this.setMapScale(this._layer, zoomLod, zoomIn);
  1104. }
  1105. zoomToScale (map, lods, zoomIn, zoomGraphic = false) {
  1106. // get scale set from child, then execute zoom
  1107. return this._featClasses[this._defaultFC].getScaleSet().then(scaleSet => {
  1108. return this._zoomToScaleSet(map, lods, zoomIn, scaleSet, zoomGraphic);
  1109. });
  1110. }
  1111. isOffScale (mapScale) {
  1112. return this._featClasses[this._defaultFC].isOffScale(mapScale);
  1113. }
  1114. /**
  1115. * Zoom to layer boundary of the layer specified by layerId
  1116. * @param {Object} map map object we want to execute the zoom on
  1117. * @return {Promise} resolves when map is done zooming
  1118. */
  1119. zoomToBoundary (map) {
  1120. // TODO add some caching? make sure it will get wiped if we end up changing projections
  1121. // or use wkid as caching key?
  1122. // NOTE this function uses the full extent property of the layer object. it does not
  1123. // drill into extents of sub-layers of dynamic layers
  1124. const l = this._layer;
  1125. let gextent;
  1126. // some user added layers have the fullExtent field, but the properties in it are undefined. Check to see if the fullExtent properties are present
  1127. if (!l.fullExtent.xmin) {
  1128. // TODO make this code block more robust? check that we have graphics?
  1129. gextent = this._apiRef.proj.localProjectExtent(
  1130. this._apiRef.proj.graphicsUtils.graphicsExtent(l.graphics), map.spatialReference);
  1131. } else {
  1132. gextent = this._apiRef.proj.localProjectExtent(l.fullExtent, map.spatialReference);
  1133. }
  1134. const reprojLayerFullExt = this._apiRef.mapManager.Extent(gextent.x0, gextent.y0,
  1135. gextent.x1, gextent.y1, gextent.sr);
  1136. return map.setExtent(reprojLayerFullExt);
  1137. }
  1138. /**
  1139. * Returns the visible scale values of the layer
  1140. * @returns {Promise} resolves in object properties .minScale and .maxScale
  1141. */
  1142. getVisibleScales () {
  1143. // default layer, take from layer object
  1144. return Promise.resolve({
  1145. minScale: this._layer.minScale,
  1146. maxScale: this._layer.maxScale
  1147. });
  1148. }
  1149. /**
  1150. * Returns the feature count
  1151. * @returns {Promise} resolves feature count
  1152. */
  1153. getFeatureCount () {
  1154. // TODO determine best result to indicate that layer does not have features
  1155. // we may want a null so that UI can display a different message (or suppress the message)
  1156. return Promise.resolve(0);
  1157. }
  1158. /**
  1159. * Create an extent centered around a point, that is appropriate for the current map scale.
  1160. * @param {Object} point point on the map for extent center
  1161. * @param {Object} map map object the extent is relevant for
  1162. * @param {Integer} tolerance optional. distance in pixels from mouse point that qualifies as a hit. default is 5
  1163. * @return {Object} an extent of desired size and location
  1164. */
  1165. makeClickBuffer (point, map, tolerance = 5) {
  1166. // take pixel tolerance, convert to map units at current scale. x2 to turn radius into diameter
  1167. const buffSize = 2 * tolerance * map.extent.getWidth() / map.width;
  1168. // Build tolerance envelope of correct size
  1169. const cBuff = new this._api.mapManager.Extent(0, 0, buffSize, buffSize, point.spatialReference);
  1170. // move the envelope so it is centered around the point
  1171. return cBuff.centerAt(point);
  1172. }
  1173. // TODO docs
  1174. isQueryable () {
  1175. return this._featClasses[this._defaultFC].queryable;
  1176. }
  1177. // TODO docs
  1178. setQueryable (value) {
  1179. this._featClasses[this._defaultFC].queryable = value;
  1180. }
  1181. getGeomType () {
  1182. return this._featClasses[this._defaultFC].geomType;
  1183. }
  1184. // returns the proxy interface object for the root of the layer (i.e. main entry in legend, not nested child things)
  1185. // TODO docs
  1186. getProxy () {
  1187. // TODO figure out control name arrays from config (specifically, disabled list)
  1188. // updated config schema uses term "enabled" but have a feeling it really means available
  1189. // TODO figure out how placeholders work with all this
  1190. // TODO does this even make sense in the baseclass anymore? Everything *should* be overriding this.
  1191. if (!this._rootProxy) {
  1192. this._rootProxy = new LayerInterface(this, this.initialConfig.controls);
  1193. this._rootProxy.convertToSingleLayer(this);
  1194. }
  1195. return this._rootProxy;
  1196. }
  1197. /**
  1198. * Create a layer record with the appropriate geoApi layer type. Layer config
  1199. * should be fully merged with all layer options defined (i.e. this constructor
  1200. * will not apply any defaults).
  1201. * @param {Object} layerClass the ESRI api object for the layer
  1202. * @param {Object} apiRef object pointing to the geoApi. allows us to call other geoApi functions.
  1203. * @param {Object} config layer config values
  1204. * @param {Object} esriLayer an optional pre-constructed layer
  1205. * @param {Function} epsgLookup an optional lookup function for EPSG codes (see geoService for signature)
  1206. */
  1207. constructor (layerClass, apiRef, config, esriLayer, epsgLookup) {
  1208. this._layerClass = layerClass;
  1209. this._featClasses = {}; // TODO how to populate first one
  1210. this._defaultFC = '0'; // TODO how to populate first one TODO check if int or string
  1211. this._apiRef = apiRef;
  1212. this.initialConfig = config;
  1213. this._stateListeners = [];
  1214. this._hoverListeners = [];
  1215. this._user = false;
  1216. this._epsgLookup = epsgLookup;
  1217. this._layerPassthroughBindings.forEach(bindingName =>
  1218. this[bindingName] = (...args) => this._layer[bindingName](...args));
  1219. this._layerPassthroughProperties.forEach(propName => {
  1220. const descriptor = {
  1221. enumerable: true,
  1222. get: () => this._layer[propName]
  1223. };
  1224. Object.defineProperty(this, propName, descriptor);
  1225. });
  1226. if (esriLayer) {
  1227. this.constructLayer = () => { throw new Error('Cannot construct pre-made layers'); };
  1228. this._layer = esriLayer;
  1229. this.bindEvents(this._layer);
  1230. this._state = states.LOADED;
  1231. } else {
  1232. this.constructLayer(config);
  1233. this._state = states.LOADING;
  1234. }
  1235. }
  1236. }
  1237. /**
  1238. * @class AttrRecord
  1239. */
  1240. class AttrRecord extends LayerRecord {
  1241. // this class has functions common to layers that have attributes
  1242. // FIXME clickTolerance is not specific to AttrRecord but rather Feature and Dynamic
  1243. get clickTolerance () { return this.config.tolerance; }
  1244. /**
  1245. * Create a layer record with the appropriate geoApi layer type. Layer config
  1246. * should be fully merged with all layer options defined (i.e. this constructor
  1247. * will not apply any defaults).
  1248. * @param {Object} layerClass the ESRI api object for the layer
  1249. * @param {Object} esriRequest the ESRI api object for making web requests with proxy support
  1250. * @param {Object} apiRef object pointing to the geoApi. allows us to call other geoApi functions.
  1251. * @param {Object} config layer config values
  1252. * @param {Object} esriLayer an optional pre-constructed layer
  1253. * @param {Function} epsgLookup an optional lookup function for EPSG codes (see geoService for signature)
  1254. */
  1255. constructor (layerClass, esriRequest, apiRef, config, esriLayer, epsgLookup) {
  1256. super(layerClass, apiRef, config, esriLayer, epsgLookup);
  1257. this._esriRequest = esriRequest;
  1258. }
  1259. /**
  1260. * Get the best user-friendly name of a field. Uses alias if alias is defined, else uses the system attribute name.
  1261. *
  1262. * @param {String} attribName the attribute name we want a nice name for
  1263. * @return {Promise} resolves to the best available user friendly attribute name
  1264. */
  1265. aliasedFieldName (attribName) {
  1266. return this._featClasses[this._defaultFC].aliasedFieldName(attribName);
  1267. }
  1268. /**
  1269. * Retrieves attributes from a layer for a specified feature index
  1270. * @return {Promise} promise resolving with formatted attributes to be consumed by the datagrid and esri feature identify
  1271. */
  1272. getFormattedAttributes () {
  1273. return this._featClasses[this._defaultFC].getFormattedAttributes();
  1274. }
  1275. checkDateType (attribName) {
  1276. return this._featClasses[this._defaultFC].checkDateType(attribName);
  1277. }
  1278. /**
  1279. * Returns attribute data for this layer.
  1280. *
  1281. * @function getAttribs
  1282. * @returns {Promise} resolves with a layer attribute data object
  1283. */
  1284. getAttribs () {
  1285. return this._featClasses[this._defaultFC].getAttribs();
  1286. }
  1287. /**
  1288. * Returns layer-specific data for this Record
  1289. *
  1290. * @function getLayerData
  1291. * @returns {Promise} resolves with a layer data object
  1292. */
  1293. getLayerData () {
  1294. return this._featClasses[this._defaultFC].getLayerData();
  1295. }
  1296. getFeatureName (objId, attribs) {
  1297. return this._featClasses[this._defaultFC].getFeatureName(objId, attribs);
  1298. }
  1299. getSymbology () {
  1300. return this._featClasses[this._defaultFC].getSymbology();
  1301. }
  1302. getFeatureCount (url) {
  1303. if (url) {
  1304. // wrapping server call in a function, as we regularly encounter sillyness
  1305. // where we need to execute the count request twice.
  1306. // having a function (with finalTry flag) lets us handle the double-request
  1307. const esriServerCount = (layerUrl, finalTry = false) => {
  1308. // extract info for this service
  1309. const defService = this._esriRequest({
  1310. url: `${layerUrl}/query`,
  1311. content: {
  1312. f: 'json',
  1313. where: '1=1',
  1314. returnCountOnly: true,
  1315. returnGeometry: false
  1316. },
  1317. callbackParamName: 'callback',
  1318. handleAs: 'json',
  1319. });
  1320. return new Promise((resolve, reject) => {
  1321. defService.then(serviceResult => {
  1322. if (serviceResult && (typeof serviceResult.error === 'undefined') &&
  1323. (typeof serviceResult.count !== 'undefined')) {
  1324. // we got a row count
  1325. resolve(serviceResult.count);
  1326. } else if (!finalTry) {
  1327. // do a second attempt
  1328. resolve(esriServerCount(layerUrl, true));
  1329. } else {
  1330. // TODO different message? more verbose?
  1331. reject('error getting feature count');
  1332. }
  1333. }, error => {
  1334. // failed to load service info.
  1335. // TODO any tricks to avoid duplicating the error case in both blocks?
  1336. if (!finalTry) {
  1337. // do a second attempt
  1338. resolve(esriServerCount(layerUrl, true));
  1339. } else {
  1340. // TODO different message? more verbose?
  1341. console.warn(error);
  1342. reject('error getting feature count');
  1343. }
  1344. });
  1345. });
  1346. };
  1347. return esriServerCount(url);
  1348. } else {
  1349. // file based layer. count local features
  1350. return Promise.resolve(this._layer.graphics.length);
  1351. }
  1352. }
  1353. /**
  1354. * Transforms esri key-value attribute object into key value array with format suitable
  1355. * for consumption by the details pane.
  1356. *
  1357. * @param {Object} attribs attribute key-value mapping, potentially with aliases as keys
  1358. * @param {Array} fields optional. fields definition array for layer. no aliasing done if not provided
  1359. * @return {Array} attribute data transformed into a list, with potential field aliasing applied
  1360. */
  1361. attributesToDetails (attribs, fields) {
  1362. // TODO make this extensible / modifiable / configurable to allow different details looks for different data
  1363. // simple array of text mapping for demonstration purposes. fancy grid formatting later?
  1364. return Object.keys(attribs)
  1365. .map(key => {
  1366. const fieldType = fields ? fields.find(f => f.name === key) : null;
  1367. return {
  1368. key: AttribFC.aliasedFieldNameDirect(key, fields), // need synchronous variant of alias lookup
  1369. value: attribs[key],
  1370. type: fieldType ? fieldType.type : fieldType
  1371. };
  1372. });
  1373. }
  1374. }
  1375. /**
  1376. * @class ImageRecord
  1377. */
  1378. class ImageRecord extends LayerRecord {
  1379. // NOTE: if we decide to support attributes from ImageServers,
  1380. // we would extend from AttrRecord instead of LayerRecord
  1381. // (and do a lot of testing!)
  1382. /**
  1383. * Create a layer record with the appropriate geoApi layer type. Layer config
  1384. * should be fully merged with all layer options defined (i.e. this constructor
  1385. * will not apply any defaults).
  1386. * @param {Object} layerClass the ESRI api object for image server layers
  1387. * @param {Object} apiRef object pointing to the geoApi. allows us to call other geoApi functions.
  1388. * @param {Object} config layer config values
  1389. * @param {Object} esriLayer an optional pre-constructed layer
  1390. * @param {Function} epsgLookup an optional lookup function for EPSG codes (see geoService for signature)
  1391. */
  1392. constructor (layerClass, apiRef, config, esriLayer, epsgLookup) {
  1393. // TODO if we have nothing to add here, delete this constructor
  1394. super(apiRef, config, esriLayer, epsgLookup);
  1395. }
  1396. /**
  1397. * Triggers when the layer loads.
  1398. *
  1399. * @function onLoad
  1400. */
  1401. onLoad () {
  1402. super.onLoad();
  1403. // TODO consider making this a function, as it is common across less-fancy layers
  1404. this._defaultFC = '0';
  1405. this._featClasses['0'] = new BasicFC(this, '0');
  1406. }
  1407. }
  1408. /**
  1409. * @class DynamicRecord
  1410. */
  1411. class DynamicRecord extends AttrRecord {
  1412. get _layerPassthroughBindings () {
  1413. return ['setOpacity', 'setVisibility', 'setVisibleLayers', 'setLayerDrawingOptions'];
  1414. }
  1415. get _layerPassthroughProperties () {
  1416. return ['visibleAtMapScale', 'visible', 'spatialReference', 'layerInfos', 'supportsDynamicLayers'];
  1417. }
  1418. /**
  1419. * Create a layer record with the appropriate geoApi layer type. Layer config
  1420. * should be fully merged with all layer options defined (i.e. this constructor
  1421. * will not apply any defaults).
  1422. * @param {Object} layerClass the ESRI api object for dynamic layers
  1423. * @param {Object} esriRequest the ESRI api object for making web requests with proxy support
  1424. * @param {Object} apiRef object pointing to the geoApi. allows us to call other geoApi functions
  1425. * @param {Object} config layer config values
  1426. * @param {Object} esriLayer an optional pre-constructed layer
  1427. * @param {Function} epsgLookup an optional lookup function for EPSG codes (see geoService for signature)
  1428. */
  1429. constructor (layerClass, esriRequest, apiRef, config, esriLayer, epsgLookup) {
  1430. super(layerClass, esriRequest, apiRef, config, esriLayer, epsgLookup);
  1431. this.ArcGISDynamicMapServiceLayer = layerClass;
  1432. // TODO what is the case where we have dynamic layer already prepared
  1433. // and passed in? Generally this only applies to file layers (which
  1434. // are feature layers).
  1435. // TODO figure out controls on config
  1436. // TODO worry about placeholders. WORRY. how does that even work here?
  1437. this._proxies = {};
  1438. }
  1439. /**
  1440. * Return a proxy interface for a child layer
  1441. *
  1442. * @param {Integer} featureIdx index of child entry (leaf or group)
  1443. * @return {Object} proxy interface for given child
  1444. */
  1445. getChildProxy (featureIdx) {
  1446. // TODO verify we have integer coming in and not a string
  1447. // in this case, featureIdx can also be a group index
  1448. if (this._proxies[featureIdx.toString()]) {
  1449. return this._proxies[featureIdx.toString()];
  1450. } else {
  1451. throw new Error(`attempt to get non-existing child proxy. Index ${featureIdx}`);
  1452. }
  1453. }
  1454. // TODO docs
  1455. getFeatureCount (featureIdx) {
  1456. // point url to sub-index we want
  1457. // TODO might change how we manage index and url
  1458. return super.getFeatureCount(this._layer.url + '/' + featureIdx);
  1459. }
  1460. /**
  1461. * Triggers when the layer loads.
  1462. *
  1463. * @function onLoad
  1464. */
  1465. onLoad () {
  1466. super.onLoad();
  1467. // TODO worry about structured legend. how is that defined in a config?
  1468. // this code here is doing auto-fill. we might need to not do this
  1469. // for structured legend.
  1470. // TODO do we need to do config defaulting here?
  1471. // e.g. a group may be defined in the config. if there is
  1472. // no specific config items for the children of the group,
  1473. // should we be copying the parent values and using those
  1474. // as initial values?
  1475. // this subfunction will recursively crawl a dynamic layerInfo structure.
  1476. // it will generate proxy objects for all groups and leafs under the
  1477. // input layerInfo.
  1478. // it also collects and returns an array of leaf nodes so each group
  1479. // can store it and have fast access to all leaves under it.
  1480. const processLayerInfo = (layerInfo, layerProxies) => {
  1481. if (layerInfo.subLayerIds && layerInfo.subLayerIds.length > 0) {
  1482. // group
  1483. // TODO probably need some placeholder magic going on here too
  1484. // TODO figure out control lists, whats available, whats disabled.
  1485. // supply on second and third parameters
  1486. const group = new LayerInterface();
  1487. group.convertToDynamicGroup(this, layerInfo.id.toString());
  1488. layerProxies[layerInfo.id.toString()] = group;
  1489. // process the kids in the group.
  1490. // store the child leaves in the internal variable
  1491. layerInfo.subLayerIds.forEach(slid => {
  1492. group._childLeafs = group._childLeafs.concat(
  1493. processLayerInfo(this._layer.layerInfos[slid], layerProxies));
  1494. });
  1495. return group._childLeafs;
  1496. } else {
  1497. // leaf
  1498. // TODO figure out control lists, whats available, whats disabled.
  1499. // supply on second and third parameters.
  1500. // might need to steal from parent, since auto-gen may not have explicit
  1501. // config settings.
  1502. const leaf = new LayerInterface();
  1503. leaf.convertToDynamicLeaf(new PlaceholderFC(this, layerInfo.name));
  1504. layerProxies[layerInfo.id.toString()] = leaf;
  1505. return [leaf];
  1506. }
  1507. };
  1508. if (this.config.layerEntries) {
  1509. this.config.layerEntries.forEach(le => {
  1510. processLayerInfo(this._layer.layerInfos[le.index], this._proxies);
  1511. });
  1512. }
  1513. // trigger attribute load and set up children bundles.
  1514. // TODO do we need an options object, with .skip set for sub-layers we are not dealing with?
  1515. // would need to inspect all leafs in this._layer.layerInfos,
  1516. // then cross reference against incoming config. extra code probably
  1517. // needed to derive auto-gen childs that are not explicitly in config.
  1518. // Alternate: figure all this out on constructor, as we might need placeholders????
  1519. // update: we are doing this, but it gives us a list of things to keep,
  1520. // not to skip.
  1521. // Alternate: add new option that is opposite of .skip. Will be more of a
  1522. // .only, and we won't have to derive a "skip" set from our inclusive
  1523. // list that was created in the ._proxies
  1524. const attributeBundle = this._apiRef.attribs.loadLayerAttribs(this._layer);
  1525. // idx is a string
  1526. attributeBundle.indexes.forEach(idx => {
  1527. // TODO need to worry about Raster Layers here. DynamicFC is based off of
  1528. // attribute things.
  1529. // TODO need to pass some type of initial state to these FCs (e.g. queryable)
  1530. this._featClasses[idx] = new DynamicFC(this, idx, attributeBundle[idx]);
  1531. // if we have a proxy watching this leaf, replace its placeholder with the real data
  1532. if (this._proxies[idx]) {
  1533. this._proxies[idx].updateSource(this._featClasses[idx]);
  1534. }
  1535. });
  1536. // TODO after config defaulting for any autogen children,
  1537. // need to get list of visible leaves and manually set
  1538. // the layer doing this._layer.setVisibleLayers([visible indexes]) .
  1539. // possibly need to do something similar for opacity (if supported)
  1540. }
  1541. // override to add child index parameter
  1542. zoomToScale (childIdx, map, lods, zoomIn, zoomGraphic = false) {
  1543. // get scale set from child, then execute zoom
  1544. return this._featClasses[childIdx].getScaleSet().then(scaleSet => {
  1545. return this._zoomToScaleSet(map, lods, zoomIn, scaleSet, zoomGraphic);
  1546. });
  1547. }
  1548. isOffScale (childIdx, mapScale) {
  1549. return this._featClasses[childIdx].isOffScale(mapScale);
  1550. }
  1551. isQueryable (childIdx) {
  1552. return this._featClasses[childIdx].queryable;
  1553. }
  1554. getGeomType (childIdx) {
  1555. return this._featClasses[childIdx].geomType;
  1556. }
  1557. /**
  1558. * Get the best user-friendly name of a field. Uses alias if alias is defined, else uses the system attribute name.
  1559. *
  1560. * @param {String} attribName the attribute name we want a nice name for
  1561. * @param {String} childIndex index of the child layer whos attributes we are looking at
  1562. * @return {Promise} resolves to the best available user friendly attribute name
  1563. */
  1564. aliasedFieldName (attribName, childIndex) {
  1565. return this._featClasses[childIndex].aliasedFieldName(attribName);
  1566. }
  1567. /**
  1568. * Retrieves attributes from a layer for a specified feature index
  1569. * @param {String} childIndex index of the child layer to get attributes for
  1570. * @return {Promise} promise resolving with formatted attributes to be consumed by the datagrid and esri feature identify
  1571. */
  1572. getFormattedAttributes (childIndex) {
  1573. return this._featClasses[childIndex].getFormattedAttributes();
  1574. }
  1575. /**
  1576. * Check to see if the attribute in question is an esriFieldTypeDate type.
  1577. *
  1578. * @param {String} attribName the attribute name we want to check if it's a date or not
  1579. * @param {String} childIndex index of the child layer whos attributes we are looking at
  1580. * @return {Promise} resolves to true or false based on the attribName type being esriFieldTypeDate
  1581. */
  1582. checkDateType (attribName, childIndex) {
  1583. return this._featClasses[childIndex].checkDateType(attribName);
  1584. }
  1585. /**
  1586. * Returns attribute data for a child layer.
  1587. *
  1588. * @function getAttribs
  1589. * @param {String} childIndex the index of the child layer
  1590. * @returns {Promise} resolves with a layer attribute data object
  1591. */
  1592. getAttribs (childIndex) {
  1593. return this._featClasses[childIndex].getAttribs();
  1594. }
  1595. /**
  1596. * Returns layer-specific data for a child layer
  1597. *
  1598. * @function getLayerData
  1599. * @param {String} childIndex the index of the child layer
  1600. * @returns {Promise} resolves with a layer data object
  1601. */
  1602. getLayerData (childIndex) {
  1603. return this._featClasses[childIndex].getLayerData();
  1604. }
  1605. getFeatureName (childIndex, objId, attribs) {
  1606. return this._featClasses[childIndex].getFeatureName(objId, attribs);
  1607. }
  1608. getSymbology (childIndex) {
  1609. return this._featClasses[childIndex].getSymbology();
  1610. }
  1611. /**
  1612. * Run a query on a dynamic layer, return the result as a promise.
  1613. * @function identify
  1614. * @param {Object} opts additional argumets like map object, clickEvent, etc.
  1615. * @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
  1616. */
  1617. identify (opts) {
  1618. // TODO caller must pass in layer ids to interrogate. geoApi wont know what is toggled in the legend.
  1619. // param is opts.layerIds, array of integer for every leaf to interrogate.
  1620. // TODO add full documentation for options parameter
  1621. // bundles results from all leaf layers
  1622. const identifyResults = [];
  1623. // create an results object for every leaf layer we are inspecting
  1624. opts.layerIds.forEach(leafIndex => {
  1625. // TODO fix these params
  1626. // TODO legendEntry.name, legendEntry.symbology appear to be fast links to populate the left side of the results
  1627. // view. perhaps it should not be in this object anymore?
  1628. // TODO see how the client is consuming the internal pointer to layerRecord. this may also now be
  1629. // directly available via the legend object.
  1630. const identifyResult =
  1631. new IdentifyResult('legendEntry.name', 'legendEntry.symbology', 'EsriFeature', this,
  1632. leafIndex, 'legendEntry.master.name'); // provide name of the master group as caption
  1633. identifyResults[leafIndex] = identifyResult;
  1634. });
  1635. opts.tolerance = this.clickTolerance;
  1636. const identifyPromise = this._api.layer.serverLayerIdentify(this._layer, opts)
  1637. .then(clickResults => {
  1638. const hitIndexes = []; // sublayers that we got results for
  1639. // transform attributes of click results into {name,data} objects
  1640. // one object per identified feature
  1641. //
  1642. // each feature will have its attributes converted into a table
  1643. // placeholder for now until we figure out how to signal the panel that
  1644. // we want to make a nice table
  1645. clickResults.forEach(ele => {
  1646. // NOTE: the identify service returns aliased field names, so no need to look them up here.
  1647. // however, this means we need to un-alias the data when doing field lookups.
  1648. // NOTE: ele.layerId is what we would call featureIdx
  1649. hitIndexes.push(ele.layerId);
  1650. // get metadata about this sublayer
  1651. this.getLayerData(ele.layerId).then(lData => {
  1652. const identifyResult = identifyResults[ele.layerId];
  1653. if (lData.supportsFeatures) {
  1654. const unAliasAtt = AttribFC.unAliasAttribs(ele.feature.attributes, lData.fields);
  1655. // TODO traditionally, we did not pass fields into attributesToDetails as data was
  1656. // already aliased from the server. now, since we are extracting field type as
  1657. // well, this means things like date formatting might not be applied to
  1658. // identify results. examine the impact of providing the fields parameter
  1659. // to data that is already aliased.
  1660. identifyResult.data.push({
  1661. name: ele.value,
  1662. data: this.attributesToDetails(ele.feature.attributes),
  1663. oid: unAliasAtt[lData.oidField],
  1664. symbology: [{
  1665. svgcode: this._api.symbology.getGraphicIcon(unAliasAtt, lData.renderer)
  1666. }]
  1667. });
  1668. }
  1669. identifyResult.isLoading = false;
  1670. });
  1671. });
  1672. // set the rest of the entries to loading false
  1673. identifyResults.forEach(identifyResult => {
  1674. if (hitIndexes.indexOf(identifyResult.requester.featureIdx) === -1) {
  1675. identifyResult.isLoading = false;
  1676. }
  1677. });
  1678. });
  1679. return {
  1680. identifyResults: identifyResults.filter(identifyResult => identifyResult), // collapse sparse array
  1681. identifyPromise
  1682. };
  1683. }
  1684. // TODO docs
  1685. getChildName (index) {
  1686. // TODO revisit logic. is this the best way to do this? what are the needs of the consuming code?
  1687. // TODO restructure so WMS can use this too?
  1688. // will not use FC classes, as we also need group names
  1689. return this._layer.layerInfos[index].name;
  1690. }
  1691. }
  1692. /**
  1693. * @class TileRecord
  1694. */
  1695. class TileRecord extends LayerRecord {
  1696. /**
  1697. * Create a layer record with the appropriate geoApi layer type. Layer config
  1698. * should be fully merged with all layer options defined (i.e. this constructor
  1699. * will not apply any defaults).
  1700. * @param {Object} layerClass the ESRI api object for tile layers
  1701. * @param {Object} apiRef object pointing to the geoApi. allows us to call other geoApi functions.
  1702. * @param {Object} config layer config values
  1703. * @param {Object} esriLayer an optional pre-constructed layer
  1704. * @param {Function} epsgLookup an optional lookup function for EPSG codes (see geoService for signature)
  1705. */
  1706. constructor (layerClass, apiRef, config, esriLayer, epsgLookup) {
  1707. // TODO if we have nothing to add here, delete this constructor
  1708. super(apiRef, config, esriLayer, epsgLookup);
  1709. }
  1710. /**
  1711. * Triggers when the layer loads.
  1712. *
  1713. * @function onLoad
  1714. */
  1715. onLoad () {
  1716. super.onLoad();
  1717. // TODO consider making this a function, as it is common across less-fancy layers
  1718. this._defaultFC = '0';
  1719. this._featClasses['0'] = new BasicFC(this, '0');
  1720. }
  1721. }
  1722. /**
  1723. * @class WmsRecord
  1724. */
  1725. class WmsRecord extends LayerRecord {
  1726. /**
  1727. * Create a layer record with the appropriate geoApi layer type. Layer config
  1728. * should be fully merged with all layer options defined (i.e. this constructor
  1729. * will not apply any defaults).
  1730. * @param {Object} layerClass the ESRI api object for wms layers
  1731. * @param {Object} apiRef object pointing to the geoApi. allows us to call other geoApi functions.
  1732. * @param {Object} config layer config values
  1733. * @param {Object} esriLayer an optional pre-constructed layer
  1734. * @param {Function} epsgLookup an optional lookup function for EPSG codes (see geoService for signature)
  1735. */
  1736. constructor (layerClass, apiRef, config, esriLayer, epsgLookup) {
  1737. // TODO if we have nothing to add here, delete this constructor
  1738. super(layerClass, apiRef, config, esriLayer, epsgLookup);
  1739. }
  1740. makeLayerConfig () {
  1741. const cfg = super.makeLayerConfig();
  1742. cfg.visibleLayers = this.config.layerEntries.map(le => le.id);
  1743. return cfg;
  1744. }
  1745. /**
  1746. * Triggers when the layer loads.
  1747. *
  1748. * @function onLoad
  1749. */
  1750. onLoad () {
  1751. super.onLoad();
  1752. // TODO consider making this a function, as it is common across less-fancy layers
  1753. this._defaultFC = '0';
  1754. this._featClasses['0'] = new BasicFC(this, '0');
  1755. }
  1756. /**
  1757. * Run a getFeatureInfo on a WMS layer, return the result as a promise. Fills the panelData array on resolution.
  1758. *
  1759. * @param {Object} opts additional argumets like map object, clickEvent, etc.
  1760. * @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
  1761. */
  1762. identify (opts) {
  1763. // TODO add full documentation for options parameter
  1764. // TODO consider having a constants area in geoApi / better place for this definition
  1765. const infoMap = {
  1766. 'text/html;fgpv=summary': 'HTML',
  1767. 'text/html': 'HTML',
  1768. 'text/plain': 'Text',
  1769. 'application/json': 'EsriFeature'
  1770. };
  1771. // ignore layers with no mime type
  1772. if (!infoMap.hasOwnProperty(this.config.featureInfoMimeType)) {
  1773. return {};
  1774. }
  1775. // TODO fix these params
  1776. // TODO legendEntry.name, legendEntry.symbology appear to be fast links to populate the left side of the results
  1777. // view. perhaps it should not be in this object anymore?
  1778. // TODO see how the client is consuming the internal pointer to layerRecord. this may also now be
  1779. // directly available via the legend object.
  1780. const identifyResult =
  1781. new IdentifyResult('legendEntry.name', 'legendEntry.symbology', infoMap[this.config.featureInfoMimeType],
  1782. this);
  1783. const identifyPromise = this._api.layer.ogc
  1784. .getFeatureInfo(
  1785. this._layer,
  1786. opts.clickEvent,
  1787. this.config.layerEntries.map(le => le.id),
  1788. this.config.featureInfoMimeType)
  1789. .then(data => {
  1790. identifyResult.isLoading = false;
  1791. // TODO: check for French service
  1792. // check if a result is returned by the service. If not, do not add to the array of data
  1793. if (data.indexOf('Search returned no results') === -1 && data !== '') {
  1794. identifyResult.data.push(data);
  1795. }
  1796. // console.info(data);
  1797. });
  1798. return { identifyResults: [identifyResult], identifyPromise };
  1799. }
  1800. }
  1801. /**
  1802. * @class FeatureRecord
  1803. */
  1804. class FeatureRecord extends AttrRecord {
  1805. // TODO add flags for file based layers?
  1806. /**
  1807. * Create a layer record with the appropriate geoApi layer type. Layer config
  1808. * should be fully merged with all layer options defined (i.e. this constructor
  1809. * will not apply any defaults).
  1810. * @param {Object} layerClass the ESRI api object for feature layers
  1811. * @param {Object} esriRequest the ESRI api object for making web requests with proxy support
  1812. * @param {Object} apiRef object pointing to the geoApi. allows us to call other geoApi functions.
  1813. * @param {Object} config layer config values
  1814. * @param {Object} esriLayer an optional pre-constructed layer
  1815. * @param {Function} epsgLookup an optional lookup function for EPSG codes (see geoService for signature)
  1816. */
  1817. constructor (layerClass, esriRequest, apiRef, config, esriLayer, epsgLookup) {
  1818. // TODO if we have nothing to add here, delete this constructor
  1819. super(layerClass, esriRequest, apiRef, config, esriLayer, epsgLookup);
  1820. }
  1821. // TODO ensure whoever is making layers from config fragments is also setting the feature index.
  1822. // remove comment once that is done
  1823. makeLayerConfig () {
  1824. const cfg = super.makeLayerConfig();
  1825. cfg.mode = this.config.state.snapshot ? this._layerClass.MODE_SNAPSHOT
  1826. : this._layerClass.MODE_ONDEMAND;
  1827. // TODO confirm this logic. old code mapped .options.snapshot.value to the button -- meaning if we were in snapshot mode,
  1828. // we would want the button disabled. in the refactor, the button may get it's enabled/disabled from a different source.
  1829. this.config.state.snapshot = !this.config.state.snapshot;
  1830. return cfg;
  1831. }
  1832. // returns the proxy interface object for the root of the layer (i.e. main entry in legend, not nested child things)
  1833. // TODO docs
  1834. getProxy () {
  1835. // TODO figure out control name arrays from config (specifically disabled stuff)
  1836. // updated config schema uses term "enabled" but have a feeling it really means available
  1837. // TODO figure out how placeholders work with all this
  1838. if (!this._rootProxy) {
  1839. this._rootProxy = new LayerInterface(this, this.initialConfig.controls);
  1840. this._rootProxy.convertToFeatureLayer(this);
  1841. }
  1842. return this._rootProxy;
  1843. }
  1844. /**
  1845. * Triggers when the layer loads.
  1846. *
  1847. * @function onLoad
  1848. */
  1849. onLoad () {
  1850. super.onLoad();
  1851. // set up attributes, set up children bundles.
  1852. const attributeBundle = this._apiRef.attribs.loadLayerAttribs(this._layer);
  1853. // feature has only one layer
  1854. const idx = attributeBundle.indexes[0];
  1855. const aFC = new AttribFC(this, idx, attributeBundle[idx]);
  1856. aFC.nameField = this.config.nameField;
  1857. this._defaultFC = idx;
  1858. this._featClasses[idx] = aFC;
  1859. }
  1860. getFeatureCount () {
  1861. // just use the layer url (or lack of in case of file layer)
  1862. return super.getFeatureCount(this._layer.url);
  1863. }
  1864. isFileLayer () {
  1865. // TODO revisit. is it robust enough?
  1866. return this._layer && this._layer.url === '';
  1867. }
  1868. // TODO determine who is setting this. if we have an internal
  1869. // snapshot process, it might become a read-only property
  1870. get isSnapshot () { return this._snapshot; }
  1871. set isSnapshot (value) { this._snapshot = value; }
  1872. onMouseOver (e) {
  1873. if (this._hoverListeners.length > 0) {
  1874. // TODO add in quick lookup for layers that dont have attributes loaded yet
  1875. const showBundle = {
  1876. type: 'mouseOver',
  1877. point: e.screenPoint,
  1878. target: e.target
  1879. };
  1880. // tell anyone listening we moused into something
  1881. this._fireEvent(this._hoverListeners, showBundle);
  1882. // pull metadata for this layer.
  1883. this.getLayerData().then(lInfo => {
  1884. // TODO this will change a bit after we add in quick lookup. for now, get all attribs
  1885. return Promise.all([Promise.resolve(lInfo), this.getAttribs()]);
  1886. }).then(([lInfo, aInfo]) => {
  1887. // graphic attributes will only have the OID if layer is server based
  1888. const oid = e.graphic.attributes[lInfo.oidField];
  1889. // get name via attribs and name field
  1890. const featAttribs = aInfo.features[aInfo.oidIndex[oid]].attributes;
  1891. const featName = this.getFeatureName(oid, featAttribs);
  1892. // get icon via renderer and geoApi call
  1893. const svgcode = this._apiRef.symbology.getGraphicIcon(featAttribs, lInfo.renderer);
  1894. // duplicate the position so listener can verify this event is same as mouseOver event above
  1895. const loadBundle = {
  1896. type: 'tipLoaded',
  1897. name: featName,
  1898. target: e.target,
  1899. svgcode
  1900. };
  1901. // tell anyone listening we moused into something
  1902. this._fireEvent(this._hoverListeners, loadBundle);
  1903. });
  1904. }
  1905. }
  1906. onMouseOut (e) {
  1907. // tell anyone listening we moused out
  1908. const outBundle = {
  1909. type: 'mouseOut',
  1910. target: e.target
  1911. };
  1912. this._fireEvent(this._hoverListeners, outBundle);
  1913. }
  1914. /**
  1915. * Run a query on a feature layer, return the result as a promise. Fills the panelData array on resolution. // TODO update
  1916. * @function identify
  1917. * @param {Object} opts additional argumets like map object, clickEvent, etc.
  1918. * @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
  1919. */
  1920. identify (opts) {
  1921. // TODO add full documentation for options parameter
  1922. // TODO fix these params
  1923. // TODO legendEntry.name, legendEntry.symbology appear to be fast links to populate the left side of the results
  1924. // view. perhaps it should not be in this object anymore?
  1925. // TODO see how the client is consuming the internal pointer to layerRecord. this may also now be
  1926. // directly available via the legend object.
  1927. const identifyResult =
  1928. new IdentifyResult('legendEntry.name', 'legendEntry.symbology', 'EsriFeature',
  1929. this, this._defaultFC);
  1930. // run a spatial query
  1931. const qry = new this._api.layer.Query();
  1932. qry.outFields = ['*']; // this will result in just objectid fields, as that is all we have in feature layers
  1933. // more accurate results without making the buffer if we're dealing with extents
  1934. // polygons from added file need buffer
  1935. // TODO further investigate why esri is requiring buffer for file-based polygons. logic says it shouldnt
  1936. if (this._layer.geometryType === 'esriGeometryPolygon' && !this.isFileLayer()) {
  1937. qry.geometry = opts.geometry;
  1938. } else {
  1939. qry.geometry = this.makeClickBuffer(opts.clickEvent.mapPoint, opts.map, this.clickTolerance);
  1940. }
  1941. const identifyPromise = Promise.all([
  1942. this.getAttributes(),
  1943. Promise.resolve(this._layer.queryFeatures(qry)),
  1944. this.getLayerData()
  1945. ])
  1946. .then(([attributes, queryResult, layerData]) => {
  1947. // transform attributes of query results into {name,data} objects one object per queried feature
  1948. //
  1949. // each feature will have its attributes converted into a table
  1950. // placeholder for now until we figure out how to signal the panel that
  1951. // we want to make a nice table
  1952. identifyResult.isLoading = false;
  1953. identifyResult.data = queryResult.features.map(
  1954. feat => {
  1955. // grab the object id of the feature we clicked on.
  1956. const objId = feat.attributes[attributes.oidField];
  1957. const objIdStr = objId.toString();
  1958. // use object id find location of our feature in the feature array, and grab its attributes
  1959. const featAttribs = attributes.features[attributes.oidIndex[objIdStr]];
  1960. return {
  1961. name: this.getFeatureName(objIdStr, featAttribs),
  1962. data: this.attributesToDetails(featAttribs, layerData.fields),
  1963. oid: objId,
  1964. symbology: [
  1965. { svgcode: this._api.symbology.getGraphicIcon(featAttribs, layerData.renderer) }
  1966. ]
  1967. };
  1968. });
  1969. });
  1970. return { identifyResults: [identifyResult], identifyPromise };
  1971. }
  1972. }
  1973. module.exports = () => ({
  1974. DynamicRecord,
  1975. FeatureRecord,
  1976. ImageRecord,
  1977. TileRecord,
  1978. WmsRecord,
  1979. WmsFC, // TODO compiler temp. remove once we are referencing it
  1980. States: states // TODO should this get exposed on the geoApi as well? currently layer module is not re-exposing it
  1981. });