UNPKG

geopf-extensions-openlayers

Version:

French Geoportal Extensions for OpenLayers libraries

1,192 lines (1,100 loc) 87.4 kB
// import CSS import "../../CSS/Controls/Catalog/GPFcatalog.css"; // import OpenLayers import Widget from "../Widget"; import Control from "../Control"; import Map from "ol/Map"; // import local import Utils from "../../Utils/Helper"; import SelectorID from "../../Utils/SelectorID"; import Logger from "../../Utils/LoggerByDefault"; import Draggable from "../../Utils/Draggable"; import Config from "../../Utils/Config"; import LayerConfig from "../../Utils/LayerConfigUtils"; // import local des layers import GeoportalWFS from "../../Layers/LayerWFS"; import GeoportalWMS from "../../Layers/LayerWMS"; import GeoportalWMTS from "../../Layers/LayerWMTS"; import GeoportalMapBox from "../../Layers/LayerMapBox"; // DOM import CatalogDOM from "./CatalogDOM"; // Gestion des topics en local : themes et services et producteurs import Topics from "./topics.json"; // import externe import { marked as Marked } from "marked"; import Clusterize from "clusterize.js"; const Test = Clusterize.default; var logger = Logger.getLogger("widget"); /** * @typedef {Object} CatalogOptions - Liste des options du widget Catalog * @property {boolean} [collapsed=true] - Définit si le widget est replié au chargement. * @property {boolean} [draggable=false] - Permet de déplacer le panneau du catalogue. * @property {boolean} [auto=true] - Active l’ajout automatique des événements sur la carte. * @property {string} [titlePrimary="Gérer vos couches de données"] - Titre principal du panneau. * @property {string} [titleSecondary=""] - Titre secondaire du panneau. * @property {string} [layerLabel="title"] - Propriété utilisée comme label pour les couches. * @property {Boolean} [layerThumbnail=false] - Affiche les miniatures des couches si disponibles. * @property {string} [size="md"] - Taille de la fenêtre : sm, md, lg ou xl. * @property {Boolean} [tabHeightAuto=false] - Gestion dynamique ou fixe de la taille des onglets en fonction du contenu. * @property {Object} [search] - Options de recherche. * @property {boolean} [search.display=true] - Affiche le champ de recherche. * @property {string} [search.label="Rechercher une donnée"] - Label du champ de recherche. * @property {Array<string>} [search.criteria=["name","title","description"]] - Critères de recherche. * @property {boolean} [addToMap=true] - Ajoute automatiquement la couche à la carte lors de la sélection. * @property {Array<Categories>} [categories] - Liste des catégories et sous-catégories. * @property {Object} [configuration] - Configuration des sources de données. * @property {string} [configuration.type="json"] - Type de configuration ("json" ou "service"). * @property {Array<string>} [configuration.urls] - URLs des fichiers de configuration JSON. * @property {Object} [configuration.data] - Données de configuration déjà chargées. * @property {string} [id] - Identifiant unique du widget. * @property {string} [position] - Position CSS du widget sur la carte. * @property {boolean} [gutter] - Ajoute ou retire l’espace autour du panneau. * @property {string} [optimisation="none"] - Type d'optimisation pour l'affichage des listes de couches : "none", "clusterize" (experimental) ou "on-demand". */ /** * @typedef {Object} Categories - Catégories principales du catalogue sous forme d'onglets * @property {string} title - Titre de la catégorie. * @property {string} id - Identifiant unique de la catégorie. * @property {boolean} default - Indique si c'est la catégorie par défaut. * @property {boolean} [cluster=false] - **Experimental** Clusterisation de la liste des couches. * @property {Object|null} clusterOptions - Options de la librairie Clusterize. * @property {boolean} [search=false] - Affiche une barre de recherche spécifique à la catégorie. * @property {Array<SubCategories>} [items] - Liste des sous-catégories. * @property {Object|null} filter - Filtre appliqué à la catégorie. * @property {string} filter.field - Champ utilisé pour le filtre. * @property {string|Array<string>} filter.value - Valeur ou liste de valeurs pour le filtre. */ /** * @typedef {Object} SubCategories - Sous-catégories du catalogue sous forme de boutons radio * avec ou sans sections. Une section, c'est un regroupement thématique des couches. * ex. : regrouper les couches par "thématique" (voir propriété "thematic" dans la conf. des couches) * @property {string} title - Titre de la sous-catégorie. * @property {string} id - Identifiant unique de la sous-catégorie. * @property {boolean} section - Indique si la sous-catégorie utilise des sections. * @property {boolean} [collapsible] - **TODO** Indique si les sections sont repliables. * @property {boolean} [icon] - Indique que l'on souhaite un icone de type dsfr classe pour les sections de la sous-catégorie. * @property {Array<Object>} [iconJson] - Liste d'icones (json) pour les sections de la sous-catégorie. * @property {Array<string>} sections - Liste des sections (remplie ultérieurement). * @property {boolean} default - Indique si c'est la sous-catégorie par défaut. * @property {boolean} [cluster=false] - **Experimental** Clusterisation de la liste des couches. * @property {Object|null} clusterOptions - Options de la librairie Clusterize. * @property {Object|null} filter - Filtre appliqué à la sous-catégorie. * @property {string} filter.field - Champ utilisé pour le filtre. * @property {string|Array<string>} filter.value - Valeur ou liste de valeurs pour le filtre. */ /** * @typedef {Object} Config - Configuration des sources de données * * {@link schema | https://raw.githubusercontent.com/IGNF/geoportal-configuration/new-url/doc/schema.json} * * {@link jsdoc | https://raw.githubusercontent.com/IGNF/geoportal-configuration/new-url/doc/schema.jsdoc} */ /** * @typedef {Object} ConfigLayer - Configuration d'une couche * * {@link schema | https://raw.githubusercontent.com/IGNF/geoportal-configuration/new-url/doc/schema.json} * * {@link jsdoc | https://raw.githubusercontent.com/IGNF/geoportal-configuration/new-url/doc/schema.jsdoc} * @description * Un objet de type ConfigLayer est un objet qui contient la configuration d'une couche. * Il est issu de la configuration globale (Config) et enrichi de propriétés supplémentaires * pour le bon fonctionnement du catalogue. * ex. : service, categories, producer_urls, thematic_urls, etc. * Les types de services supportés sont : WMTS, WMS, WFS, TMS. */ /** * @classdesc * * Catalog Data * * @alias ol.control.Catalog * @module Catalog */ class Catalog extends Control { /** * @constructor * @param {CatalogOptions} options - options for function call. * * @fires catalog:loaded * @fires catalog:layer:add * @fires catalog:layer:remove * * {@link schema | https://raw.githubusercontent.com/IGNF/geoportal-configuration/new-url/doc/schema.json} * * {@link jsdoc | https://raw.githubusercontent.com/IGNF/geoportal-configuration/new-url/doc/schema.jsdoc} * @example * var widget = new ol.control.Catalog({ * collapsed : true, * draggable : false, * titlePrimary : "", * titleSecondary : "Gérer vos couches de données", * layerLabel : "title", * layerThumbnail : true, * search : { * display : true, * label : "Rechercher une donnée", * criteria : [ * "name", * "title", * "description" * ] * }, * addToMap : true, * categories : [ * { * title : "Données", * id : "data", * default : true, * search : false, * filter : null * // sous categories * // items : [ * // { * // title : "", * // default : true, * // section : true, // avec section (ex. regroupement par themes) * // icon : true, // icone pour les sections (svg ou lien http ou dsfr classe) * // filter : { * // field : "", * // value : "" * // } * // } * // ] * } * ], * configuration : { * type : "json", * urls : [ // data:{} * "https://raw.githubusercontent.com/IGNF/cartes.gouv.fr-entree-carto/main/public/data/layers.json", * "https://raw.githubusercontent.com/IGNF/cartes.gouv.fr-entree-carto/main/public/data/edito.json" * ] * } * }); * widget.on("catalog:loaded", (e) => { console.log(e.data); }); * widget.on("catalog:layer:add", (e) => { console.log(e); }); * widget.on("catalog:layer:remove", (e) => { console.log(e); }); * map.addControl(widget); * * @todo validation du schema */ constructor (options) { options = options || {}; // call ol.control.Control constructor super(options); if (!(this instanceof Catalog)) { throw new TypeError("ERROR CLASS_CONSTRUCTOR"); } /** * Nom de la classe (heritage) * @private */ this.CLASSNAME = "Catalog"; // initialisation du composant this.initialize(options); // Widget main DOM container this.container = this.initContainer(); // ajout du container (this.element) ? this.element.appendChild(this.container) : this.element = this.container; // INFO // le DOM est mis en place sans la liste des couches du catalogue // car l'opération peut être async si un download est demandé. // une patience permet d'attendre que la liste soit récupérée. this.showWaiting(); this.initConfigData() .then((data) => { logger.trace(this, data); this.hideWaiting(); // sauvegarde de la configuration this.configData = data; /** * event triggered when data is loaded */ this.dispatchEvent({ type : this.LOADED_CATALOG_EVENT, data : data }); }) .catch((e) => { this.hideWaiting(); // TODO gestion des erreurs logger.error(e); }); return this; } /** * Overwrite OpenLayers setMap method * This method sets the map for the Catalog control. * It initializes event listeners for the map and sets up the control's draggable and collapsed states. * It also checks for existing layers on the map and updates the control accordingly. * * @param {Map} map - Map instance to set for the control. */ setMap (map) { if (map) { // INFO // on verifie les couches déjà présentes sur la cartes this.on("catalog:loaded", this.checkLayersOnMap); // mode "draggable" if (this.draggable) { Draggable.dragElement( this.panelCatalogContainer, this.panelCatalogHeaderContainer, map.getTargetElement() ); } // mode "collapsed" if (!this.collapsed) { this.buttonCatalogShow.setAttribute("aria-pressed", true); } // ajout des evenements sur la carte if (this.auto) { this.addEventsListeners(map); } } else { this.un("catalog:loaded", this.checkLayersOnMap); // suppression des evenements sur la carte // pour les futurs suppressions de couche if (this.auto) { this.removeEventsListeners(); } } // on appelle la méthode setMap originale d'OpenLayers super.setMap(map); // position if (this.options.position) { this.setPosition(this.options.position); } // reunion du bouton avec le précédent if (this.options.gutter === false) { this.getContainer().classList.add("gpf-button-no-gutter"); } } /** * Returns true if widget is collapsed (minimized), false otherwise * * @returns {Boolean} collapsed - true if widget is collapsed */ getCollapsed () { return this.collapsed; } /** * Collapse or display widget main container * * @param {Boolean} collapsed - True to collapse widget, False to display it */ setCollapsed (collapsed) { if (collapsed === undefined) { logger.error("[ERROR] Catalog:setCollapsed - missing collapsed parameter"); return; } if ((collapsed && this.collapsed) || (!collapsed && !this.collapsed)) { return; } if (collapsed) { this.buttonCatalogClose.click(); } else { this.buttonCatalogShow.click(); } this.collapsed = collapsed; } // ################################################################### // // ##################### public methods ############################## // // ################################################################### // /** * Add a layer config * This method processes a configuration object containing layer definitions. * * @param {Config} conf conf */ addLayerConfig (conf) { for (const key in conf) { if (Object.prototype.hasOwnProperty.call(conf, key)) { const layer = conf[key]; if (layer.serviceParams) { // si la couche a bien une configuration valide liée au service var service = layer.serviceParams.id.split(":").slice(-1)[0]; // beurk! layer.service = service; // new proprerty ! layer.categories = []; // new property ! vide pour le moment layer.producer_urls = this.createCatalogLinks("producer", layer.producer); // plus d'info layer.thematic_urls = this.createCatalogLinks("thematic", layer.thematic); // plus d'info this.layersList[key] = layer; } } } // clean container var element = document.getElementById("GPcatalogContainerTabs"); if (element) { element.remove(); } // on reordonne la liste this.layersList.sort((a, b) => a.title.localeCompare(b.title, "fr", { sensitivity : "base" })); // on va recréer le container this.createCatalogContentEntries(this.layersList); } /** * Activate a layer by its ID * This method activates a layer based on its ID, which is expected to be in the format "name$service". * It splits the ID to extract the layer name and service, then calls the `activeLayer` method. * * @param {*} id - Layer ID in the format "name$service". * @example * activeLayerByID("GEOGRAPHICALGRIDSYSTEMS.PLANIGNV2$WMTS"); * activeLayerByID("PLAN.IGN$GEOPORTAIL:TMS"); */ activeLayerByID (id) { var name = id.split("$")[0]; var service = id.split(":").slice(-1)[0]; this.activeLayer(name, service); } /** * Disable a layer by its ID * This method disables a layer based on its ID, which is expected to be in the format "name$service". * It splits the ID to extract the layer name and service, then calls the `disableLayer` method. * @param {*} id - Layer ID in the format "name$service". * @example * disableLayerByID("GEOGRAPHICALGRIDSYSTEMS.PLANIGNV2$WMTS"); * disableLayerByID("PLAN.IGN$GEOPORTAIL:TMS"); * @todo * - ajouter un test pour vérifier si l'ID est valide * - ajouter un test pour vérifier si la couche est déjà active * - ajouter un test pour vérifier si la couche est déjà désactivée */ disableLayerByID (id) { var name = id.split("$")[0]; var service = id.split(":").slice(-1)[0]; this.disableLayer(name, service); } /** * Activate a layer * This method activates a layer by its name and service. * It checks if the layer exists in the `layersList` and if it does, it adds the layer to the map if `addToMap` is true. * It then dispatches an event indicating that the layer has been added to the catalog. * @param {String} name - Layer name. * @param {String} service - Layer service. * @example * activeLayer("GEOGRAPHICALGRIDSYSTEMS.PLANIGNV2", "WMTS"); * activeLayer("PLAN.IGN", "GEOPORTAIL:TMS"); */ activeLayer (name, service) { // cf. this.onSelectCatalogEntryClick var id = this.getLayerId(name, service); if (id) { var layer = {}; // conf tech if (this.options.addToMap) { layer = this.addLayer(name, service); } this.dispatchEvent({ type : this.ADD_CATALOG_LAYER_EVENT, name : name, service : service, layer : layer }); } } /** * Disable a layer * This method disables a layer by its name and service. * It checks if the layer exists in the `layersList` and if it does, it removes the layer from the map if `addToMap` is true. * It then dispatches an event indicating that the layer has been removed from the catalog. * @param {String} name - Layer name. * @param {String} service - Layer service. * @example * disableLayer("GEOGRAPHICALGRIDSYSTEMS.PLANIGNV2", "WMTS"); * disableLayer("PLAN.IGN", "GEOPORTAIL:TMS"); */ disableLayer (name, service) { var id = this.getLayerId(name, service); if (id) { var layer = {}; // conf tech if (this.options.addToMap) { layer = this.removeLayer(name, service); } this.dispatchEvent({ type : this.REMOVE_CATALOG_LAYER_EVENT, name : name, service : service, layer : layer }); } } /** * Get long layer ID * * @param {*} name - nom de la couche * @param {*} service - service de la couche * @return {String|null} - long layer ID or null if not found * @description * This method retrieves the long layer ID based on the provided name and service. * It searches through the `layersList` object for a key that matches the pattern of "name.*service". * If a match is found, it returns the key; otherwise, it returns null. * @example * getLayerId("GEOGRAPHICALGRIDSYSTEMS.PLANIGNV2", "WMTS"); * getLayerId("PLAN.IGN", "GEOPORTAIL:TMS"); */ getLayerId (name, service) { if (!this.layersList || typeof this.layersList !== "object") { return null; } var regex = new RegExp(name + ".*" + service); for (const key in this.layersList) { if (Object.prototype.hasOwnProperty.call(this.layersList, key)) { if (regex.test(key)) { return key; } } } return null; } /** * Get layers by category * This method filters the layers based on the provided category. * It checks if the category has a filter defined and applies it to the layers. * If the filter matches, the layer is added to the `layersCategorised` object. * It also updates the `categories` property of each layer to include the category ID. * * @param {*} category - Category object containing the filter. * @param {*} layers - Object containing all layers. * @return {Object} - Filtered layers categorized by the provided category. */ getLayersByCategory (category, layers) { // INFO // comment gerer les listes de layers filtrées pour chaque categorie ? // on doit les stocker si l'on souhaite faire des requêtes // avec l'outil de recherche par la suite var layersCategorised = layers; var filter = category.filter; if (filter) { layersCategorised = {}; for (const key in layers) { if (Object.prototype.hasOwnProperty.call(layers, key)) { const layer = layers[key]; if (layer[filter.field]) { // FIXME impl. clef multiple : property.property ! var condition = Array.isArray(filter.value) ? filter.value.includes(layer[filter.field].toString()) : (filter.value === "*" || layer[filter.field].toString() === filter.value); if (condition) { layersCategorised[key] = layer; // on ajoute l'appartenance de la couche à une categorie this.layersList[key].categories.push(category.id); } } } } } return layersCategorised; } // ################################################################### // // ################### getters / setters ############################# // // ################################################################### // /** * Get container * * @returns {HTMLElement} container */ getContainer () { return this.container; } // ################################################################### // // #################### privates methods ############################# // // ################################################################### // /** * Initialize Catalog control (called by Catalog constructor) * * @param {Object} options - constructor options * @private */ initialize (options) { /** @private */ this.uid = options.id || SelectorID.generate(); // set default options this.clusterOptions = { rows_in_block : 50, blocks_in_cluster : 4 }; this.options = { collapsed : true, draggable : false, auto : true, titlePrimary : "Gérer vos couches de données", titleSecondary : "", layerLabel : "title", layerThumbnail : false, tabHeightAuto : false, optimisation : "none", // none | clusterize | on-demand size : "md", search : { display : true, // barre de recherche globale label : "Rechercher une donnée", criteria : [ "name", "title", "description" ] }, addToMap : true, categories : [ { // INFO // categories : sous forme d'un onglet par categorie title : "Données", id : "data", cluster : true, clusterOptions : this.clusterOptions, default : true, filter : null // INFO // subcategories : sous forme d'un bouton radio par sous categoris // items : [ // { // title : "", // default : true, // cluster : false, // section : true, // avec section (ex. regroupement par themes) // icon : true, // icone pour les sections (svg ou lien http ou dsfr classe) // filter : { // field : "thematic", // value : "" // } // } // { // title : "Toutes les données", // default : false, // section : false, // sans section // cluster : false, // filter : null // sans filtre, on prend toutes les données // } // ] } ], configuration : { type : "json", // TODO type:"service" urls : [ // data:{} // ex. // "https://raw.githubusercontent.com/IGNF/cartes.gouv.fr-entree-carto/main/public/data/layers.json", // "https://raw.githubusercontent.com/IGNF/cartes.gouv.fr-entree-carto/main/public/data/edito.json", "https://raw.githubusercontent.com/IGNF/geoportal-configuration/new-url/dist/entreeCarto.json" ] } }; // merge with user optionssearch var searchOptions = Utils.assign(this.options.search, options.search); Utils.assign(this.options, options); Utils.assign(this.options.search, searchOptions); /** * specify if control is collapsed (true) or not (false) * @type {Boolean} */ this.collapsed = this.options.collapsed; /** * specify if control is draggable (true) or not (false) * @type {Boolean} */ this.draggable = this.options.draggable; /** * specify if control add some stuff auto * @type {Boolean} */ this.auto = this.options.auto; /** * specify some events listeners * @type {Array} */ this.eventsListeners = []; // DOM /** @private */ this.buttonCatalogShow = null; /** @private */ this.panelCatalogContainer = null; /** @private */ this.panelCatalogHeaderContainer = null; // usefull for the dragNdrop /** @private */ this.buttonCatalogClose = null; /** @private */ this.contentCatalogContainer = null; /** @private */ this.waitingContainer = null; /** * specify configuration data (configuration service) */ this.configData = {}; /** * specify all list of layers (configuration service) * @type {Array<Object>} */ this.layersList = {}; /** * specify clusterize instances for each category/subcategory/section * @type {Object} * @example * { * "data" : Clusterize, // category id + instance * "454587412" : Clusterize, // subcategory id + instance * "457121205" : Clusterize, // subcategory id + instance * } */ this.clusterizeRef = {}; /** * specify clusterize sections for each category/subcategory * @type {Object} * @example * { * 548487533 : { // subcategory id * "section-accordion-548487533-72432" : [rows], // section id + rows * "section-accordion-548487533-78155" : [rows] // section id + rows * } * } */ this.clusterizeSections = {}; /** * specify data on demand instances for each category/subcategory/section * @type {Object} * @example * { * base : fragmentDocument, // category id + fragmentDocument * 457121205 : fragmentDocument, // subcategory id + fragmentDocument * 548487533 : { * "section-accordion-548487533-72432" : fragmentDocument, // section id + fragmentDocument * "section-accordion-548487533-78155" : fragmentDocument // section id + fragmentDocument * } */ this.dataOnDemand = {}; /** * specify all categories * @type {Array<Categories} * @example * [ * { * title : "Données", // title of the category * id : "data", // id of the category * default : true, // if true, this category is selected by default * search : false, // if true, a search bar is displayed for this category * cluster : false, // if true, clustering is activated for this category * filter : null, // filter to apply on the category * items : [ // list of subcategories * { * title : "Toutes les données", // title of the subcategory * id : "all", // id of the subcategory * default : true, // if true, this subcategory is selected by default * icon : true, // icon for the subcategory (svg or http link or dsfr class) * iconJson : [], // list of icons (json) for the sections * cluster : false, // if true, clustering is activated for this subcategory * section : false, // if true, this subcategory has a section * sections : [], // list of sections (filled later) * filter : null, // filter to apply on the subcategory * } * ] * } * ] */ this.categories = this.options.categories.map((cat) => { // INFO // on reecrit correctement les categories // ex. properties mal renseignées tels que id ou default var items = cat.items; if (cat.items) { items = cat.items.map((i) => { return { title : i.title, id : i.id || this.generateID(i.title), default : i.hasOwnProperty("default") ? i.default : false, section : i.hasOwnProperty("section") ? i.section : false, sections : [], // liste des valeurs des sections remplie ulterieurement ! subcategory : true, // new property ! icon : i.hasOwnProperty("icon") ? i.icon : false, iconJson : i.iconJson || [], // liste des icones (json) pour les sections cluster : i.hasOwnProperty("cluster") ? i.cluster : false, clusterOptions : i.hasOwnProperty("clusterOptions") ? i.clusterOptions : this.clusterOptions, filter : i.filter || null, }; }); } return { title : cat.title, id : cat.id || this.generateID(cat.title), default : cat.hasOwnProperty("default") ? cat.default : false, search : cat.hasOwnProperty("search") ? cat.search : false, cluster : cat.hasOwnProperty("cluster") ? cat.cluster : false, clusterOptions : cat.hasOwnProperty("clusterOptions") ? cat.clusterOptions : this.clusterOptions, filter : cat.filter || null, items : items || null }; }); /** * specify the current category selected * @type {String} */ this.categoryId = (() => { // INFO // par défaut, la categorie affichée sera la 1ere // sauf si on a specifié une categorie avec l'attribut 'default:true' var index = this.categories.findIndex((category) => category.default); if (index === -1) { index = 0; this.categories[index].default = true; } return this.categories[index].id; })(); /** * list of layers added on map by key pair : name/service * @type {Object} * @example * { * "GEOGRAPHICALGRIDSYSTEMS.PLANIGNV2:WMTS" : ol/layer/Tile, * "PLAN.IGN$GEOPORTAIL:TMS" : ol/layer/VectorTile * } */ this.layersListOnMap = {}; /** @private */ this._searchTimeout = null; // timeout for search input /** * event triggered when layer is added * * @event catalog:layer:add * @defaultValue "catalog:layer:add" * @group Events * @property {Object} type - event * @property {String} name - layer name * @property {String} service - service name * @property {Object} layer - layer conf * @property {Object} target - instance Catalog * @example * Catalog.on("catalog:layer:add", function (e) { * console.log(e.layer); * }) */ this.ADD_CATALOG_LAYER_EVENT = "catalog:layer:add"; /** * event triggered when layer is removed * * @event catalog:layer:remove * @defaultValue "catalog:layer:remove" * @group Events * @property {Object} type - event * @property {String} name - layer name * @property {String} service - service name * @property {Object} layer - layer conf * @property {Object} target - instance Catalog * @example * Catalog.on("catalog:layer:remove", function (e) { * console.log(e.layer); * }) */ this.REMOVE_CATALOG_LAYER_EVENT = "catalog:layer:remove"; /** * event triggered when data is loaded * * @event catalog:loaded * @defaultValue "catalog:loaded" * @group Events * @property {Object} type - event * @property {Object} data - data * @property {Object} target - instance Catalog * @example * Catalog.on("catalog:loaded", function (e) { * console.log(e.data); * }) */ this.LOADED_CATALOG_EVENT = "catalog:loaded"; } /** * Create control main container (DOM initialize) * * @returns {HTMLElement} DOM element * @private */ initContainer () { // create main container var container = this._createMainContainerElement(); var picto = this.buttonCatalogShow = this._createShowCatalogPictoElement(); container.appendChild(picto); // panel var widgetPanel = this.panelCatalogContainer = this._createCatalogPanelElement(); var widgetPanelSize = this._createCatalogPanelDivSizeElement(this.options.size); widgetPanel.appendChild(widgetPanelSize); var widgetPanelDiv = this._createCatalogPanelDivElement(); widgetPanelSize.appendChild(widgetPanelDiv); // header var widgetPanelHeader = this.panelCatalogHeaderContainer = this._createCatalogPanelHeaderElement(); // icone var widgetPanelIcon = this._createCatalogPanelIconElement(this.options.titlePrimary); widgetPanelHeader.appendChild(widgetPanelIcon); // title var widgetPanelTitle = this._createCatalogPanelTitleElement(this.options.titlePrimary); widgetPanelHeader.appendChild(widgetPanelTitle); // close picto var widgetCloseBtn = this.buttonCatalogClose = this._createCatalogPanelCloseElement(); widgetPanelHeader.appendChild(widgetCloseBtn); widgetPanelDiv.appendChild(widgetPanelHeader); var widgetContentDiv = this._createCatalogPanelContentDivElement(); // container for the custom dynamic code (cf. initConfigData()) var widgetContentElementDiv = this.contentCatalogContainer = this._createCatalogContentDivElement(); widgetContentElementDiv.appendChild(this._createCatalogContentTitleElement(this.options.titleSecondary)); // search bar (global) if (this.options.search.display) { widgetContentElementDiv.appendChild(this._createCatalogContentSearchGlobalElement(this.options.search.label)); } // waiting var waiting = this.waitingContainer = this._createCatalogWaitingElement(); widgetContentElementDiv.appendChild(waiting); widgetContentDiv.appendChild(widgetContentElementDiv); widgetPanelDiv.appendChild(widgetContentDiv); container.appendChild(widgetPanel); return container; } /** * Check layers already present on the map * This method checks the layers already present on the map * and marks them as checked in the catalog. * @private */ checkLayersOnMap () { var map = this.getMap(); if (!map) { return; } var layers = map.getLayers(); layers.forEach((layer) => { if (layer.name && layer.service) { // sauvegarde this.layersListOnMap[layer.name + ":" + layer.service] = layer; // cocher la case dans le catalogue var inputs = document.querySelectorAll(`input[data-layer="${layer.name}:${layer.service}"]`); if (inputs) { inputs.forEach((input) => { input.checked = true; }); } } }); } /** * Initialize layers list and other properties * This method initializes the layers list from the configuration data. * It can load data from a local object or fetch it from URLs. * It processes the layers to add additional properties such as `service`, `categories`, and URLs for producers and thematics. * It also creates the catalog content entries based on the layers. * * @returns {Promise} - promise * @private */ async initConfigData () { var data = null; // reponse brute du service if (this.options.configuration.data) { data = this.options.configuration.data || {}; if (Config.isConfigLoaded()) { Utils.mergeParams(data, Config.configuration); } // contrôle des couches this.checkConfigLayers(data.layers); // sauvegarde des couches de données this.layersList = data.layers; this.createCatalogContentEntries(data); return new Promise((resolve, reject) => { resolve(data); }); } if (this.options.configuration.urls) { var fetchUrls = []; for (let i = 0; i < this.options.configuration.urls.length; i++) { const url = this.options.configuration.urls[i]; const fetchUrl = function () { return fetch(url, {}) .then(function (response) { if (response.ok) { return response.json() .then(function (json) { return json; }) .catch(error => { logger.warn("fetch json exception :", error); }); } else { var err = new Error("HTTP status code: " + response.status); throw err; } }) .catch(error => { return new Promise((resolve, reject) => { logger.error("fetch json exception :", error); reject(error); }); }); }; fetchUrls.push(fetchUrl()); } try { const values = await Promise.all(fetchUrls); data = values[0]; for (let i = 1; i < values.length; i++) { const value = values[i]; Utils.mergeParams(data, value); } if (Config.isConfigLoaded()) { Utils.mergeParams(data, Config.configuration); } // contrôle des couches this.checkConfigLayers(data.layers); // sauvegarde de la liste des couches this.layersList = data.layers; this.createCatalogContentEntries(data); return await new Promise((resolve, reject) => { resolve(data); }); } catch (e) { return await new Promise((resolve, reject) => { reject(e); }); } } } /** * Check configuration layers * This method checks the configuration of layers to ensure they have valid service parameters. * It also adds additional properties to each layer, such as `service`, `categories`, and URLs for producers and thematics. * It cleans the list of layers by removing those without valid configuration and adds a default thumbnail if enabled and not present. * * @param {Array<ConfigLayer>} layers - list of layers * @private */ checkConfigLayers (layers) { // INFO // on en profite pour ajouter des properties : // - service : utile pour identifier la couche // de manière unique : name + service // - categories : utile pour definir l'appartenance d'une couche // à une ou plusieurs categories // on en profite aussi pour nettoyer la liste // des couches qui n'ont pas de configuration valide // cf. serviceParams obligatoire // on en profite aussi pour ajouter une vignette par défaut // si la couche n'en a pas et que l'option est activée for (const key in layers) { if (Object.prototype.hasOwnProperty.call(layers, key)) { const layer = layers[key]; if (layer.serviceParams) { // TEST const isHTML = (str) => { const doc = document.createElement("div"); doc.innerHTML = str.trim(); // Si le premier enfant est un élément HTML, c'est du HTML return doc.childNodes.length > 0 && doc.firstChild.nodeType === 1; }; if (isHTML(layer.description)) { logger.error(`layer description contains HTML code, which is not allowed. Layer: ${key}`); logger.error("Please use Markdown syntax for layer descriptions instead."); logger.error(layer.description); delete layers[key]; continue; } // si la couche a bien une configuration valide liée au service var service = layer.serviceParams.id.split(":").slice(-1)[0]; // beurk! layer.service = service; // new proprerty ! layer.categories = []; // new property ! vide pour le moment layer.producer_urls = this.createCatalogProducerLinks(layer.producer); // plus d'info layer.thematic_urls = this.createCatalogThematicLinks(layer.thematic); // plus d'info // label de la couche layer.label = (this.options.layerLabel) ? (layer[this.options.layerLabel] || layer.title) : layer.title; // INFO // On transforme le markdown en HTML // et on nettoie le HTML pour éviter les injections XSS // cf. https://marked.js.org/ // Le markdown ne doit pas être échappé pour realiser une transformation ! layer.description = Marked.parse(layer.description); // les vignettes ! if (this.options.layerThumbnail) { // si on souhaite afficher une vignette // et que la couche n'en a pas // on met une vignette par défaut if (!layer.thumbnail) { layer.thumbnail = "default"; } } else { // sinon pas de vignette if (layer.thumbnail) { // FIXME // suppression !? delete layer.thumbnail; } } } else { // sinon on supprime l'entrée car pas de configuration valide delete layers[key]; } } } } /** * Create DOM content categories and entries * @param {Config} data - data * @private */ createCatalogContentEntries (data) { var container = this.contentCatalogContainer; var widgetContentEntryTabs = this._createCatalogContentCategoriesTabs(this.categories, this.options.tabHeightAuto); container.appendChild(widgetContentEntryTabs); // INFO // Remise à plat des catégories / sous-categories sur le même niveau // pour simplifier la gestion des couches // et la création des onglets de contenu // on a autant de catégories / sous-catégories que de containers // dans le DOM, on ne peut pas faire autrement // on va donc créer un tableau de catégories / sous-catégories // qui contiendra toutes les couches // et on va créer le contenu de chaque catégorie / sous-catégorie // dans le DOM, dans l'ordre des catégories / sous-catégories var categories = []; this.categories.forEach((category) => { if (category.items) { for (let i = 0; i < category.items.length; i++) { const element = category.items[i]; // INFO // on recherche la liste des icones pour les sections // si l'élément est une section et qu'il n'a pas d'icones // on va chercher les icones dans les données // en fonction du filtre de la section // ex. filter.field = "thematic" // on va chercher toutes les valeurs de "thematic" // dans les couches if (element.icon && element.iconJson.length === 0 && element.section && element.filter) { const tag = element.filter.field; // recherche si on a un mapping des topics if (data.topics && data.topics[tag]) { // dans la configuration avec un tag 'topics' (ex. edisto.json) element.iconJson = data.topics[tag]; } else if (data[tag]) { // dans la configuration avec directement la cléf element.iconJson = data[tag]; } else if (Topics[tag]) { // dans le fichier de mapping local element.iconJson = Topics[tag]; } else { // pas d'icones element.iconJson = []; } } element.subcategory = true; // new property ! categories.push(element); } } else { categories.push(category); } }); // Crée uniquement le contenu de la catégorie active var activeIndex = categories.findIndex(cat => cat.id === this.categoryId); if (activeIndex === -1) { activeIndex = 0; } // INFO // les containers de contenu sont definis à partir // de l'ordre des catégories / sous-categories // il y'a autant de catégories / sous-categories que de containers var contents = container.querySelectorAll(".tabcontent"); for (let i = 0; i < contents.length; i++) { const content = contents[i]; if (i === activeIndex) { // TODO // on peut faire un lazy-load des autres catégories // pour ne pas charger toutes les couches d'un coup // on affiche le contenu de la catégorie active // et on charge les autres au fur et à mesure } var layersCategorised = this.getLayersByCategory(categories[i], data.layers); // INFO // Pas de données directement dans le DOM si // - en mode cluster, on attend la création du cluster pour ajouter des données // - en mode on-demand, on attend la demande de chargement // - en mode none, on ajoute directement les données dans le DOM var nodata = (categories[i].cluster && this.options.optimisation === "clusterize") || this.options.optimisation === "on-demand"; this._createCatalogContentCategoryTabContent(categories[i], layersCategorised,