geopf-extensions-openlayers
Version:
French Geoportal Extensions for OpenLayers libraries
1,192 lines (1,100 loc) • 87.4 kB
JavaScript
// 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,