GetFeatureInfoFormat.js 12 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395
  1. import Cartographic from "../Core/Cartographic.js";
  2. import defined from "../Core/defined.js";
  3. import DeveloperError from "../Core/DeveloperError.js";
  4. import RuntimeError from "../Core/RuntimeError.js";
  5. import ImageryLayerFeatureInfo from "./ImageryLayerFeatureInfo.js";
  6. /**
  7. * Describes the format in which to request GetFeatureInfo from a Web Map Service (WMS) server.
  8. *
  9. * @alias GetFeatureInfoFormat
  10. * @constructor
  11. *
  12. * @param {string} type The type of response to expect from a GetFeatureInfo request. Valid
  13. * values are 'json', 'xml', 'html', or 'text'.
  14. * @param {string} [format] The info format to request from the WMS server. This is usually a
  15. * MIME type such as 'application/json' or text/xml'. If this parameter is not specified, the provider will request 'json'
  16. * using 'application/json', 'xml' using 'text/xml', 'html' using 'text/html', and 'text' using 'text/plain'.
  17. * @param {Function} [callback] A function to invoke with the GetFeatureInfo response from the WMS server
  18. * in order to produce an array of picked {@link ImageryLayerFeatureInfo} instances. If this parameter is not specified,
  19. * a default function for the type of response is used.
  20. */
  21. function GetFeatureInfoFormat(type, format, callback) {
  22. //>>includeStart('debug', pragmas.debug);
  23. if (!defined(type)) {
  24. throw new DeveloperError("type is required.");
  25. }
  26. //>>includeEnd('debug');
  27. this.type = type;
  28. if (!defined(format)) {
  29. if (type === "json") {
  30. format = "application/json";
  31. } else if (type === "xml") {
  32. format = "text/xml";
  33. } else if (type === "html") {
  34. format = "text/html";
  35. } else if (type === "text") {
  36. format = "text/plain";
  37. }
  38. //>>includeStart('debug', pragmas.debug);
  39. else {
  40. throw new DeveloperError(
  41. 'format is required when type is not "json", "xml", "html", or "text".'
  42. );
  43. }
  44. //>>includeEnd('debug');
  45. }
  46. this.format = format;
  47. if (!defined(callback)) {
  48. if (type === "json") {
  49. callback = geoJsonToFeatureInfo;
  50. } else if (type === "xml") {
  51. callback = xmlToFeatureInfo;
  52. } else if (type === "html") {
  53. callback = textToFeatureInfo;
  54. } else if (type === "text") {
  55. callback = textToFeatureInfo;
  56. }
  57. //>>includeStart('debug', pragmas.debug);
  58. else {
  59. throw new DeveloperError(
  60. 'callback is required when type is not "json", "xml", "html", or "text".'
  61. );
  62. }
  63. //>>includeEnd('debug');
  64. }
  65. this.callback = callback;
  66. }
  67. function geoJsonToFeatureInfo(json) {
  68. const result = [];
  69. const features = json.features;
  70. for (let i = 0; i < features.length; ++i) {
  71. const feature = features[i];
  72. const featureInfo = new ImageryLayerFeatureInfo();
  73. featureInfo.data = feature;
  74. featureInfo.properties = feature.properties;
  75. featureInfo.configureNameFromProperties(feature.properties);
  76. featureInfo.configureDescriptionFromProperties(feature.properties);
  77. // If this is a point feature, use the coordinates of the point.
  78. if (defined(feature.geometry) && feature.geometry.type === "Point") {
  79. const longitude = feature.geometry.coordinates[0];
  80. const latitude = feature.geometry.coordinates[1];
  81. featureInfo.position = Cartographic.fromDegrees(longitude, latitude);
  82. }
  83. result.push(featureInfo);
  84. }
  85. return result;
  86. }
  87. const mapInfoMxpNamespace = "http://www.mapinfo.com/mxp";
  88. const esriWmsNamespace = "http://www.esri.com/wms";
  89. const wfsNamespace = "http://www.opengis.net/wfs";
  90. const gmlNamespace = "http://www.opengis.net/gml";
  91. function xmlToFeatureInfo(xml) {
  92. const documentElement = xml.documentElement;
  93. if (
  94. documentElement.localName === "MultiFeatureCollection" &&
  95. documentElement.namespaceURI === mapInfoMxpNamespace
  96. ) {
  97. // This looks like a MapInfo MXP response
  98. return mapInfoXmlToFeatureInfo(xml);
  99. } else if (
  100. documentElement.localName === "FeatureInfoResponse" &&
  101. documentElement.namespaceURI === esriWmsNamespace
  102. ) {
  103. // This looks like an Esri WMS response
  104. return esriXmlToFeatureInfo(xml);
  105. } else if (
  106. documentElement.localName === "FeatureCollection" &&
  107. documentElement.namespaceURI === wfsNamespace
  108. ) {
  109. // This looks like a WFS/GML response.
  110. return gmlToFeatureInfo(xml);
  111. } else if (documentElement.localName === "ServiceExceptionReport") {
  112. // This looks like a WMS server error, so no features picked.
  113. throw new RuntimeError(
  114. new XMLSerializer().serializeToString(documentElement)
  115. );
  116. } else if (documentElement.localName === "msGMLOutput") {
  117. return msGmlToFeatureInfo(xml);
  118. } else {
  119. // Unknown response type, so just dump the XML itself into the description.
  120. return unknownXmlToFeatureInfo(xml);
  121. }
  122. }
  123. function mapInfoXmlToFeatureInfo(xml) {
  124. const result = [];
  125. const multiFeatureCollection = xml.documentElement;
  126. const features = multiFeatureCollection.getElementsByTagNameNS(
  127. mapInfoMxpNamespace,
  128. "Feature"
  129. );
  130. for (let featureIndex = 0; featureIndex < features.length; ++featureIndex) {
  131. const feature = features[featureIndex];
  132. const properties = {};
  133. const propertyElements = feature.getElementsByTagNameNS(
  134. mapInfoMxpNamespace,
  135. "Val"
  136. );
  137. for (
  138. let propertyIndex = 0;
  139. propertyIndex < propertyElements.length;
  140. ++propertyIndex
  141. ) {
  142. const propertyElement = propertyElements[propertyIndex];
  143. if (propertyElement.hasAttribute("ref")) {
  144. const name = propertyElement.getAttribute("ref");
  145. const value = propertyElement.textContent.trim();
  146. properties[name] = value;
  147. }
  148. }
  149. const featureInfo = new ImageryLayerFeatureInfo();
  150. featureInfo.data = feature;
  151. featureInfo.properties = properties;
  152. featureInfo.configureNameFromProperties(properties);
  153. featureInfo.configureDescriptionFromProperties(properties);
  154. result.push(featureInfo);
  155. }
  156. return result;
  157. }
  158. function esriXmlToFeatureInfo(xml) {
  159. const featureInfoResponse = xml.documentElement;
  160. const result = [];
  161. let properties;
  162. const features = featureInfoResponse.getElementsByTagNameNS("*", "FIELDS");
  163. if (features.length > 0) {
  164. // Standard esri format
  165. for (let featureIndex = 0; featureIndex < features.length; ++featureIndex) {
  166. const feature = features[featureIndex];
  167. properties = {};
  168. const propertyAttributes = feature.attributes;
  169. for (
  170. let attributeIndex = 0;
  171. attributeIndex < propertyAttributes.length;
  172. ++attributeIndex
  173. ) {
  174. const attribute = propertyAttributes[attributeIndex];
  175. properties[attribute.name] = attribute.value;
  176. }
  177. result.push(
  178. imageryLayerFeatureInfoFromDataAndProperties(feature, properties)
  179. );
  180. }
  181. } else {
  182. // Thredds format -- looks like esri, but instead of containing FIELDS, contains FeatureInfo element
  183. const featureInfoElements = featureInfoResponse.getElementsByTagNameNS(
  184. "*",
  185. "FeatureInfo"
  186. );
  187. for (
  188. let featureInfoElementIndex = 0;
  189. featureInfoElementIndex < featureInfoElements.length;
  190. ++featureInfoElementIndex
  191. ) {
  192. const featureInfoElement = featureInfoElements[featureInfoElementIndex];
  193. properties = {};
  194. // node.children is not supported in IE9-11, so use childNodes and check that child.nodeType is an element
  195. const featureInfoChildren = featureInfoElement.childNodes;
  196. for (
  197. let childIndex = 0;
  198. childIndex < featureInfoChildren.length;
  199. ++childIndex
  200. ) {
  201. const child = featureInfoChildren[childIndex];
  202. if (child.nodeType === Node.ELEMENT_NODE) {
  203. properties[child.localName] = child.textContent;
  204. }
  205. }
  206. result.push(
  207. imageryLayerFeatureInfoFromDataAndProperties(
  208. featureInfoElement,
  209. properties
  210. )
  211. );
  212. }
  213. }
  214. return result;
  215. }
  216. function gmlToFeatureInfo(xml) {
  217. const result = [];
  218. const featureCollection = xml.documentElement;
  219. const featureMembers = featureCollection.getElementsByTagNameNS(
  220. gmlNamespace,
  221. "featureMember"
  222. );
  223. for (
  224. let featureIndex = 0;
  225. featureIndex < featureMembers.length;
  226. ++featureIndex
  227. ) {
  228. const featureMember = featureMembers[featureIndex];
  229. const properties = {};
  230. getGmlPropertiesRecursively(featureMember, properties);
  231. result.push(
  232. imageryLayerFeatureInfoFromDataAndProperties(featureMember, properties)
  233. );
  234. }
  235. return result;
  236. }
  237. // msGmlToFeatureInfo is similar to gmlToFeatureInfo, but assumes different XML structure
  238. // eg. <msGMLOutput> <ABC_layer> <ABC_feature> <foo>bar</foo> ... </ABC_feature> </ABC_layer> </msGMLOutput>
  239. function msGmlToFeatureInfo(xml) {
  240. const result = [];
  241. // Find the first child. Except for IE, this would work:
  242. // const layer = xml.documentElement.children[0];
  243. let layer;
  244. const children = xml.documentElement.childNodes;
  245. for (let i = 0; i < children.length; i++) {
  246. if (children[i].nodeType === Node.ELEMENT_NODE) {
  247. layer = children[i];
  248. break;
  249. }
  250. }
  251. if (!defined(layer)) {
  252. throw new RuntimeError(
  253. "Unable to find first child of the feature info xml document"
  254. );
  255. }
  256. const featureMembers = layer.childNodes;
  257. for (
  258. let featureIndex = 0;
  259. featureIndex < featureMembers.length;
  260. ++featureIndex
  261. ) {
  262. const featureMember = featureMembers[featureIndex];
  263. if (featureMember.nodeType === Node.ELEMENT_NODE) {
  264. const properties = {};
  265. getGmlPropertiesRecursively(featureMember, properties);
  266. result.push(
  267. imageryLayerFeatureInfoFromDataAndProperties(featureMember, properties)
  268. );
  269. }
  270. }
  271. return result;
  272. }
  273. function getGmlPropertiesRecursively(gmlNode, properties) {
  274. let isSingleValue = true;
  275. for (let i = 0; i < gmlNode.childNodes.length; ++i) {
  276. const child = gmlNode.childNodes[i];
  277. if (child.nodeType === Node.ELEMENT_NODE) {
  278. isSingleValue = false;
  279. }
  280. if (
  281. child.localName === "Point" ||
  282. child.localName === "LineString" ||
  283. child.localName === "Polygon" ||
  284. child.localName === "boundedBy"
  285. ) {
  286. continue;
  287. }
  288. if (
  289. child.hasChildNodes() &&
  290. getGmlPropertiesRecursively(child, properties)
  291. ) {
  292. properties[child.localName] = child.textContent;
  293. }
  294. }
  295. return isSingleValue;
  296. }
  297. function imageryLayerFeatureInfoFromDataAndProperties(data, properties) {
  298. const featureInfo = new ImageryLayerFeatureInfo();
  299. featureInfo.data = data;
  300. featureInfo.properties = properties;
  301. featureInfo.configureNameFromProperties(properties);
  302. featureInfo.configureDescriptionFromProperties(properties);
  303. return featureInfo;
  304. }
  305. function unknownXmlToFeatureInfo(xml) {
  306. const xmlText = new XMLSerializer().serializeToString(xml);
  307. const element = document.createElement("div");
  308. const pre = document.createElement("pre");
  309. pre.textContent = xmlText;
  310. element.appendChild(pre);
  311. const featureInfo = new ImageryLayerFeatureInfo();
  312. featureInfo.data = xml;
  313. featureInfo.description = element.innerHTML;
  314. return [featureInfo];
  315. }
  316. const emptyBodyRegex = /<body>\s*<\/body>/im;
  317. const wmsServiceExceptionReportRegex = /<ServiceExceptionReport([\s\S]*)<\/ServiceExceptionReport>/im;
  318. const titleRegex = /<title>([\s\S]*)<\/title>/im;
  319. function textToFeatureInfo(text) {
  320. // If the text is HTML and it has an empty body tag, assume it means no features were found.
  321. if (emptyBodyRegex.test(text)) {
  322. return undefined;
  323. }
  324. // If this is a WMS exception report, treat it as "no features found" rather than showing
  325. // bogus feature info.
  326. if (wmsServiceExceptionReportRegex.test(text)) {
  327. return undefined;
  328. }
  329. // If the text has a <title> element, use it as the name.
  330. let name;
  331. const title = titleRegex.exec(text);
  332. if (title && title.length > 1) {
  333. name = title[1];
  334. }
  335. const featureInfo = new ImageryLayerFeatureInfo();
  336. featureInfo.name = name;
  337. featureInfo.description = text;
  338. featureInfo.data = text;
  339. return [featureInfo];
  340. }
  341. export default GetFeatureInfoFormat;