Source: map/popup/PopupRenderer.js

import { resolveComplexTemplate } from "../../utils/template/resolveComplexTemplate.js";
import { enhanceHTML } from "../../utils/template/enhanceHTML.js";
import DefaultPopupTemplate from './default.popup.md?raw';
import DefaultSidebarTemplate from './default.sidebar.md?raw';

export { PopupRenderer }

/**
 * Static class for resolving popup templates depending on the type and
 * rendering the content.
 *
 * @author rhess <robin.hess@awi.de>
 * 
 * @memberof vef.map.popup
 */
class PopupRenderer {

    static templates = {};
    static urlCache = {};
    static templateUrlPrefix = "";

    static registerTemplate(name, template) {
        PopupRenderer.templates[name] = template;
    }

    /**
     * Builds and returns an array of features and HTML describing
     * the features defined in content.
     * 
     * Returns an object like
     * ```
     * {
     *   response: response,
     *   feature: feature,
     *   layer: layer,
     *   html: html
     * }
     * ```
     *
     * @param {object} content
     * @param {object} response
     * @param {object} layer
     * @param {string} style "popup" or "sidebar"
     */
    static async render(content, response, layer, style) {
        const uId = layer.uniqueId;

        // simple handling of unparsed responses (e.g. html)
        if ((typeof content == "string") || (content instanceof HTMLElement)) {
            return [{
                response: response,
                feature: content,
                layer: layer,
                html: content
            }];
        }

        // handle invalid content
        if (!layer) {
            console.warn('Layer was not defined for the popup content');
            return [];
        } else if (!content || (typeof content != "object")) {
            console.warn('Empty or error content for ', uId);
            return [];

        } else if (!["FeatureCollection", "Feature"].includes(content?.type)) {
            console.warn('The popup content cannot be parsed as a GeoJson FeatureCollection');
            return [];
        } else if (content.ServiceExceptionReport) {
            console.warn('Exception report for ', uId, content.ServiceExceptionReport);
            return [];
        }

        if (content.type == "Feature") content = {
            type: "FeatureCollection",
            features: [content]
        }

        if (!("features" in content)) {
            console.warn('"features" is missing in the GeoJson FeatureCollection');
            return [];
        }

        // resolve and replace templates
        let template = await PopupRenderer.getTemplate(layer, style);
        return PopupRenderer.renderTemplate(content, response, layer, style, template);
    }

    /**
     * fetch a file based template via url. Check if the url starts with this.templateUrlPrefix first.
     * 
     * @param {string} url 
     * @returns {string} template
     */
    static fetchTemplate(url) {
        return new Promise((resolve, reject) => {
            if (url in this.urlCache) {
                resolve(this.urlCache[url]);
            } else if (url.startsWith(this.templateUrlPrefix)) {
                const xhttp = new XMLHttpRequest();
                xhttp.onreadystatechange = e => {
                    if (xhttp.readyState == 4) {
                        if (xhttp.status == 200) {
                            PopupRenderer.urlCache[url] = xhttp.responseText;
                            resolve(xhttp.responseText);
                        } else {
                            reject(`could not load template: ${url}, code: ${xhttp.status}`)
                        }

                    }
                };
                xhttp.onerror = e => {
                    reject(e);
                }
                xhttp.open("GET", url, true);
                xhttp.send();
            } else {
                reject(`Invalid url prefix: ${this.templateUrlPrefix}`);
            }
        });
    }

    static getDefaultTemplate(style, layer) {
        console.log('Using default template for', layer.uniqueId);
        return (style == "sidebar") ? DefaultSidebarTemplate : DefaultPopupTemplate;
    }

    /**
     * Get the template for a layer without resolving it
     * 
     * @param {Layer} layer 
     * @param {string} style popup or sidebar
     * @returns {string} template
     */
    static async getTemplate(layer, style) {
        const resolveString = async (template) => {
            let result = null;
            if (template.startsWith('id::')) {
                if (template.substr(3) in PopupRenderer.templates) {
                    console.log('Using named popup template', template, 'for', layer.uniqueId);
                    result = PopupRenderer.templates[template.substr(3)];
                }
            } else if (template.startsWith('https://') || template.startsWith('https://')) {
                console.log('Using file based template', template, 'for', layer.uniqueId);
                try {
                    result = await PopupRenderer.fetchTemplate(template);
                }
                catch (error) {
                    console.warn(error);
                }
            } else {
                console.log('Using integrated template for', layer.uniqueId);
                result = template;
            }

            return result;
        }

        const template = layer.metadataTemplate;
        let result = null;

        if (typeof template == "string") {
            result = await resolveString(template);
        } else if ((typeof template == "object") && template) {
            if (!style || !(style in template)) style = "popup";
            if ((style in template) && (typeof template[style] == "string")) {
                result = await resolveString(template[style]);
            }
        }

        return (result) ? result : this.getDefaultTemplate(style, layer);
    }

    static renderTemplate(featureCollection, response, layer, style, template) {
        const responses = [];

        // interpret strings as markdown and resolve template syntax
        if (typeof template !== 'string') return responses;

        for (let i = 0; i < featureCollection.features.length; ++i) {
            const feature = featureCollection.features[i];
            const data = Object.assign({ layer: layer, response: response }, feature)
            try {
                const html = resolveComplexTemplate(template, data, {
                    markdown: true,
                    postProcess: true
                });
                enhanceHTML(html, (style == "sidebar") ? {} : { groupedHeadlines: false })

                responses.push({
                    response: response,
                    feature: feature,
                    layer: layer,
                    title: html.dataset.title || layer.title,
                    html: html
                });
            } catch (msg) {
                if (msg != "discard_template") console.warn(msg);
            }
        }

        return responses;
    }

}