Source: map/popup/MapPopup.js

import { EventObject } from "../../events/EventObject.js";
import { PopupPagination } from "./PopupPagination.js";
import { PopupHeader } from "./PopupHeader.js";
import { PopupRenderer } from "./PopupRenderer.js";
import { GeoJSONLayer } from "../layer/GeoJSONLayer/GeoJSONLayer.js";
import { reprojectGeojson } from "../utils/reprojectGeojson.js";
import "./MapPopup.css";
import { EventManager } from "../../events/EventManager.js";

export { MapPopup }

/**
 * Class for getting popup data and creating popups on Map
 *
 * @author sjaswal <shahzeib.jaswal@awi.de>
 * @author rkoppe <roland.koppe@awi.de>
 * @author rhess <robin.hess@awi.de>
 * 
 * @memberof vef.map.popup
 */

class MapPopup extends EventObject {

    /**
     * @param {Map} map 
     */
    constructor(map, options) {
        // set available events
        super({
            "show_popup": [],
            "backToSidebar": []
        });

        this.map_ = map;

        this.options_ = Object.assign({
            showMore: true
        }, options || {});

        this.pagination_ = new PopupPagination();
        this.header_ = new PopupHeader();
        this.promises_ = [];

        this.selectedFeature = new GeoJSONLayer({
            interactive: false,
            opacity: 1,
            opacityRatio: 0,
            style: {
                color: "#FFFFFF",
                weight: 4,
                radius: 7
            }
        });

        this.initHeightTesting_();
    }

    /**
     * Method that requests the featureinfo and displays it in a popup
     *
     * @param {Layer[]} layers
     * @param {object} bbox
     */
    requestPopup(layers, lat, lng) {
        const options = this.map_.getFeatureInfoOptions(lat, lng);

        const promises = [];
        for (let i = 0; i < layers.length; ++i) {
            if (layers[i].getFeatureInfo) promises.push(layers[i].getFeatureInfo(options));
        }
        this.promises_ = promises;

        // display placeholder
        this.map_.showPopup(lat, lng, `
            <div class="vef-map-popup-placeholder">
                <svg xmlns="https://www.w3.org/2000/svg" xmlns:xlink="https://www.w3.org/1999/xlink" style="margin: auto; shape-rendering: auto;" width="22px" height="22px" viewBox="0 0 100 100" preserveAspectRatio="xMidYMid">
                    <circle cx="50" cy="50" r="32" stroke-width="8" stroke="var(--primary-color)" stroke-dasharray="50.26548245743669 50.26548245743669" fill="none" stroke-linecap="round">
                        <animateTransform attributeName="transform" type="rotate" repeatCount="indefinite" dur="1s" keyTimes="0;1" values="0 50 50;360 50 50"></animateTransform>
                    </circle>
                </svg>
            </div>
        `);

        // wait until all layers have responded
        Promise.all(promises)
            .then(responses => {
                // don't render an old request that responded later
                if (this.promises_ != promises) return;
                // wait until the renderer has responded
                const rendererPromises = [];
                for (let i = 0; i < responses.length; ++i) {
                    rendererPromises.push(PopupRenderer.render(responses[i].data, responses[i].data, responses[i].layer, "popup"));
                }
                Promise.all(rendererPromises).then(content => {
                    const popupWrapper = this.buildPopup_(content);
                    this.map_.showPopup(lat, lng, popupWrapper);
                });
            });
    }

    initHeightTesting_() {
        let div = this.map_.getElement().querySelector(".popup-content-height-test")
        if (!div) {
            div = document.createElement("div");
            div.classList.add("popup-content-height-test", "vef-map-popup-content");

            this.map_.getElement().appendChild(div);
        }

        this.heightTestContainer_ = div;
    }

    /**
     * Calculate height of an element by adding it to an invisible container
     * and checking the offsetHeight
     * 
     * @param {HTMLElement} element 
     * @returns {number} height
     */
    testHeight_(element) {
        element.style.display = "block";

        this.heightTestContainer_.appendChild(element);

        const height = element.offsetHeight;

        element.style.display = "none";
        element.remove();

        return height;
    }

