diff --git a/assets/src/components/PresentationCards.js b/assets/src/components/PresentationCards.js
new file mode 100644
index 0000000000..909cf9acee
--- /dev/null
+++ b/assets/src/components/PresentationCards.js
@@ -0,0 +1,446 @@
+/**
+ * @module components/PresentationCards.js
+ * @name PresentationCards
+ * @copyright 2023 3Liz
+ * @author DOUCHIN Michaël
+ * @license MPL-2.0
+ */
+
+import { mainLizmap } from '../modules/Globals.js';
+
+/**
+ * @class
+ * @name PresentationCards
+ * @augments HTMLElement
+ */
+export default class PresentationCards extends HTMLElement {
+
+ constructor() {
+ super();
+
+ // Id of the component
+ this.id = this.getAttribute('id');
+
+ // Presentations
+ this.presentations = [];
+
+ // Attribute to force the refresh of data
+ // Store the last refresh timestamp
+ this.updated = this.getAttribute('updated');
+
+ // Presentation which must be shown
+ this.detail = this.getAttribute('detail');
+ }
+
+ async load() {
+
+ // Get presentations related to the element scope from query
+ mainLizmap.presentation.getPresentations()
+ .then(data => {
+ // Set property
+ this.presentations = data;
+
+ // Render
+ this.render();
+
+ // If a presentation was running, display it again
+ if (mainLizmap.presentation.ACTIVE_LIZMAP_PRESENTATION !== null) {
+ // Get active page
+ mainLizmap.presentation.runLizmapPresentation(
+ mainLizmap.presentation.ACTIVE_LIZMAP_PRESENTATION,
+ mainLizmap.presentation.LIZMAP_PRESENTATION_ACTIVE_PAGE_NUMBER
+ );
+ }
+ })
+ .catch(err => console.log(err))
+
+ }
+
+ getFieldDisplayHtml(field, fieldValue) {
+ let fieldHtml = fieldValue;
+ if (['background_image', 'illustration_media'].includes(field)) {
+ const mediaUrl = `${lizUrls.media}?repository=${lizUrls.params.repository}&project=${lizUrls.params.project}&path=`;
+ const fileExtension = fieldValue.split('.').pop().toLowerCase();
+ if (['webm', 'mp4'].includes(fileExtension)) {
+ fieldHtml = `
+
+ `;
+ } else if (['png', 'webp', 'jpeg', 'jpg', 'gif'].includes(fileExtension)) {
+ fieldHtml = ``;
+ } else {
+ fieldHtml = fieldValue;
+ }
+ }
+
+ return fieldHtml;
+ }
+
+ render() {
+
+ // Check if a specific presentation must be shown
+ const activePresentationId = parseInt(this.getAttribute('detail'));
+
+ // Remove previous content
+ this.innerHTML = '';
+
+ // Get the base content of a card from the template
+ const createTemplate = document.getElementById('lizmap-presentation-create-button-template');
+ this.innerHTML = createTemplate.innerHTML;
+
+ // Get the base content of a card from the template
+ const cardTemplate = document.getElementById('lizmap-presentation-card-template');
+ for (const a in this.presentations) {
+ // Get the presentation
+ const presentation = this.presentations[a];
+
+ // Create the div and fill it with the template content
+ let div = document.createElement("div");
+ div.classList.add('lizmap-presentation-card');
+ div.dataset.id = presentation.id;
+
+ // Change display if given presentation ID is an integer
+ let cardDisplay = 'normal';
+ if (activePresentationId > 0) {
+ const currentPresentationId = parseInt(presentation.id);
+ const isDetail = (parseInt(currentPresentationId) == activePresentationId);
+ if (isDetail) {
+ cardDisplay = 'detail';
+ } else {
+ cardDisplay = 'none';
+ }
+ }
+ div.dataset.display = cardDisplay;
+ div.innerHTML = cardTemplate.innerHTML;
+
+ // Title
+ div.querySelector('h3.lizmap-presentation-title').innerHTML = `
+ ${presentation.title}${presentation.id}
+ `;
+
+ // Description
+ div.querySelector('p.lizmap-presentation-description').innerHTML = presentation.description;
+
+ // Detailed information
+ const table = div.querySelector('table.presentation-detail-table');
+ const fields = [
+ 'background_color', 'background_image',
+ 'footer', 'published', 'granted_groups',
+ 'created_by', 'created_at', 'updated_by', 'updated_at'
+ ];
+ fields.forEach(field => {
+ let fieldValue = (!presentation[field]) ? '' : presentation[field];
+ const fieldHtml = this.getFieldDisplayHtml(field, fieldValue);
+ table.querySelector(`td.presentation-detail-${field}`).innerHTML = fieldHtml;
+ })
+
+ // Buttons
+ const detailButton = div.querySelector('button.liz-presentation-detail')
+ detailButton.value = presentation.id;
+ detailButton.innerText = (cardDisplay != 'detail') ? detailButton.dataset.label : detailButton.dataset.reverseLabel;
+ div.querySelector('button.liz-presentation-edit').value = presentation.id;
+ div.querySelector('button.liz-presentation-delete').value = presentation.id;
+ div.querySelector('button.liz-presentation-launch').value = presentation.id;
+ div.querySelector('button.liz-presentation-create.page').value = presentation.id;
+
+ // Add pages preview (small vertical view of mini pages)
+ this.renderPagesPreview(div, presentation);
+
+ // Add the card to the parent
+ this.appendChild(div);
+ }
+
+ // Add click event on the create button
+ const createButtons = this.querySelectorAll("button.liz-presentation-create");
+ Array.from(createButtons).forEach(createButton => {
+ createButton.addEventListener("click", this.onButtonCreateClick);
+ });
+
+ // Add click event on the presentation cards buttons
+ const buttons = this.querySelectorAll("div.lizmap-presentation-card-toolbar button, div.lizmap-presentation-page-preview-toolbar button");
+ Array.from(buttons).forEach(button => {
+ const classes = button.classList;
+ if (classes.contains('liz-presentation-edit')) {
+ button.addEventListener('click', this.onButtonEditClick);
+ } else if (classes.contains('liz-presentation-delete')) {
+ button.addEventListener('click', this.onButtonDeleteClick);
+ } else if (classes.contains('liz-presentation-detail')) {
+ button.addEventListener('click', this.onButtonDetailClick);
+ } else if (classes.contains('liz-presentation-launch')) {
+ button.addEventListener('click', this.onButtonLaunchClick);
+ }
+ });
+ }
+
+ /**
+ * Render a presentation page preview
+ *
+ * @param {Object} page Presentation page object
+ *
+ * @return {HTMLDivElement} Page div to insert in the list
+ */
+ getPagePreview(page) {
+ const pagePreviewTemplate = document.getElementById('lizmap-presentation-page-preview-template');
+ const previewHtml = pagePreviewTemplate.innerHTML;
+
+ let pageDiv = document.createElement('div');
+ pageDiv.classList.add('lizmap-presentation-page-preview');
+ pageDiv.dataset.presentationId = page.presentation_id;
+ pageDiv.dataset.pageId = page.id;
+ pageDiv.dataset.pageOrder = page.page_order;
+ pageDiv.innerHTML = previewHtml;
+ pageDiv.querySelector('h3.lizmap-presentation-page-preview-title').innerHTML = `
+ ${page.title}${page.page_order}
+ `;
+
+ // Detailed information
+ const pageTable = pageDiv.querySelector('table.presentation-detail-table');
+ const pageFields = [
+ // 'description'
+ //, 'background_image'
+ ];
+ pageFields.forEach(field => {
+ const pageTd = pageTable.querySelector(`td.presentation-page-${field}`);
+ if (pageTd) {
+ let pageFieldValue = (!page[field]) ? '' : page[field];
+ const pageFieldHtml = this.getFieldDisplayHtml(field, pageFieldValue);
+ pageTd.innerHTML = pageFieldHtml;
+ }
+ })
+
+ pageDiv.querySelector('button.liz-presentation-edit').value = page.id;
+ pageDiv.querySelector('button.liz-presentation-delete').value = page.id;
+
+ return pageDiv;
+ }
+
+ /**
+ * Render the pages preview of the given presentation
+ * and add content in the given presentation div
+ *
+ * @param {HTMLDivElement} presentationDiv Div which must contain the pages preview
+ * @param {Object} presentation Presentation properties
+ */
+ renderPagesPreview(presentationDiv, presentation) {
+ let pageContainer = presentationDiv.querySelector('div.lizmap-presentation-card-pages-preview');
+ presentation.pages.forEach(page => {
+ const pagePreviewDiv = this.getPagePreview(page);
+ pagePreviewDiv.setAttribute('draggable', 'true');
+ pagePreviewDiv.addEventListener('dragstart', onDragStart)
+ pagePreviewDiv.addEventListener('drop', OnDropped)
+ pagePreviewDiv.addEventListener('dragenter', onDragEnter)
+ pagePreviewDiv.addEventListener('dragover', onDragOver)
+ pageContainer.appendChild(pagePreviewDiv);
+ })
+
+ // Utility functions for drag & drop capability
+ function onDragStart (e) {
+ const index = [].indexOf.call(e.target.parentElement.children, e.target);
+ e.dataTransfer.setData('text/plain', index)
+ }
+
+ function onDragEnter (e) {
+ cancelDefault(e);
+ }
+
+ function onDragOver (e) {
+ cancelDefault(e);
+ }
+
+ function OnDropped (e) {
+ cancelDefault(e)
+
+ // Get item
+ const item = e.currentTarget;
+
+ // Get dragged item old and new index
+ const oldIndex = e.dataTransfer.getData('text/plain');
+ const newIndex = [].indexOf.call(item.parentElement.children, item);
+
+ // Get the dropped item
+ const dropped = item.parentElement.children[oldIndex];
+
+ // Move the dropped items at new place
+ if (newIndex < oldIndex) {
+ item.before(dropped);
+ } else {
+ item.after(dropped);
+ }
+
+ // Recalculate page numbers
+ let i = 1;
+ let pagesNumbers = {};
+ let pagesIds = [];
+ let presentationId = null;
+ for (const child of item.parentElement.children) {
+ if (!child.classList.contains('lizmap-presentation-page-preview')) {
+ continue;
+ }
+ const pageOrder = i;
+ child.dataset.pageOrder = pageOrder;
+ child.querySelector('h3.lizmap-presentation-page-preview-title > span').innerText = pageOrder;
+ presentationId = child.dataset.presentationId;
+ const pageId = child.dataset.pageId;
+ pagesNumbers[pageId] = pageOrder;
+ pagesIds.push(pageId);
+ i++;
+ }
+
+ if (presentationId !== null) {
+ // Set the component presentation pages object
+ const presentation = mainLizmap.presentation.getPresentationById(presentationId);
+ let newPages = [];
+ for (const i in pagesIds) {
+ const correspondingPage = presentation.pages.find(x => x.id === pagesIds[i]);
+ newPages.push(correspondingPage);
+ }
+ presentation.pages = newPages;
+
+ // Send new pagesNumbers to the server
+ mainLizmap.presentation.setPresentationPagination(presentationId, pagesNumbers)
+ .then(data => {
+ console.log('pagination modifiée');
+
+ })
+ .catch(err => console.log(err))
+ }
+ }
+
+ function cancelDefault (e) {
+ e.preventDefault();
+ e.stopPropagation();
+
+ return false;
+ }
+
+ }
+
+ connectedCallback() {
+ this.load();
+ }
+
+ static get observedAttributes() { return ['updated']; }
+
+ attributeChangedCallback(name, oldValue, newValue) {
+ // Listen to the change of the updated attribute
+ // This will trigger the load (refresh the content)
+ if (name === 'updated') {
+ this.load();
+ }
+ }
+
+ getPresentationById(presentationId) {
+ for (let a in this.presentations) {
+ let presentation = this.presentations[a];
+ if (presentation.id == presentationId) {
+ return presentation;
+ }
+ }
+
+ return null;
+ }
+
+ onButtonCreateClick(event) {
+ // Get the host component
+ const host = event.target.closest("lizmap-presentation-cards");
+ const button = event.currentTarget;
+ const item = (button.classList.contains('presentation')) ? 'presentation' : 'page';
+ if (item == 'presentation') {
+ mainLizmap.presentation.launchPresentationCreationForm(item);
+ } else {
+ const presentation_id = button.value;
+ mainLizmap.presentation.launchPresentationCreationForm(item, null, presentation_id);
+ }
+ }
+
+ onButtonEditClick(event) {
+ const button = event.currentTarget;
+ const item = (button.classList.contains('presentation')) ? 'presentation' : 'page';
+ const id = button.value;
+ mainLizmap.presentation.launchPresentationCreationForm(item, id);
+ }
+
+ onButtonDetailClick(event) {
+ const host = event.target.closest("lizmap-presentation-cards");
+ const button = event.currentTarget;
+ const presentationId = button.value;
+
+ // Chosen card
+ const chosenCard = host.querySelector(`[data-id='${presentationId}']`);
+ const isActive = (chosenCard.dataset.display == 'detail');
+
+ // Set other cards status
+ host.presentations.forEach(item => {
+ const card = host.querySelector(`[data-id='${item.id}']`);
+ const display = (isActive) ? 'normal' : 'none';
+ card.dataset.display = display;
+ })
+
+ // Set the clicked card display property
+ chosenCard.dataset.display = (isActive) ? 'normal' : 'detail';
+
+ // Set its detail button label & title
+ button.innerText = (isActive) ? button.dataset.label : button.dataset.reverseLabel;
+ button.setAttribute('title', (isActive) ? button.dataset.title : button.dataset.reverseTitle);
+
+ // Set the full panel class
+ const parentDiv = document.getElementById('presentation-container');
+ parentDiv.dataset.display = (isActive) ? 'normal' : 'detail';
+ }
+
+ onButtonDeleteClick(event) {
+ const host = event.target.closest("lizmap-presentation-cards");
+ const button = event.currentTarget;
+ const item = (button.classList.contains('presentation')) ? 'presentation' : 'page';
+ const id = button.value;
+ // Confirmation message
+ const confirmMessage = button.dataset.confirm;
+ const areYourSure = window.confirm(confirmMessage);
+ if (!areYourSure) {
+ return false;
+ }
+ mainLizmap.presentation.deletePresentation(item, id);
+
+ // Set all presentations visible
+ const parentDiv = document.getElementById('presentation-container');
+ parentDiv.dataset.display = 'normal';
+ host.presentations.forEach(item => {
+ const card = host.querySelector(`[data-id='${item.id}']`);
+ card.dataset.display = 'normal';
+ })
+ }
+
+ onButtonLaunchClick(event) {
+ const host = event.target.closest("lizmap-presentation-cards");
+ const button = event.currentTarget;
+ const presentationId = button.value;
+
+ // Get presentation item
+ const presentation = host.getPresentationById(presentationId);
+ if (presentation === null) {
+ return false;
+ }
+
+ mainLizmap.presentation.runLizmapPresentation(presentationId);
+ }
+
+
+ disconnectedCallback() {
+ // Remove click events on the presentation buttons
+ const createButton = this.querySelector("button.liz-presentation-create");
+ createButton.removeEventListener("click", this.onButtonCreateClick);
+ const buttons = this.querySelectorAll("div.lizmap-presentation-card-toolbar button, div.lizmap-presentation-page-preview-toolbar button");
+ Array.from(buttons).forEach(button => {
+ if (button.classList.contains('liz-presentation-edit')) {
+ button.removeEventListener('click', this.onButtonEditClick);
+ } else if (button.classList.contains('liz-presentation-delete')) {
+ button.removeEventListener('click', this.onButtonDeleteClick);
+ } else if (button.classList.contains('liz-presentation-detail')) {
+ button.removeEventListener('click', this.onButtonDetailClick);
+ } else if (button.classList.contains('liz-presentation-launch')) {
+ button.removeEventListener('click', this.onButtonLaunchClick);
+ }
+ });
+ }
+}
diff --git a/assets/src/components/PresentationPage.js b/assets/src/components/PresentationPage.js
new file mode 100644
index 0000000000..dba516dc70
--- /dev/null
+++ b/assets/src/components/PresentationPage.js
@@ -0,0 +1,252 @@
+/**
+ * @module components/PresentationPage.js
+ * @name PresentationPage
+ * @copyright 2023 3Liz
+ * @author DOUCHIN Michaël
+ * @license MPL-2.0
+ */
+
+import { mainLizmap } from '../modules/Globals.js';
+
+/**
+ * @class
+ * @name PresentationPage
+ * @augments HTMLElement
+ */
+export default class PresentationPage extends HTMLElement {
+
+ constructor() {
+ super();
+
+ // UUID of the page
+ this.uuid = this.getAttribute('data-uuid');
+
+ // Page number
+ this.number = this.getAttribute('data-page-number');
+
+ // Page visibility
+ this.active = this.getAttribute('data-active');
+
+ // Presentation properties
+ this._presentation = null;
+
+ // Properties
+ this._properties = null;
+ }
+
+ load() {
+ this.render();
+ }
+
+ render() {
+ // Base URL for media files
+ const mediaUrl = `${lizUrls.media}?repository=${lizUrls.params.repository}&project=${lizUrls.params.project}&path=`;
+
+ // Remove previous content
+ this.innerHTML = '';
+
+ // Get the base content of a card from the template
+ const pageTemplate = document.getElementById('lizmap-presentation-page-template');
+ this.innerHTML = pageTemplate.innerHTML;
+
+ // Set the content of the child HTML elements
+ if (this._properties === null) {
+ return;
+ }
+
+ // Anchor
+ const pageAnchor = this.querySelector('a.lizmap-presentation-page-anchor');
+ pageAnchor.setAttribute('name', this._properties['page_order']);
+
+ // Toolbar buttons
+ const editButton = this.querySelector('button.liz-presentation-edit.page');
+ editButton.value = this._properties['id'];
+ if (editButton) {
+ editButton.addEventListener('click', function(event) {
+ const button = event.currentTarget;
+ const id = button.value;
+ mainLizmap.presentation.launchPresentationCreationForm('page', id);
+ });
+ }
+ // title of the page
+ const pageTitle = this.querySelector('h2.lizmap-presentation-page-title');
+ pageTitle.innerHTML = this._properties['title'];
+
+ // Content
+ const pageContent = this.querySelector('div.lizmap-presentation-page-content');
+ const textDiv = pageContent.querySelector('.lizmap-presentation-page-text');
+ const illustrationDiv = pageContent.querySelector('.lizmap-presentation-page-media');
+
+ // Page model
+ const pageModel = this._properties['model'];
+
+ // Description
+ const description = this._properties['description'];
+ if (pageModel != 'media') {
+ textDiv.innerHTML = description;
+ }
+
+ // Illustration
+ let illustrationHtml = '';
+ let illustrationValue = '';
+ if (pageModel != 'text') {
+ switch (this._properties['illustration_type']) {
+ case 'none':
+ case '':
+ case null:
+ break;
+ case 'image':
+ illustrationValue = this._properties['illustration_media'];
+ illustrationHtml = `
+
+ `;
+ break;
+ case 'video':
+ illustrationValue = this._properties['illustration_media'];
+ illustrationHtml = `
+
+ `;
+ break;
+ case 'iframe':
+ illustrationValue = this._properties['illustration_url'];
+ illustrationHtml = `
+
+ `;
+ break;
+ case 'popup':
+ illustrationHtml = `
+
POPUP CONTENT
+ `;
+ break;
+ default:
+ console.log(`Illustration type ${this._properties['illustration_type']} not valid.`);
+
+ }
+ illustrationDiv.innerHTML = illustrationHtml;
+ }
+
+ let flexDirection = 'column';
+ switch (pageModel) {
+ case 'text':
+ case null:
+ break;
+ case 'media':
+ break;
+ case 'text-left-media':
+ flexDirection = 'row';
+ break;
+ case 'media-left-text':
+ flexDirection = 'row-reverse';
+ break;
+ case 'text-above-media':
+ flexDirection = 'column';
+ break;
+ case 'media-above-text':
+ flexDirection = 'column-reverse';
+ break;
+ default:
+ console.log(`Model ${pageModel} for page ${this._properties['title']}.`);
+ }
+ pageContent.style.flexDirection = flexDirection;
+
+ // Set some properties
+ // Nullify text div padding if it must not be visible
+ textDiv.style.margin = (pageModel != 'media') ? '20px' : '0px';
+
+ // Nullify flex if object must not be visible
+ textDiv.style.flex = (pageModel != 'media') ? '1' : '0';
+ illustrationDiv.style.flex = !(['text'].includes(pageModel)) ? '1' : '0';
+
+ // Page background color from the presentation data
+ if (this._presentation['background_color']) {
+ this.style.backgroundColor = this._presentation['background_color'];
+ }
+ // override it if the page background color is also set
+ if (this._properties['background_color']) {
+ this.style.backgroundColor = this._properties['background_color'];
+ }
+
+ // Page background image from the presentation data
+ if (this._presentation['background_image']) {
+ this.style.backgroundImage = `url(${mediaUrl}${this._presentation['background_image']})`;
+ this.classList.add(`background-${this._presentation['background_display']}`);
+ }
+ // override it if the page background color is also set
+ if (this._properties['background_image']) {
+ this.style.backgroundImage = `url(${mediaUrl}${this._properties['background_image']})`;
+ // Force cover - TODO add a new field
+ this.classList.add(`background-cover`);
+ }
+
+ }
+
+ /**
+ * Set the parent presentation properties
+ *
+ */
+ set presentation(value) {
+ this._presentation = value;
+ }
+
+ /**
+ * Get the page properties
+ */
+ get presentation() {
+ return this._presentation;
+ }
+
+ /**
+ * Set the page properties
+ *
+ */
+ set properties(value) {
+ this._properties = value;
+
+ // Re-render with new data
+ this.render();
+ }
+
+ /**
+ * Get the page properties
+ */
+ get properties() {
+ return this._properties;
+ }
+
+ connectedCallback() {
+ this.load();
+ }
+
+ static get observedAttributes() { return ['data-active']; }
+
+ attributeChangedCallback(name, oldValue, newValue) {
+ // Listen to the change of specific attributes
+ console.log(`attribute active changed from ${oldValue} to ${newValue}`);
+ }
+
+ disconnectedCallback() {
+ // Remove page events
+ }
+}
diff --git a/assets/src/index.js b/assets/src/index.js
index 21a9c46574..bd537b8f0f 100644
--- a/assets/src/index.js
+++ b/assets/src/index.js
@@ -26,6 +26,11 @@ import NavBar from './components/NavBar.js';
import Tooltip from './components/Tooltip.js';
import Message from './components/Message.js';
+
+import PresentationCards from './components/PresentationCards.js';
+import PresentationPage from './components/PresentationPage.js';
+
+
import { mainLizmap, mainEventDispatcher } from './modules/Globals.js';
import executeJSFromServer from './modules/ExecuteJSFromServer.js';
@@ -58,10 +63,11 @@ lizMap.events.on({
window.customElements.define('lizmap-tooltip', Tooltip);
window.customElements.define('lizmap-message', Message);
+
+ window.customElements.define('lizmap-presentation-cards', PresentationCards);
+ window.customElements.define('lizmap-presentation-page', PresentationPage);
+
lizMap.mainLizmap = mainLizmap;
lizMap.mainEventDispatcher = mainEventDispatcher;
-
}
});
-
-executeJSFromServer();
diff --git a/assets/src/modules/Lizmap.js b/assets/src/modules/Lizmap.js
index 929dc2f9a8..1611c7435e 100644
--- a/assets/src/modules/Lizmap.js
+++ b/assets/src/modules/Lizmap.js
@@ -25,6 +25,7 @@ import Legend from './Legend.js';
import Permalink from './Permalink.js';
import Search from './Search.js';
import Tooltip from './Tooltip.js';
+import Presentation from './Presentation.js';
import WMSCapabilities from 'ol/format/WMSCapabilities.js';
import { Coordinate as olCoordinate } from 'ol/coordinate.js'
@@ -63,6 +64,7 @@ export default class Lizmap {
proj4.defs(ref, def);
}
}
+
// Register project projection if unknown
const configProj = this._initialConfig.options.projection;
if (configProj.ref !== "" && !proj4.defs(configProj.ref)) {
@@ -164,6 +166,7 @@ export default class Lizmap {
this.legend = new Legend();
this.search = new Search();
this.tooltip = new Tooltip();
+ this.presentation = new Presentation();
// Removed unusable button
if (!this.config['printTemplates'] || this.config.printTemplates.length == 0 ) {
diff --git a/assets/src/modules/Presentation.js b/assets/src/modules/Presentation.js
new file mode 100644
index 0000000000..de5e7248c0
--- /dev/null
+++ b/assets/src/modules/Presentation.js
@@ -0,0 +1,919 @@
+/**
+ * @module modules/Presentation.js
+ * @name Presentation
+ * @copyright 2023 3Liz
+ * @author DOUCHIN Michaël
+ * @license MPL-2.0
+ */
+
+import { mainLizmap } from '../modules/Globals.js';
+import { Vector as VectorSource } from 'ol/source.js';
+import { Vector as VectorLayer } from 'ol/layer.js';
+import GeoJSON from 'ol/format/GeoJSON.js';
+import Utils from './Utils.js';
+
+/**
+ * @class
+ * @name Presentation
+ */
+export default class Presentation {
+
+ /**
+ * @boolean If the project has presentations
+ */
+ hasPresentations = false;
+
+ /**
+ * @object List of presentations
+ */
+ presentations = [];
+
+ /**
+ * @string Unique ID of an presentation object
+ * We allow only one active presentation at a time
+ */
+ ACTIVE_LIZMAP_PRESENTATION = null;
+
+ /**
+ * @string Active page uuid
+ */
+ LIZMAP_PRESENTATION_ACTIVE_PAGE_UUID = null;
+
+ /**
+ * @string Active page number
+ */
+ LIZMAP_PRESENTATION_ACTIVE_PAGE_NUMBER = 1;
+
+ /**
+ * @string Active presentation number of pages
+ */
+ activePresentationPagesCount = 0;
+
+ /**
+ * OpenLayers vector layer to draw the presentation results
+ */
+ presentationLayer = null;
+
+ // Original map div left margin
+ mapLeftMargin = '30px';
+
+ /**
+ * Build the lizmap presentation instance
+ */
+ constructor() {
+
+ this.hasPresentations = true;
+ if (typeof presentationConfig === 'undefined') {
+ this.hasPresentations = false;
+ }
+
+ if (this.hasPresentations) {
+
+ // Get the list of presentations
+ this.presentations = presentationConfig;
+
+ // Hide the presentation dock if no presentation exists
+ // And the user has no right to manage them
+ const hideDock = false;
+ if (hideDock) {
+ let presentationMenu = document.querySelector('#mapmenu li.presentation');
+ if (presentationMenu) {
+ presentationMenu.style.display = "none";
+ }
+ }
+
+ // Add an OpenLayers layer to show & use the geometries returned by an presentation
+ this.createPresentationMapLayer();
+
+ // Add a div which will contain the slideshow
+ const slidesDiv = document.createElement('div');
+ slidesDiv.id = 'lizmap-presentation-slides-container';
+ const slidesDivTemplate = document.getElementById('lizmap-presentation-slides-container-template');
+ slidesDiv.innerHTML = slidesDivTemplate.innerHTML;
+ const containerId = 'presentation';
+ document.getElementById(containerId).appendChild(slidesDiv);
+
+ // Also add the button allowing to toggle back the presentation when minified
+ const slidesRestoreDiv = document.createElement('div');
+ slidesRestoreDiv.id = 'lizmap-presentation-slides-minified-toolbar-container';
+ const slidesRestoreTemplate = document.getElementById('lizmap-presentation-slides-minified-toolbar-template');
+ slidesRestoreDiv.innerHTML = slidesRestoreTemplate.innerHTML;
+ document.getElementById(containerId).appendChild(slidesRestoreDiv);
+
+ // React on the main Lizmap events
+ mainLizmap.lizmap3.events.on({
+ });
+ }
+
+ // Keep the map left margin before running the presentation
+ let olMapDiv = document.getElementById('map');
+ if (!olMapDiv) {
+ olMapDiv = document.getElementById('newOlMap');
+ }
+ if (olMapDiv) {
+ this.mapLeftMargin = olMapDiv.style.marginLeft;
+ }
+ }
+
+ /**
+ * Create the OpenLayers layer to display the presentation geometries.
+ *
+ */
+ createPresentationMapLayer() {
+ // Create the OL layer
+ const strokeColor = 'blue';
+ const strokeWidth = 3;
+ const fillColor = 'rgba(173,216,230,0.8)'; // lightblue
+ this.presentationLayer = new VectorLayer({
+ source: new VectorSource({
+ wrapX: false
+ }),
+ style: {
+ 'circle-radius': 6,
+ 'circle-stroke-color': strokeColor,
+ 'circle-stroke-width': strokeWidth,
+ 'circle-fill-color': fillColor,
+ 'stroke-color': strokeColor,
+ 'stroke-width': strokeWidth,
+ 'fill-color': fillColor,
+ }
+ });
+
+ // Add the layer inside Lizmap objects
+ mainLizmap.map.addLayer(this.presentationLayer);
+ }
+
+ /**
+ * Get an presentation item by its uid.
+ *
+ * @param {integer} presentationId - id of the presentation
+ *
+ * @return {null|object} The corresponding presentation data
+ */
+ getPresentationById(presentationId) {
+ if (!this.hasPresentations) {
+ return null;
+ }
+
+ // Get presentation data from the PresentationCards component
+ const presentationCards = document.querySelector('lizmap-presentation-cards');
+ if (presentationCards === null) {
+ return null;
+ }
+ for (const p in presentationCards.presentations) {
+ const presentation = presentationCards.presentations[p];
+ if (presentation && presentation.id == presentationId) {
+ return presentation;
+ }
+ }
+
+ return null;
+ }
+
+ /**
+ * Get the list of presentations
+ *
+ * @return {Promise} presentations - Promise with the JSON list of presentations
+ */
+ getPresentations() {
+
+ const url = presentationConfig.url;
+ let formData = new FormData();
+ formData.append('request', 'list');
+
+ // Return promise
+ return fetch(url, {
+ method: 'POST',
+ body: formData
+ }).then(function (response) {
+ if (response.ok) {
+ return response.json();
+ }
+ return Promise.reject(response);
+ }).then(function (json) {
+ return json;
+ }).catch(function (error) {
+ console.warn(error);
+ });
+ }
+
+ /**
+ * Set the pagination for a list of given presentation
+ *
+ * @param {integer} presentationId ID of the presentation
+ * @param {array} pages Array with the page ID as key and page number as value
+ * @return {Promise} Modified presentation - Promise with the JSON of the presentations
+ */
+ setPresentationPagination(presentationId, pages) {
+
+ const url = presentationConfig.url;
+ let formData = new FormData();
+ formData.append('request', 'set_pagination');
+ formData.append('id', presentationId);
+ formData.append('pages', JSON.stringify(pages));
+
+ // Return promise
+ return fetch(url, {
+ method: 'POST',
+ body: formData
+ }).then(function (response) {
+ if (response.ok) {
+ return response.json();
+ }
+ return Promise.reject(response);
+ }).then(function (json) {
+ return json;
+ }).catch(function (error) {
+ console.warn(error);
+ });
+ }
+
+
+
+
+ /**
+ * Run a Lizmap presentation.
+ *
+ * @param {integer} presentationId - The presentation id
+ * @param {integer} pageNumber - The page number to go to
+ */
+ async runLizmapPresentation(presentationId, pageNumber = 0) {
+ if (!this.hasPresentations) {
+ this.addMessage('No presentation found for the current Lizmap map !', 'error', 5000);
+
+ return false;
+ }
+
+ // Get the presentation
+ let presentation = this.getPresentationById(presentationId);
+ if (presentation === null) {
+ this.addMessage('No corresponding presentation found in the configuration !', 'error', 5000);
+
+ return false;
+ }
+
+ // Set current presentation page number
+ this.activePresentationPagesCount = presentation.pages.length;
+
+ // Reset the other presentations
+ // We allow only one active presentation at a time
+ // We do not remove the active status of the button (btn-primary)
+ this.resetLizmapPresentation(true, true, true, false);
+ try {
+ // Reset its content from the template
+ const slidesContainer = document.getElementById('lizmap-presentation-slides-container');
+ const slidesDivTemplate = document.getElementById('lizmap-presentation-slides-container-template');
+ slidesContainer.innerHTML = slidesDivTemplate.innerHTML;
+
+ // slidesContainer background color
+ slidesContainer.style.backgroundColor = presentation.background_color;
+
+ // Create an observer for page visibility
+ let observers = {};
+ let pageObserverOptions = {
+ root: slidesContainer,
+ rootMargin: "0px",
+ threshold: [0, 0.25, 0.5, 0.75, 1],
+ };
+ const pageIntersectionCallback = (entries) => {
+ entries.forEach((entry) => {
+
+ const page = entry.target;
+ const uuid = page.dataset.uuid;
+ const visiblePct = `${Math.floor(entry.intersectionRatio * 100)}`;
+ if (visiblePct >= 50) {
+ page.classList.add('active');
+ mainLizmap.presentation.onPageVisible(page);
+ } else {
+ page.classList.remove('active');
+ }
+
+ });
+ };
+
+ // Add the pages
+ let targetPageElement = null;
+ presentation.pages.forEach(page => {
+ const presentationPage = document.createElement('lizmap-presentation-page');
+ presentationPage.dataset.uuid = page.uuid;
+ presentationPage.dataset.number = page.page_order;
+ presentationPage.presentation = presentation;
+ presentationPage.properties = page;
+ slidesContainer.appendChild(presentationPage);
+
+ // Add intersection observer
+ const observer = new IntersectionObserver(
+ pageIntersectionCallback,
+ pageObserverOptions,
+ );
+ observers[page.uuid] = observer;
+ observers[page.uuid].observe(presentationPage);
+
+ // Store first page component
+ if (targetPageElement === null || page.page_order == pageNumber) {
+ targetPageElement = presentationPage;
+ }
+ })
+
+ // Set the presentation slides container visible
+ const dock = document.getElementById('dock');
+ dock.classList.add('presentation-visible');
+ slidesContainer.classList.add('visible');
+ slidesContainer.classList.add('visible');
+
+ // Set first page as active
+ let targetPageNumber = (pageNumber > 0) ? pageNumber : 1;
+ if (targetPageElement !== null) {
+ this.goToGivenPage(targetPageNumber);
+ mainLizmap.presentation.onPageVisible(targetPageElement, true);
+ }
+
+ /**
+ * Lizmap event to allow other scripts to process the data if needed
+ * @event presentationLaunched
+ * @property {string} presentationId Id of the presentation
+ */
+ lizMap.events.triggerEvent("presentationLaunched",
+ {
+ 'presentation': presentationId
+ }
+ );
+
+ // Set the presentation as active
+ this.ACTIVE_LIZMAP_PRESENTATION = presentationId;
+
+ } catch (error) {
+ // Display the error
+ console.warn(error);
+ this.addMessage(error, 'error', 5000);
+
+ // Reset the presentation
+ this.resetLizmapPresentation(true, true, true, true);
+
+ }
+ }
+
+ /**
+ * Reset presentation
+ *
+ * @param {boolean} destroyFeatures - If we must remove the geometries in the map.
+ * @param {boolean} removeMessage - If we must remove the message displayed at the top.
+ * @param {boolean} resetGlobalVariable - If we must empty the global variable ACTIVE_LIZMAP_PRESENTATION
+ * @param {boolean} resetActiveInterfaceElements - If we must remove the "active" interface for the buttons
+ */
+ resetLizmapPresentation(destroyFeatures = true, removeMessage = true, resetGlobalVariable = true, resetActiveInterfaceElements = true) {
+
+ // Hide presentation slides container
+ const slidesContainer = document.getElementById('lizmap-presentation-slides-container');
+ if (!slidesContainer) {
+ return;
+ }
+
+ // Reactivate presentation dock
+ const notActivePresentationDock = document.querySelector('#mapmenu ul > li.presentation:not(.active) a');
+ if (notActivePresentationDock) {
+ notActivePresentationDock.click();
+ }
+
+ // Put back the interface as initial
+ slidesContainer.innerHTML = '';
+ slidesContainer.classList.remove('visible');
+ const dock = document.getElementById('dock');
+ dock.classList.remove('presentation-visible', 'presentation-half', 'presentation-full');
+ const oldMapDiv = document.getElementById('map');
+ const mapDiv = document.getElementById('newOlMap');
+ // THIS CANNOT BE DONE WITH CSS
+ if (mapDiv) {
+ mapDiv.style.marginLeft = this.mapLeftMargin;
+ mapDiv.style.width = '100%';
+ }
+ if (oldMapDiv) {
+ oldMapDiv.style.marginLeft = this.mapLeftMargin;
+ oldMapDiv.style.width = '100%';
+ }
+
+ // Remove the objects in the map
+ if (destroyFeatures) {
+ this.presentationLayer.getSource().clear();
+ }
+
+ // Clear the previous Lizmap message
+ if (removeMessage) {
+ let previousMessage = document.getElementById('lizmap-presentation-message');
+ if (previousMessage) previousMessage.remove();
+ }
+
+ // Remove all btn-primary classes in the target objects
+ if (resetActiveInterfaceElements) {
+ const selector = '.popup-presentation.btn-primary';
+ Array.from(document.querySelectorAll(selector)).map(element => {
+ element.classList.remove('btn-primary');
+ });
+ }
+
+ // Reset the global variable
+ if (resetGlobalVariable) {
+ this.ACTIVE_LIZMAP_PRESENTATION = null;
+ }
+ }
+
+ /**
+ * Toggle the lizMap presentation
+ *
+ */
+ toggleLizmapPresentation(show = false) {
+ const slidesContainer = document.getElementById('lizmap-presentation-slides-container');
+ const slidesRestore = document.getElementById('lizmap-presentation-slides-minified-toolbar');
+ const dock = document.getElementById('dock');
+ const oldMapDiv = document.getElementById('map');
+ const mapDiv = document.getElementById('newOlMap');
+ if (!show) {
+ // Remove visible class
+ slidesContainer.classList.remove('visible');
+ slidesRestore.classList.add('visible');
+ dock.classList.remove('presentation-visible');
+
+ // Put map back to original size
+ // THIS CANNOT BE DONE WITH CSS
+ if (mapDiv) {
+ mapDiv.style.marginLeft = this.mapLeftMargin;
+ mapDiv.style.width = '100%';
+ }
+ if (oldMapDiv) {
+ oldMapDiv.style.marginLeft = this.mapLeftMargin;
+ oldMapDiv.style.width = '100%';
+ }
+ } else {
+ // Add presentation visible classes
+ dock.classList.add('presentation-visible');
+ slidesContainer.classList.add('visible');
+ slidesRestore.classList.remove('visible');
+ // We need to run the setInterfaceFromPage method to set the page & map width
+ const page = document.querySelector(`lizmap-presentation-page[data-number="${this.LIZMAP_PRESENTATION_ACTIVE_PAGE_NUMBER}"]`);
+ if (page) {
+ this.setInterfaceFromPage(page);
+ }
+ }
+ }
+
+ /**
+ * Go to the given active presentation page
+ * @param {integer} pageNumber The page number to go to
+ */
+ goToGivenPage(pageNumber) {
+ // console.log(`from ${this.LIZMAP_PRESENTATION_ACTIVE_PAGE_NUMBER} to ${pageNumber}`);
+ const targetAnchor = document.querySelector(`a.lizmap-presentation-page-anchor[name="${pageNumber}"]`);
+ if (targetAnchor) {
+ targetAnchor.scrollIntoView();
+ }
+ }
+
+ // Go to the previous page
+ goToPreviousPage() {
+ const targetPage = parseInt(this.LIZMAP_PRESENTATION_ACTIVE_PAGE_NUMBER) - 1;
+ if (targetPage >= 1) {
+ this.goToGivenPage(targetPage);
+ }
+ }
+
+ // Go to the next page
+ goToNextPage() {
+ // Get current page
+ const targetPage = parseInt(this.LIZMAP_PRESENTATION_ACTIVE_PAGE_NUMBER) + 1;
+ if (targetPage <= this.activePresentationPagesCount) {
+ this.goToGivenPage(targetPage);
+ }
+ }
+
+ // Go to the first page
+ goToFirstPage() {
+ this.goToGivenPage(1);
+ }
+
+ // Go to the last page
+ goToLastPage() {
+ this.goToGivenPage(this.activePresentationPagesCount);
+ }
+
+
+ /**
+ * Add the geometry features
+ * to the OpenLayers layer in the map
+ *
+ * @param {object} data - The data to add in GeoJSON format
+ * @param {object|undefined} style - Optional OpenLayers style object
+ *
+ * @return {object} features The OpenLayers features converted from the data
+ */
+ addFeatures(data, style) {
+ // Change the layer style
+ if (style) {
+ this.presentationLayer.setStyle(style);
+ }
+
+ // Convert the GeoJSON data into OpenLayers features
+ const features = (new GeoJSON()).readFeatures(data, {
+ featureProjection: mainLizmap.projection
+ });
+
+ // Add them to the presentation layer
+ this.presentationLayer.getSource().addFeatures(features);
+
+ return features;
+ }
+
+ /**
+ * Display a lizMap message
+ *
+ * @param {string} message Message to display
+ * @param {string} type Type : error or info
+ * @param {number} duration Number of millisecond the message must be displayed
+ */
+ addMessage(message, type, duration) {
+
+ let previousMessage = document.getElementById('lizmap-presentation-message');
+ if (previousMessage) previousMessage.remove();
+ mainLizmap.lizmap3.addMessage(
+ message, type, true, duration
+ ).attr('id', 'lizmap-presentation-message');
+ }
+
+ /**
+ * Hide & empty a form.
+ *
+ */
+ hideForm() {
+ // Get sub-dock
+ const subDock = document.getElementById('sub-dock');
+ subDock.innerHTML = '';
+ subDock.style.maxWidth = '30%';
+ if (subDock.checkVisibility()) {
+ subDock.style.display = 'none';
+ }
+ }
+
+ /**
+ * Set Lizmap interface (dock & map) according to the given page
+ *
+ * This is mainly used to set the correct width & margins
+ *
+ * @param {HTMLElement} page Lizmap presentation page component
+ */
+ setInterfaceFromPage(page) {
+ // TODO page width must be set from page data
+ const pageWidthClass = 'presentation-half';
+ const dockWidth = (pageWidthClass == 'presentation-half') ? '50%' : '100%';
+ const dock = document.getElementById('dock');
+ const oldMapDiv = document.getElementById('map');
+ const mapDiv = document.getElementById('newOlMap');
+
+ // Set interface classes
+ dock.classList.remove('presentation-half', 'presentation-full');
+ dock.classList.add(pageWidthClass);
+ // THIS CANNOT BE DONE WITH CSS
+ if (mapDiv) {
+ mapDiv.style.marginLeft = (pageWidthClass == 'presentation-half') ? '50%' : this.mapLeftMargin;
+ mapDiv.style.width = dockWidth;
+ }
+ if (oldMapDiv) {
+ oldMapDiv.style.marginLeft = (pageWidthClass == 'presentation-half') ? '50%' : this.mapLeftMargin;
+ oldMapDiv.style.width = dockWidth;
+ }
+ }
+
+ /**
+ * Set Lizmap interface view depending on active page
+ */
+ onPageVisible(page, isFirst = false) {
+ // Manage global uuid property
+ const uuid = page.dataset.uuid;
+ let newPageVisible = null;
+
+ // Set the global object page UUID if not set yet
+ if (this.LIZMAP_PRESENTATION_ACTIVE_PAGE_UUID === null) {
+ this.LIZMAP_PRESENTATION_ACTIVE_PAGE_UUID = uuid;
+ newPageVisible = uuid;
+ }
+
+ // Check if the active UUID is different from the given page uuid
+ if (uuid != this.LIZMAP_PRESENTATION_ACTIVE_PAGE_UUID) {
+ this.LIZMAP_PRESENTATION_ACTIVE_PAGE_UUID = uuid;
+ newPageVisible = uuid;
+ }
+
+ // Set the active page number
+ this.LIZMAP_PRESENTATION_ACTIVE_PAGE_NUMBER = page.dataset.number;
+
+ // Store when the page has valid map properties (extent or tree state)
+ let hasMapProperties = false;
+
+ // Change Lizmap state only if active page has changed
+ if (newPageVisible !== null || isFirst) {
+ // Change Lizmap interface depending of the current page map properties
+ this.setInterfaceFromPage(page);
+
+ // Set layer tree state if needed
+ const treeStateString = page.properties.tree_state;
+ if (treeStateString !== null) {
+ try {
+ const treeState = JSON.parse(treeStateString);
+ if ('groups' in treeState && 'layers' in treeState) {
+ // Groups
+ const groups = lizMap.mainLizmap.state.layersAndGroupsCollection.groups;
+ if (treeState.groups.length > 0) {
+ for (const group of groups) {
+ group.checked = (treeState.groups.includes(group.name));
+ }
+ }
+
+ // Then layers
+ const layers = lizMap.mainLizmap.state.layersAndGroupsCollection.layers;
+ if (treeState.layers.length > 0) {
+ for (const layer of layers) {
+ layer.checked = (treeState.layers.includes(layer.name));
+ }
+ }
+
+ hasMapProperties = true;
+ }
+ } catch(error) {
+ console.log(error);
+ console.log(`Wrong tree state for the active presentation page ${uuid}`);
+ }
+ }
+
+ // Set Map extent if needed
+ // Use OpenLayers animation
+ const mapExtent = page.properties.map_extent;
+ if (mapExtent !== null) {
+ // Set map extent
+ const newExtent = mapExtent.split(',');
+ if (newExtent.length == 4) {
+ hasMapProperties = true;
+ // lizMap.mainLizmap.extent = newExtent;
+ const view = lizMap.mainLizmap.map.getView();
+ // Animate the view to the extent
+ view.fit(
+ newExtent, {
+ duration: 500
+ }
+ );
+ }
+ }
+ }
+ }
+
+
+ /**
+ * Display the form to create a new presentation or a new page
+ *
+ * @param {string} itemType Type of item to edit: presentation or page.
+ * @param {null|number} id Id of the presentation or page. If null, it is a creation form.
+ * @param {null|number} presentation_id Id of the parent presentation. Only for creation of page
+ */
+ async launchPresentationCreationForm(itemType = 'presentation', id = null, presentation_id = null) {
+ // Get the form
+ try {
+ const url = presentationConfig.url;
+ const request = (id === null) ? 'create' : 'modify';
+ let formData = new FormData();
+ formData.append('request', request);
+ formData.append('id', id);
+ formData.append('item_type', itemType);
+ if (itemType == 'page' && request == 'create' && presentation_id) {
+ formData.append('presentation_id', presentation_id);
+ }
+ const response = await fetch(url, {
+ method: "POST",
+ body: formData
+ });
+
+ // Check content type
+ const contentType = response.headers.get("content-type");
+ if (!contentType || !contentType.includes("text/plain")) {
+ throw new TypeError("Wrong content-type. HTML Expected !");
+ }
+
+ // Get the response
+ const htmlContent = await response.text();
+
+ // Display it
+ // Get sub-dock
+ const subDock = document.getElementById('sub-dock');
+ subDock.style.maxWidth = '50%';
+ const html = `
+
${htmlContent}
+ `;
+ // Using innerHtml or insertAdjacentHTML prevents from running jForms embedded