layer/ogc.js

  1. 'use strict';
  2. const yxList = require('./reversedAxis.json');
  3. function getFeatureInfoBuilder(esriBundle) {
  4. /**
  5. * Handles click events for WMS layers (makes a WMS GetFeatureInfo call behind the scenes).
  6. *
  7. * @param {WMSLayer} wmsLayer an ESRI WMSLayer object to be queried
  8. * @param {Object} clickEvent an ESRI map click event (used for screen coordinates)
  9. * @param {Array} layerList a list of strings identifying the WMS layers to be queried
  10. * @param {String} mimeType the format to be requested for the response
  11. * @returns {Promise} a promise which resolves with the raw text of the GetFeatureInfo response
  12. */
  13. return (wmsLayer, clickEvent, layerList, mimeType) => {
  14. let wkid;
  15. let req;
  16. const esriMap = wmsLayer.getMap();
  17. const ext = esriMap.extent;
  18. const srList = wmsLayer.spatialReferences;
  19. const layers = layerList.join(',');
  20. if (srList && srList.length > 1) {
  21. wkid = srList[0];
  22. } else if (esriMap.spatialReference.wkid) {
  23. wkid = esriMap.spatialReference.wkid;
  24. }
  25. if (wmsLayer.version === '1.3' || wmsLayer.version === '1.3.0') {
  26. req = { CRS: 'EPSG:' + wkid, I: clickEvent.screenPoint.x, J: clickEvent.screenPoint.y,
  27. STYLES: '', FORMAT: wmsLayer.imageFormat };
  28. if (yxList.indexOf(String(wkid)) > -1) {
  29. req.BBOX = `${ext.ymin},${ext.xmin},${ext.ymax},${ext.xmax}`;
  30. }
  31. } else {
  32. req = { SRS: 'EPSG:' + wkid, X: clickEvent.screenPoint.x, Y: clickEvent.screenPoint.y };
  33. }
  34. if (!req.hasOwnProperty('BBOX')) {
  35. req.BBOX = `${ext.xmin},${ext.ymin},${ext.xmax},${ext.ymax}`;
  36. }
  37. const settings = {
  38. SERVICE: 'WMS',
  39. REQUEST: 'GetFeatureInfo',
  40. VERSION: wmsLayer.version,
  41. WIDTH: esriMap.width,
  42. HEIGHT: esriMap.height,
  43. QUERY_LAYERS: layers,
  44. LAYERS: layers,
  45. INFO_FORMAT: mimeType
  46. };
  47. Object.keys(settings).forEach(key => req[key] = settings[key]);
  48. return Promise.resolve(esriBundle.esriRequest({
  49. url: wmsLayer.url.split('?')[0],
  50. content: req,
  51. handleAs: 'text'
  52. }));
  53. };
  54. }
  55. function parseCapabilitiesBuilder(esriBundle) {
  56. const query = esriBundle.dojoQuery;
  57. /**
  58. * Fetch layer data from a WMS endpoint. This method will execute a WMS GetCapabilities
  59. * request against the specified URL, it requests WMS 1.3 and it is capable of parsing
  60. * 1.3 or 1.1.1 responses. It returns a promise which will resolve with basic layer
  61. * metadata and querying information.
  62. *
  63. * metadata response format:
  64. * { queryTypes: [mimeType(str)],
  65. * layers: [
  66. * {name(str), desc(str), queryable(bool), layers:[recursive] }
  67. * ] }
  68. *
  69. * @param {string} wmsEndpoint a URL pointing to a WMS server (it must not include a query string)
  70. * @return {Promise} a promise resolving with a metadata object (as specified above)
  71. */
  72. return (wmsEndpoint) => {
  73. const reqPromise = new Promise(resolve => {
  74. getCapabilities()
  75. .then(data => resolve(data)) // if successful, pass straight back
  76. .catch(() => { // if errors, try again; see fgpv-vpgf/fgpv-vpgf#908 issue
  77. console.error('Get capabilities failed; trying the second time;');
  78. resolve(getCapabilities());
  79. });
  80. });
  81. // there might already be a way to do this in the parsing API
  82. // I don't know XML parsing well enough (and I don't want to)
  83. // this has now been ported from RAMP to FGPV and I still, happily,
  84. // do not know any more about XML parsing now
  85. function getImmediateChild(node, childName) {
  86. for (let i = 0; i < node.childNodes.length; ++i) {
  87. if (node.childNodes[i].nodeName === childName) {
  88. return node.childNodes[i];
  89. }
  90. }
  91. return undefined;
  92. }
  93. // find all <Layer> nodes under the given XML node
  94. // pick title, name and queryable nodes/attributes
  95. // recursively called on all child <Layer> nodes
  96. function getLayers(xmlNode) {
  97. return query('> Layer', xmlNode).map(layer => {
  98. const nameNode = getImmediateChild(layer, 'Name');
  99. const titleNode = getImmediateChild(layer, 'Title');
  100. return {
  101. name: nameNode ? nameNode.textContent : null,
  102. desc: titleNode.textContent,
  103. queryable: layer.getAttribute('queryable') === '1',
  104. layers: getLayers(layer)
  105. };
  106. });
  107. }
  108. function getCapabilities() {
  109. let url = wmsEndpoint;
  110. // if url has a '?' do not append to avoid errors, user must add this manually
  111. if (wmsEndpoint.indexOf('?') === -1) {
  112. url += '?service=WMS&version=1.3&request=GetCapabilities';
  113. }
  114. return Promise.resolve(new esriBundle.esriRequest({
  115. url,
  116. handleAs: 'xml'
  117. }).promise);
  118. }
  119. return reqPromise.then(data => ({
  120. layers: getLayers(query('Capability', data)[0]),
  121. queryTypes: query('GetFeatureInfo > Format', data).map(node => node.textContent)
  122. }));
  123. };
  124. }
  125. /**
  126. * Recursively crawl a wms layer info structure. Store any legends in the provided map object.
  127. *
  128. * @private
  129. * @param {Array} layerInfos array of ESRI WMSLayerInfo objects
  130. * @param {Map} urlMap a Map of sublayer names to legend urls
  131. */
  132. function crawlLayerInfos(layerInfos, urlMap) {
  133. layerInfos.forEach(li => {
  134. if (li.name) {
  135. urlMap.set(li.name, li.legendURL);
  136. }
  137. if (li.subLayers.length > 0) {
  138. crawlLayerInfos(li.subLayers, urlMap);
  139. }
  140. });
  141. }
  142. /**
  143. * Finds the appropriate legend URLs for WMS layers.
  144. *
  145. * @param {WMSLayer} wmsLayer an ESRI WMSLayer object to be queried
  146. * @param {Array} layerList a list of strings identifying the WMS layers to be queried
  147. * @returns {Array} a list of strings containing URLs for specified layers (order is preserved)
  148. */
  149. function getLegendUrls(wmsLayer, layerList) {
  150. const liMap = new Map(); // use Map in case someone clever uses a WMS layer name that matches an Object's default properties
  151. crawlLayerInfos(wmsLayer.layerInfos, liMap);
  152. return layerList.map(l => liMap.get(l));
  153. }
  154. module.exports = esriBundle => {
  155. return {
  156. WmsLayer: esriBundle.WmsLayer,
  157. getFeatureInfo: getFeatureInfoBuilder(esriBundle),
  158. parseCapabilities: parseCapabilitiesBuilder(esriBundle),
  159. getLegendUrls
  160. };
  161. };