    buildPopup_(responses) {
        const items = [];
        let itemIndex = 0;

        this.header_.reset();

        // popup content wrapper element
        const $wrapper = document.createElement('div');

        const $content = document.createElement('div');
        $content.classList.add("vef-map-popup-content");

        let minHeight = 0;

        // prepare markup for features
        for (let i = 0; i < responses.length; ++i) {
            if (!responses[i]) continue;

            for (let j = 0; j < responses[i].length; ++j) {

                const content = responses[i][j];
                const item = {
                    popup: document.createElement('div'),
                    sidebar: null,
                    content: content
                };

                item.popup.style.display = 'none';

                const contentInner = document.createElement("div");
                contentInner.classList.add("vef-map-popup-content-inner");

                if (content.html instanceof HTMLElement) {
                    contentInner.appendChild(content.html);
                } else {
                    contentInner.insertAdjacentHTML("beforeend", content.html);
                }

                // add show more link
                if (this.options_.showMore && (content.feature || content.response)) {
                    const a = document.createElement("a");
                    a.classList.add("popup-link");
                    a.href = "#";
                    a.innerHTML = "<i class='fas fa-eye'></i> show more";
                    a.addEventListener("click", e => {
                        e.preventDefault();
                        if (item.onShowMore) item.onShowMore(item);
                    });
                    contentInner.appendChild(a);
                }

                // event button group
                const buttonGroup = document.createElement("div");
                buttonGroup.classList.add("vef-map-popup-button-group");

                // create buttons for the events
                const events = content.layer.getPopupEvents();
                for (let k = 0; k < events.length; ++k) {
                    const a = document.createElement("a");
                    a.classList.add("popup-button");
                    a.href = "#";
                    a.style.width = events[k].width || "100%";
                    a.innerHTML = events[k].title;
                    if (events[k].toggleMode && events[k].isEnabled()) {
                        a.classList.add("active");
                    }
                    a.addEventListener("click", e => {
                        e.preventDefault();
                        events[k].callback(item);
                        if (events[k].toggleMode) {
                            if (a.classList.contains("active")) {
                                a.classList.remove("active");
                            } else {
                                a.classList.add("active");
                            }
                        }
                    });

                    buttonGroup.appendChild(a);
                }

                this.header_.addLayer(content.layer);
                item.popup.appendChild(contentInner);
                item.popup.appendChild(buttonGroup);

                const height = this.testHeight_(item.popup);
                if (height > minHeight) minHeight = height;

                $content.appendChild(item.popup);
                items.push(item);
            }
        }

        $content.style.minHeight = minHeight + "px";

        if (items.length == 0) {
            // set popup placeholder for empty content
            $wrapper.innerHTML = `
                <div class="vef-map-popup-placeholder">
                    <p>
                        No data to display!
                    </p>
                </div>
            `;
            return $wrapper;
        }

        this.header_.initDropdown();
        $wrapper.appendChild(this.header_.getElement());
        $wrapper.appendChild($content);

        // event listener for handling prev/next navigation
        const hideAndShow = (index) => {
            items[itemIndex].popup.style.display = 'none';
            itemIndex = index;

            const item = items[itemIndex];
            const content = item.content;

            item.popup.style.display = 'block';

            this.header_.setLayer(content.layer, content.title);

            // attach the sidebar content to the item and fire the "show_popup" event
            if (content.feature || content.response) {
                PopupRenderer.render(content.feature || content.response, content.response, content.layer, 'sidebar')
                    .then(sidebar => {
                        item.sidebar = sidebar[0].html;
                        if (content.feature) {
                            this.selectedFeature.setData({
                                type: "Feature",
                                geometry: (content?.response?.crs?.properties?.name)
                                    ? reprojectGeojson(content?.response?.crs?.properties?.name, "EPSG:4326", content.feature.geometry)
                                    : content.feature.geometry,
                                properties: {}
                            });
                        } else {
                            this.selectedFeature.setData(null);
                        }
                        this.fire("show_popup", item);
                        EventManager.fire("backToSidebar")
                    });
            } else {
                this.selectedFeature.setData(null);
            }
        }

        // show content of the first entry
        hideAndShow(0);

        if (items.length > 1) {
            this.pagination_.off("change");
            this.pagination_.setPageCount(items.length);
            this.pagination_.setPage(1);
            this.pagination_.on("change", page => hideAndShow(page - 1));
            $wrapper.appendChild(this.pagination_.getElement());

            this.header_.on("show_index", index => this.pagination_.setPage(index + 1));
        }

        return $wrapper;
    }

}