Source: map/popup/PopupRenderer.js

  1. import { resolveComplexTemplate } from "../../utils/template/resolveComplexTemplate.js";
  2. import { enhanceHTML } from "../../utils/template/enhanceHTML.js";
  3. import DefaultPopupTemplate from './default.popup.md?raw';
  4. import DefaultSidebarTemplate from './default.sidebar.md?raw';
  5. export { PopupRenderer }
  6. /**
  7. * Static class for resolving popup templates depending on the type and
  8. * rendering the content.
  9. *
  10. * @author rhess <robin.hess@awi.de>
  11. *
  12. * @memberof vef.map.popup
  13. */
  14. class PopupRenderer {
  15. static templates = {};
  16. static urlCache = {};
  17. static templateUrlPrefix = "";
  18. static registerTemplate(name, template) {
  19. PopupRenderer.templates[name] = template;
  20. }
  21. /**
  22. * Builds and returns an array of features and HTML describing
  23. * the features defined in content.
  24. *
  25. * Returns an object like
  26. * ```
  27. * {
  28. * response: response,
  29. * feature: feature,
  30. * layer: layer,
  31. * html: html
  32. * }
  33. * ```
  34. *
  35. * @param {object} content
  36. * @param {object} response
  37. * @param {object} layer
  38. * @param {string} style "popup" or "sidebar"
  39. */
  40. static async render(content, response, layer, style) {
  41. const uId = layer.uniqueId;
  42. if ((layer.metadataTemplate === false) || (layer.metadataTemplate?.popup === false)) return [];
  43. // simple handling of unparsed responses (e.g. html)
  44. if ((typeof content == "string") || (content instanceof HTMLElement)) {
  45. return [{
  46. response: response,
  47. feature: content,
  48. layer: layer,
  49. html: content
  50. }];
  51. }
  52. // handle invalid content
  53. if (!layer) {
  54. console.warn('Layer was not defined for the popup content');
  55. return [];
  56. } else if (!content || (typeof content != "object")) {
  57. console.warn('Empty or error content for ', uId);
  58. return [];
  59. } else if (!["FeatureCollection", "Feature"].includes(content?.type)) {
  60. console.warn('The popup content cannot be parsed as a GeoJson FeatureCollection');
  61. return [];
  62. } else if (content.ServiceExceptionReport) {
  63. console.warn('Exception report for ', uId, content.ServiceExceptionReport);
  64. return [];
  65. }
  66. if (content.type == "Feature") content = {
  67. type: "FeatureCollection",
  68. features: [content]
  69. }
  70. if (!("features" in content)) {
  71. console.warn('"features" is missing in the GeoJson FeatureCollection');
  72. return [];
  73. }
  74. // resolve and replace templates
  75. let template = await PopupRenderer.getTemplate(layer, style);
  76. return PopupRenderer.renderTemplate(content, response, layer, style, template);
  77. }
  78. /**
  79. * fetch a file based template via url. Check if the url starts with this.templateUrlPrefix first.
  80. *
  81. * @param {string} url
  82. * @returns {string} template
  83. */
  84. static fetchTemplate(url) {
  85. return new Promise((resolve, reject) => {
  86. if (url in this.urlCache) {
  87. resolve(this.urlCache[url]);
  88. } else if (url.startsWith(this.templateUrlPrefix)) {
  89. const xhttp = new XMLHttpRequest();
  90. xhttp.onreadystatechange = e => {
  91. if (xhttp.readyState == 4) {
  92. if (xhttp.status == 200) {
  93. PopupRenderer.urlCache[url] = xhttp.responseText;
  94. resolve(xhttp.responseText);
  95. } else {
  96. reject(`could not load template: ${url}, code: ${xhttp.status}`)
  97. }
  98. }
  99. };
  100. xhttp.onerror = e => {
  101. reject(e);
  102. }
  103. xhttp.open("GET", url, true);
  104. xhttp.send();
  105. } else {
  106. reject(`Invalid url prefix: ${this.templateUrlPrefix}`);
  107. }
  108. });
  109. }
  110. static getDefaultTemplate(style, layer) {
  111. console.log('Using default template for', layer.uniqueId);
  112. return (style == "sidebar") ? DefaultSidebarTemplate : DefaultPopupTemplate;
  113. }
  114. /**
  115. * Get the template for a layer without resolving it
  116. *
  117. * @param {Layer} layer
  118. * @param {string} style popup or sidebar
  119. * @returns {string} template
  120. */
  121. static async getTemplate(layer, style) {
  122. const resolveString = async (template) => {
  123. const localPrefixes = ['#', 'id:', 'id::'];
  124. for (const localPrefix of localPrefixes) {
  125. if (template.startsWith(localPrefix)) {
  126. if (template.substr(localPrefix.length) in PopupRenderer.templates) {
  127. console.log('Using named popup template', template, 'for', layer.uniqueId);
  128. return PopupRenderer.templates[template.substr(localPrefix.length)];
  129. } else {
  130. console.error('Missing named popup template', template, 'for', layer.uniqueId);
  131. return null;
  132. }
  133. }
  134. }
  135. if (template.startsWith('https://') || template.startsWith('https://')) {
  136. console.log('Using file based template', template, 'for', layer.uniqueId);
  137. try {
  138. return await PopupRenderer.fetchTemplate(template);
  139. } catch (error) {
  140. console.warn(error);
  141. }
  142. } else {
  143. console.log('Using integrated template for', layer.uniqueId);
  144. return template;
  145. }
  146. }
  147. const template = layer.metadataTemplate;
  148. let result = null;
  149. if (typeof template == "string") {
  150. result = await resolveString(template);
  151. } else if ((typeof template == "object") && template) {
  152. if (!style || !(style in template)) style = "popup";
  153. if ((style in template) && (typeof template[style] == "string")) {
  154. result = await resolveString(template[style]);
  155. }
  156. }
  157. return (result) ? result : this.getDefaultTemplate(style, layer);
  158. }
  159. static renderTemplate(featureCollection, response, layer, style, template) {
  160. const responses = [];
  161. // interpret strings as markdown and resolve template syntax
  162. if (typeof template !== 'string') return responses;
  163. for (let i = 0; i < featureCollection.features.length; ++i) {
  164. const feature = featureCollection.features[i];
  165. const data = Object.assign({ layer: layer, response: response }, feature)
  166. try {
  167. const html = resolveComplexTemplate(template, data, {
  168. markdown: true,
  169. postProcess: true
  170. });
  171. enhanceHTML(html, (style == "sidebar") ? {} : { groupedHeadlines: false })
  172. responses.push({
  173. response: response,
  174. feature: feature,
  175. layer: layer,
  176. title: html.dataset.title || layer.title,
  177. html: html
  178. });
  179. } catch (msg) {
  180. if (msg != "discard_template") console.warn(msg);
  181. }
  182. }
  183. return responses;
  184. }
  185. }