Skip to content

GeoLeaf.POI — Documentation du module POI

Version : 2.0.0 Dernière mise à jour : Mars 2026 Plateforme : GeoLeaf Platform V2 (MapLibre GL JS v5)

Le module GeoLeaf.POI gère l'ensemble de la logique liée aux Points d'Intérêt (POI) dans GeoLeaf. Il repose exclusivement sur l'adapter MapLibre GL JS : les POI sont rendus via une source GeoJSON avec clustering natif MapLibre (Supercluster), sans dépendance cartographique legacy.


Architecture — sous-modules

Le module POI est divisé en sous-modules spécialisés organisés dans packages/core/src/modules/built-in/poi/ :

Sous-moduleFichierResponsabilité
API publiquepoi-api.tsAssemblage du namespace GeoLeaf.POI — délègue aux sous-modules
Corecore.tsInit, chargement, affichage, CRUD POI
Sharedshared.tsÉtat partagé et constantes (singleton)
Normalizersnormalizers.tsNormalisation, validation et extraction de coordonnées
Markersmarkers.tsCréation des marqueurs via l'adapter MapLibre
Markers Stylingmarkers-styling.tsRésolution des couleurs et icônes par catégorie
Markers Icon HTMLmarkers-icon-html.tsConstruction HTML de l'icône de marqueur
Markers Spritemarkers-sprite-loader.tsInjection du sprite SVG du profil dans le DOM
Markers Eventsmarkers-events.tsÉvénements carte MapLibre (click, popup, panneau latéral)
Markers Configmarkers-config.tsConfiguration de base des marqueurs
Popuppopup.tsPopups et tooltips via l'adapter MapLibre
SidePanelsidepanel.tsPanneau latéral de détails POI
Renderersrenderers.tsAgrégateur des renderers de contenu (délègue au contract)
Renderers/renderers/Sous-répertoire : field, media, component, section, links, lightbox

Adaptateur MapLibre (src/adapters/maplibre/) :

FichierRôle
maplibre-poi-renderer.tsSources GeoJSON cluster, calques de rendu, événements carte
maplibre-poi-icons.tsEnregistrement des icônes SVG sprite dans MapLibre via map.addImage()

La façade publique packages/core/src/modules/geoleaf.poi.ts réexporte uniquement POI depuis poi/poi-api.ts.


Architecture de rendu MapLibre (mode GPU)

Dans GeoLeaf V2, les POI ne sont pas des éléments DOM individuels sur la carte. Ils sont rendus côté GPU via la chaîne suivante :

  1. Un tableau de POI est converti en GeoJSON FeatureCollection par poisToFeatureCollection()
  2. La FeatureCollection est injectée dans une source GeoJSON MapLibre (cluster: true, Supercluster)
  3. Quatre calques de rendu sont attachés à cette source :
    • gl-poi-{id}-clusters — cercles de cluster (type circle)
    • gl-poi-{id}-cluster-count — labels de comptage (type symbol)
    • gl-poi-{id}-unclustered — points individuels (type circle)
    • gl-poi-{id}-unclustered-icons — icônes SVG superposées (type symbol)
  4. Les événements click sont liés au niveau de la carte sur ces calques, pas sur des éléments DOM

Les popups et le panneau latéral sont, eux, des éléments DOM créés à la demande lors d'un clic.


Fonctionnalités principales

  • Chargement POI depuis le profil JSON actif, une dataUrl, ou le cache offline (plugin Storage)
  • Source GeoJSON cluster MapLibre (Supercluster) — clustering natif GPU, sans bibliothèque tiers
  • Marqueurs personnalisés avec icônes SVG issues du sprite de profil, couleurs par catégorie
  • Popups rapides via adapter.createPopup() / adapter.openPopup()
  • Tooltips (mode hover ou permanent)
  • Panneau latéral détaillé avec layouts personnalisables (slide-in animé, accessible RGAA)
  • Normalisation et sanitisation des données (anti-XSS via Security.escapeHtml)
  • Gestion gracieuse des erreurs (pattern "Logging over Throwing" — jamais d'exception levée)
  • Filtrage par catégories, tags, recherche (via GeoLeaf.Filters + setFilteredDisplay)
  • Événement geoleaf:poi:click et geoleaf:poi:panel:open/close sur le bus d'événements

API publique GeoLeaf.POI

Initialisation

init(mapOrOptions, config?)

Initialise le module POI. Détecte automatiquement l'adapter MapLibre, crée la source GeoJSON cluster, injecte le sprite d'icônes et déclenche le chargement initial.

Signatures supportées :

javascript
// Signature 1 : objet options avec map
GeoLeaf.POI.init({
    map: mapInstance, // ignoré — l'adapter est résolu via GeoLeaf.Core.getAdapter()
    clustering: true,
    showIconsOnMap: true,
});

// Signature 2 : séparés (compatibilité legacy)
GeoLeaf.POI.init(mapInstance, { clustering: true });

Note : En V2, la référence à la carte n'est plus passée manuellement. init() résout l'adapter via GeoLeaf.Core.getAdapter() et la carte native via adapter.getNativeMap(). Le paramètre map est accepté pour rétrocompatibilité mais non utilisé directement.

Options de configuration :

ParamètreTypeDéfautDescription
clusteringbooleantrueActive le clustering Supercluster
showIconsOnMapbooleantrueAffiche les icônes SVG sur les marqueurs
clusterRadiusnumber50Rayon de cluster en pixels
disableClusteringAtZoomnumber18Zoom à partir duquel le clustering est désactivé
showPopupbooleantrueSi false, le clic ouvre directement le panneau
tooltipModestring"hover""hover", "permanent", ou "none"

Retour : Promise<void> (async, mais retour ignorable).

Comportement :

  • Crée la source GeoJSON cluster et ses 4 calques de rendu dans MapLibre
  • Crée le panneau latéral DOM (lazy, une seule fois)
  • Enregistre les icônes SVG du sprite dans MapLibre via map.addImage()
  • Lie les événements click sur les calques unclustered et clusters
  • Déclenche loadAndDisplay() automatiquement

Exemple :

javascript
await GeoLeaf.Core.init({ mapId: "map", center: [45.76, 4.83], zoom: 12 });
GeoLeaf.POI.init({ clustering: true, clusterRadius: 50, disableClusteringAtZoom: 15 });

Chargement et affichage

loadAndDisplay()

Charge les POI depuis la source disponible et les affiche sur la carte.

javascript
GeoLeaf.POI.loadAndDisplay();

Ordre de priorité des sources :

  1. Profil actif (GeoLeaf.Config.getActiveProfilePoi()) — si un tableau de POI est défini dans le profil JSON
  2. Cache local (plugin Storage, si disponible) — POI de la file de synchronisation IndexedDB
  3. dataUrl — si poiConfig.dataUrl est défini, chargement HTTP asynchrone

Les POI du profil et du cache sont fusionnés sans doublons (par id).

Retour : Aucun.

Remarque : si le module Storage n'est pas encore prêt, loadAndDisplay() écoute l'événement geoleaf:storage:ready et se réexécute automatiquement.


displayPois(pois)

Affiche un tableau de POI sur la carte en mettant à jour la source GeoJSON cluster. Remplace l'affichage courant sans modifier state.allPois.

javascript
const pois = [
    { id: "p1", latlng: [45.76, 4.83], title: "Lyon" },
    { id: "p2", latlng: [48.85, 2.35], title: "Paris" },
];
GeoLeaf.POI.displayPois(pois);

Paramètres :

ParamètreTypeObligatoireDescription
poisarrayOuiTableau d'objets POI

Retour : Aucun.


setFilteredDisplay(filteredPois)

Met à jour la source cluster MapLibre avec un sous-ensemble filtré de POI, sans modifier state.allPois. L'ensemble complet des données reste intact pour les appels de filtre suivants.

javascript
const allPois = GeoLeaf.POI.getAllPois();
const restaurants = allPois.filter((p) => p.attributes?.categoryId === "restaurant");
GeoLeaf.POI.setFilteredDisplay(restaurants);

Paramètres :

ParamètreTypeObligatoireDescription
filteredPoisarrayOuiSous-ensemble de POI à afficher

Retour : Aucun.

Préférer setFilteredDisplay() à reload() pour les opérations de filtrage, afin de ne pas perdre le dataset complet.


reload(pois?)

Efface l'affichage courant et réaffiche. Si pois est fourni, remplace aussi state.allPois.

javascript
// Recharger l'affichage depuis state.allPois (sans changement de données)
GeoLeaf.POI.reload();

// Remplacer les données et réafficher
const updatedPois = await fetchPoisFromAPI();
GeoLeaf.POI.reload(updatedPois);

Paramètres :

ParamètreTypeObligatoireDescription
poisarrayNonNouveau tableau POI (si absent, réaffiche l'existant)

Retour : Aucun.


Gestion des POI individuels

addPoi(poi)

Ajoute un POI à la carte et au registre interne. Le POI est normalisé avant ajout.

javascript
const addedPoi = GeoLeaf.POI.addPoi({
    id: "custom-poi-1",
    latlng: [45.76, 4.83],
    title: "Restaurant Le Central",
    description: "Cuisine française traditionnelle",
    attributes: {
        categoryId: "restaurant",
        phone: "+33 4 78 00 00 00",
        website: "https://example.com",
        mainImage: "https://example.com/photo.jpg",
    },
});

if (addedPoi) {
    console.log("POI ajouté :", addedPoi.id);
}

Paramètres :

ParamètreTypeObligatoireDescription
poiobjectOuiObjet POI (au minimum latlng)

Retour : L'objet POI normalisé, ou null en cas d'échec (coordonnées invalides, normalisation impossible).

Comportement :

  • Normalise le POI via POINormalizers.normalizePoi()
  • Génère un id si absent (poi-{label}-{timestamp}-{random})
  • Pousse le POI dans state.allPois
  • Reconstruit la FeatureCollection et met à jour la source MapLibre via adapter.updateLayerData()
  • Retourne null et log l'erreur si les coordonnées sont invalides (NaN, hors limites)

add(poi)

Alias de addPoi().

javascript
GeoLeaf.POI.add(poi);

getAllPois()

Retourne tous les POI du registre interne.

javascript
const allPois = GeoLeaf.POI.getAllPois();
console.log(`${allPois.length} POI chargés`);
allPois.forEach((poi) => console.log(`- ${poi.title} (${poi.id})`));

Retour : array — tableau des objets POI normalisés.


getPoiById(id)

Retourne un POI par son identifiant.

javascript
const poi = GeoLeaf.POI.getPoiById("restaurant-123");
if (poi) {
    GeoLeaf.POI.showPoiDetails(poi);
} else {
    console.log("POI introuvable");
}

Paramètres :

ParamètreTypeObligatoireDescription
idstringOuiIdentifiant du POI

Retour : Objet POI ou null si introuvable.


getDisplayedPoisCount()

Retourne le nombre de POI dans le registre interne (state.allPois.length).

javascript
const count = GeoLeaf.POI.getDisplayedPoisCount();
console.log(`${count} POI(s) en mémoire`);

Retour : number.


Panneau latéral

showPoiDetails(poi, customLayout?)

Ouvre le panneau latéral et affiche les détails du POI. Crée le panneau DOM si nécessaire.

javascript
// Usage simple
const poi = GeoLeaf.POI.getPoiById("restaurant-123");
GeoLeaf.POI.showPoiDetails(poi);

// Avec layout personnalisé
GeoLeaf.POI.showPoiDetails(poi, [
    { field: "attributes.mainImage", type: "image", fullWidth: true },
    { field: "title", type: "title" },
    { field: "attributes.rating", type: "rating" },
    { field: "description", type: "paragraph" },
    { field: "attributes.phone", type: "phone", icon: "phone" },
    { field: "attributes.website", type: "link", label: "Site web" },
]);

Paramètres :

ParamètreTypeObligatoireDescription
poiobjectOuiObjet POI à afficher
customLayoutarrayNonTableau de sections layout (défaut : depuis le profil actif)

Retour : Promise<void>.

Comportement :

  • Crée le panneau <aside class="gl-poi-sidepanel"> si absent
  • Peuple le contenu via POIRenderers.populateSidePanel()
  • Anime l'ouverture (classe CSS open)
  • Place le focus sur le bouton de fermeture (RGAA F3)
  • Gère la touche Escape (fermeture) et le focus trap Tab/Shift+Tab (RGAA F4/F5)
  • Dispatche l'événement geoleaf:poi:panel:open

hideSidePanel()

Ferme le panneau latéral.

javascript
GeoLeaf.POI.hideSidePanel();

Retour : Aucun.

Comportement :

  • Retire la classe open du panneau et de l'overlay
  • Supprime les listeners Escape et focus trap
  • Nettoie la lightbox globale si ouverte
  • Dispatche l'événement geoleaf:poi:panel:close

openSidePanelWithLayout(poi, customLayout)

Alias de showPoiDetails(poi, customLayout) avec layout obligatoire.


Accès à l'état interne

getLayer()

Retourne le groupe de calques actif (clusterGroup ou layerGroup).

javascript
const layer = GeoLeaf.POI.getLayer();
// Retourne state.poiClusterGroup ou state.poiLayerGroup

Retour : Référence interne au groupe de calques, ou null si non initialisé.

En V2 MapLibre, cette méthode retourne un objet interne de l'état partagé. Elle n'expose pas un layer MapLibre natif ; les opérations cartographiques avancées passent par GeoLeaf.Core.getAdapter().


Format des données POI

Structure minimale

javascript
{
    latlng: [45.76, 4.83]; // REQUIS — [latitude, longitude]
}

Formats de coordonnées acceptés

javascript
// Tableau [lat, lng]
{ latlng: [45.76, 4.83] }

// Objet { lat, lng }
{ latlng: { lat: 45.76, lng: 4.83 } }

// Champs plats
{ lat: 45.76, lng: 4.83 }
{ latitude: 45.76, longitude: 4.83 }

// GeoJSON geometry
{ geometry: { type: "Point", coordinates: [4.83, 45.76] } }
// Note : GeoJSON utilise [lng, lat], la conversion est automatique

Structure complète normalisée

javascript
{
  // Identification
  id: "poi-123",

  // Position — REQUIS (un des formats ci-dessus)
  latlng: [45.7640, 4.8357],

  // Titre (multi-alias : title, label ou name — le premier non vide est utilisé)
  title: "Restaurant Le Central",
  label: "Le Central",
  name: "Le Central",
  description: "Cuisine française traditionnelle",

  // Métadonnées enrichies
  attributes: {

    // Catégorisation
    categoryId: "restaurant",
    subCategoryId: "french",

    // Contact
    phone: "+33 4 78 00 00 00",
    email: "contact@example.com",
    website: "https://example.com",         // sanitisé : http/https/data:image uniquement
    address: "12 rue de la Paix",

    // Médias
    mainImage: "https://example.com/photo.jpg",
    gallery: [
      "https://example.com/photo1.jpg",
      "https://example.com/photo2.jpg"
    ],

    // Descriptions longues
    shortDescription: "Bistrot lyonnais",
    longDescription: "Établissement familial depuis 1985...",

    // Horaires
    openingHours: ["Lun-Ven: 12h-14h30", "Lun-Sam: 19h-22h"],
    // openingHoursTable est calculé automatiquement si openingHours est un tableau
    openingHoursTable: [
      { day: "Lun-Ven", open: "12h", close: "14h30" },
      { day: "Lun-Sam", open: "19h", close: "22h" }
    ],

    // Prix et évaluation
    price: "25€",
    rating: 4.5,
    reviews: [...],

    // Tags et services
    tags: ["terrasse", "wifi"],
    services: ["livraison", "click-and-collect"],

    // Champs personnalisés libres
    speciality: "Bouchon lyonnais",
  },

  // Propriétés GeoJSON passthrough (si POI issu d'une feature GeoJSON)
  properties: {},

  // Métadonnées internes (injectées par le profil ou la configuration de couche)
  _layerConfig: { ... },       // config de couche (style, popup, tooltip)
  _sidepanelConfig: { ... },   // config de panneau latéral
}

Validation et normalisation

Validation des coordonnées

POINormalizers.extractCoordinates() valide les coordonnées avant tout rendu :

javascript
// Valide
{ latlng: [45.76, 4.83] }      // latitude -90..90, longitude -180..180
{ lat: 45.76, lng: 4.83 }

// Invalide — retourne null, log erreur
{ latlng: [95, 4.83] }         // latitude > 90
{ latlng: [45.76, 200] }       // longitude > 180
{ latlng: [NaN, NaN] }
{ latlng: null }

Limites strictes :

  • Latitude : -90 à 90
  • Longitude : -180 à 180
  • NaN : rejeté

Sanitisation HTML (anti-XSS)

Tous les champs texte sont échappés via Security.escapeHtml() lors de la normalisation :

javascript
GeoLeaf.POI.addPoi({
    latlng: [45.76, 4.83],
    title: '<script>alert("XSS")</script>',
});
// Rendu : &lt;script&gt;alert("XSS")&lt;/script&gt;

Sanitisation des URLs

Les URLs sont validées par _sanitizeUrl() — seuls les protocoles suivants sont acceptés :

javascript
// Accepté
website: "https://example.com";
photo: "http://example.com/img.jpg";
photo: "data:image/png;base64,iVBORw0KG...";

// Rejeté (devient null)
website: "javascript:alert(1)";
website: "data:text/html,<script>...";

Champs URL concernés : website, link, mainImage, photo, éléments de gallery.

Résolution multi-source

Le normaliseur applique une cascade de fallbacks pour chaque champ afin de supporter les formats GeoLeaf legacy, GeoJSON natif et formats personnalisés :

  • title : titlelabelnameattributes.titleproperties.name"Sans nom"
  • categoryId : attributes.categoryIdcategoryIdcategoryproperties.category
  • website : attributes.linkattributes.websiteproperties.linkproperties.website

Clustering MapLibre

Architecture

Le clustering est géré nativement par MapLibre GL JS via Supercluster. La source GeoJSON est déclarée avec cluster: true :

javascript
map.addSource("gl-poi-src-poi-source", {
    type: "geojson",
    data: featureCollection,
    cluster: true,
    clusterRadius: 50, // pixels
    clusterMaxZoom: 14, // dernier niveau avec clusters
});

Trois types de calques sont créés automatiquement :

CalqueTypeFiltre MapLibreRôle
gl-poi-{id}-clusterscircle["has", "point_count"]Cercles de groupe
gl-poi-{id}-cluster-countsymbol["has", "point_count"]Nombre de points dans le groupe
gl-poi-{id}-unclusteredcircle["!", ["has", "point_count"]]Points individuels
gl-poi-{id}-unclustered-iconssymbol["!", ...] + ["has", "symbolId"]Icônes SVG superposées

Configuration

Les paramètres de clustering sont passés à init() :

javascript
GeoLeaf.POI.init({
    clustering: true,
    clusterRadius: 50, // rayon cluster en pixels (défaut : 50)
    disableClusteringAtZoom: 15, // zoom à partir duquel les clusters se désagrègent
});

Clic sur un cluster

Un clic sur un cercle de cluster provoque un zoom d'expansion automatique. Le zoom cible est calculé par source.getClusterExpansionZoom() (API Supercluster).

Styles data-driven

Les couleurs et icônes sont encodées comme propriétés GeoJSON sur chaque feature, permettant des expressions MapLibre data-driven :

json
{
    "circle-color": ["coalesce", ["get", "colorFill"], "#4a90e5"],
    "circle-radius": ["coalesce", ["get", "radius"], 6],
    "circle-stroke-color": ["coalesce", ["get", "colorStroke"], "#ffffff"]
}

Icônes et sprites

Les icônes de marqueur proviennent du sprite SVG du profil actif. Le processus d'enregistrement MapLibre :

  1. Le sprite SVG est injecté dans le DOM sous la forme <svg data-geoleaf-sprite="profile">
  2. Chaque <symbol id="..."> est rendu sur un canvas 2D (API directe, sans <img>)
  3. L'ImageData résultante est enregistrée dans MapLibre via map.addImage(symbolId, imageData)
  4. Le calque unclustered-icons utilise ["get", "symbolId"] pour afficher l'icône correspondante

Le rendu canvas évite les contraintes CSP img-src et les comportements aléatoires du navigateur pour les SVG stroke-only chargés comme <img>.


Événements système

Le module POI émet des événements sur le bus d'événements GeoLeaf :

| Événement | Déclencheur | Données | | ------------------------- | ---------------------------- | ---------------------------------- | ----------- | | geoleaf:poi:click | Clic sur un point individuel | { poiId, layerId, source: "popup" | "direct" } | | geoleaf:poi:panel:open | Ouverture du panneau latéral | { poiId, poiName } | | geoleaf:poi:panel:close | Fermeture du panneau latéral | { poiId } |

Écoute d'un événement POI :

javascript
document.addEventListener("geoleaf:poi:click", (e) => {
    const { poiId, source } = e.detail;
    console.log(`POI cliqué : ${poiId} (source : ${source})`);
});

document.addEventListener("geoleaf:poi:panel:open", (e) => {
    const { poiId, poiName } = e.detail;
    console.log(`Panneau ouvert pour : ${poiName}`);
});

Gestion des erreurs

Le module POI applique le pattern "Logging over Throwing" : aucune exception n'est levée, les erreurs sont loguées via GeoLeaf.Log et les fonctions retournent null ou rien.

javascript
// Coordonnées invalides
const poi = GeoLeaf.POI.addPoi({ latlng: [95, 4.83] });
// poi === null
// Console : [POI] addPoi() : POI normalization failed.

// POI introuvable
const found = GeoLeaf.POI.getPoiById("inexistant");
// found === null (pas d'erreur)

// Module non initialisé
GeoLeaf.POI.displayPois([...]);
// Console : [POI] Core module not loaded.

// Adapter MapLibre absent
GeoLeaf.POI.init({});
// Console : [POI] MapLibre adapter not found. Cannot initialize POI module without adapter.

Principe :

  • Les fonctions retournent null ou undefined en cas d'échec
  • Les erreurs sont loguées : Log.error(), Log.warn(), Log.info(), Log.debug()
  • L'application ne crash pas sur un POI invalide ou manquant

Intégration avec les autres modules

Avec GeoLeaf.Config

javascript
// 1. Charger la configuration et le profil
await GeoLeaf.Config.load("/data/geoleaf.config.json");

// 2. Initialiser la carte
await GeoLeaf.Core.init({ mapId: "map" });

// 3. Initialiser POI — loadAndDisplay() est appelé automatiquement
GeoLeaf.POI.init({ clustering: true });
// Les POI du profil actif sont chargés automatiquement

Avec GeoLeaf.Filters

Pour le filtrage, utiliser setFilteredDisplay() (préserve le dataset complet) plutôt que reload() :

javascript
const allPois = GeoLeaf.POI.getAllPois();

// Filtrage par catégorie
const restaurants = GeoLeaf.Filters.filterPois(allPois, { categoryId: "restaurant" });
GeoLeaf.POI.setFilteredDisplay(restaurants);

// Réinitialisation du filtre (afficher tous les POI)
GeoLeaf.POI.setFilteredDisplay(allPois);

Voir docs/filters/GeoLeaf_Filters_README.md pour les options de filtrage détaillées.

Avec le plugin Storage (cache offline)

Quand le plugin @geoleaf-plugins/storage est actif, loadAndDisplay() fusionne automatiquement les POI du profil avec les POI en attente dans la file de synchronisation IndexedDB (pattern offline-first). L'intégration est transparente — aucune configuration supplémentaire n'est nécessaire.

Avec GeoLeaf._UIPanelBuilder

Le panneau latéral POI utilise POIRenderers.populateSidePanel() qui délègue à POIRenderersContract. Le rendu du contenu est configurable via les layouts de section définis dans le profil ou passés en paramètre à showPoiDetails().


Exemples d'usage

Exemple 1 : chargement depuis un profil

javascript
await GeoLeaf.Core.init({ mapId: "map", center: [45.76, 4.83], zoom: 12 });
GeoLeaf.POI.init({ clustering: true });
// Les POI du profil JSON actif sont affichés automatiquement

Exemple 2 : ajout de POI depuis une API

javascript
async function addPoiFromApi(id) {
    const response = await fetch(`/api/pois/${id}`);
    const data = await response.json();

    const poi = GeoLeaf.POI.addPoi({
        id: data.id,
        latlng: [data.lat, data.lng],
        title: data.name,
        description: data.description,
        attributes: {
            categoryId: data.category,
            phone: data.phone,
            mainImage: data.photo_url,
        },
    });

    if (poi) {
        console.log("POI ajouté :", poi.id);
    }
}

Exemple 3 : panneau latéral avec layout personnalisé

javascript
const poi = GeoLeaf.POI.getPoiById("restaurant-123");
if (!poi) return;

const customLayout = [
    { field: "attributes.mainImage", type: "image", fullWidth: true },
    { field: "title", type: "title" },
    { field: "attributes.rating", type: "rating", maxStars: 5 },
    { field: "description", type: "paragraph" },
    {
        type: "section",
        title: "Contact",
        fields: [
            { field: "attributes.phone", type: "phone" },
            { field: "attributes.email", type: "email" },
            { field: "attributes.website", type: "link", label: "Site web" },
        ],
    },
    { field: "attributes.gallery", type: "gallery", columns: 3 },
];

GeoLeaf.POI.showPoiDetails(poi, customLayout);

Exemple 4 : filtrage dynamique (barre de recherche)

javascript
// Utiliser setFilteredDisplay pour préserver state.allPois
document.getElementById("search").addEventListener("input", (e) => {
    const searchText = e.target.value.toLowerCase();
    const allPois = GeoLeaf.POI.getAllPois();

    const filtered = allPois.filter(
        (p) =>
            p.title?.toLowerCase().includes(searchText) ||
            p.description?.toLowerCase().includes(searchText)
    );

    GeoLeaf.POI.setFilteredDisplay(filtered);
});

// Réinitialiser le filtre
document.getElementById("reset").addEventListener("click", () => {
    GeoLeaf.POI.setFilteredDisplay(GeoLeaf.POI.getAllPois());
});

Exemple 5 : écoute d'événements POI

javascript
document.addEventListener("geoleaf:poi:click", (e) => {
    const { poiId, source } = e.detail;
    analytics.track("poi_click", { id: poiId, source });
});

document.addEventListener("geoleaf:poi:panel:open", (e) => {
    const { poiId, poiName } = e.detail;
    document.title = `${poiName} — GeoLeaf`;
});

Bonnes pratiques

A faire

  1. Toujours initialiser après GeoLeaf.Core.init()

    javascript
    await GeoLeaf.Core.init({ mapId: "map" });
    GeoLeaf.POI.init({ clustering: true });
  2. Vérifier le retour de addPoi()

    javascript
    const poi = GeoLeaf.POI.addPoi(data);
    if (!poi) {
        console.error("Échec ajout POI — coordonnées invalides ?");
    }
  3. Utiliser setFilteredDisplay() pour le filtrage, pas reload()

    javascript
    // Bon : préserve state.allPois
    GeoLeaf.POI.setFilteredDisplay(filteredSubset);
    
    // À éviter pour le filtrage : écrase state.allPois
    GeoLeaf.POI.reload(filteredSubset);
  4. Regrouper les métadonnées dans attributes

    javascript
    { latlng: [...], title: "...", attributes: { categoryId: "...", phone: "..." } }

A éviter

  1. Ne pas manipuler l'état interne directement

    javascript
    // Mauvais
    GeoLeaf._POIShared.state.allPois.push(poi);
    
    // Bon
    GeoLeaf.POI.addPoi(poi);
  2. Ne pas supposer que addPoi() réussit toujours

    javascript
    // Mauvais — crash si poi === null
    const poi = GeoLeaf.POI.addPoi(data);
    console.log(poi.id);
    
    // Bon
    const poi = GeoLeaf.POI.addPoi(data);
    if (poi) console.log(poi.id);
  3. Ne pas référencer d'anciens types cartographiques dans le code POI

    En V2, MapLibre GL JS est le seul moteur cartographique. Le code POI doit rester aligné sur l'adapter MapLibre et les sources/couches GeoJSON associées.


État partagé (POIShared.state)

L'état interne, accessible en lecture par les sous-modules :

| Propriété | Type | Description | | --------------------- | ------------- | ----------------------------------------------- | ------------------------------------------------ | | allPois | array | Tous les POI chargés (dataset complet) | | poiMarkers | Map | Marqueurs indexés par clé POI (id ou title) | | poiConfig | object | Configuration passée à init() | | mapInstance | any | Référence à la carte MapLibre native | | adapter | IMapAdapter | Adapter MapLibre actif | | poiSourceId | string | null | ID de la source GeoJSON cluster ("poi-source") | | poiClusterGroup | any | Référence interne au groupe cluster | | poiLayerGroup | any | Référence interne au groupe sans cluster | | isLoading | boolean | Chargement en cours | | sidePanelElement | HTMLElement | null | Élément DOM du panneau latéral | | currentPoiInPanel | any | POI actuellement affiché dans le panneau | | currentGalleryIndex | number | Index courant dans la galerie d'images |

Ne jamais modifier state directement — utiliser l'API publique GeoLeaf.POI.*.


Références

  • Façade publique : packages/core/src/modules/geoleaf.poi.ts
  • Implémentation : packages/core/src/modules/built-in/poi/poi-api.ts
  • Adapter MapLibre : packages/core/src/adapters/maplibre/maplibre-poi-renderer.ts
  • Icônes MapLibre : packages/core/src/adapters/maplibre/maplibre-poi-icons.ts
  • État partagé : packages/core/src/modules/built-in/poi/shared.ts
  • Filters : packages/core/docs/filters/GeoLeaf_Filters_README.md
  • Panel Builder : packages/core/docs/ui/GeoLeaf_UI_PanelBuilder_README.md
  • Tests POI : packages/core/__tests__/poi/

Changelog

v2.0.0 (Mars 2026)

  • Refactorisation complète sur MapLibre GL JS v5
  • Clustering via source GeoJSON MapLibre (Supercluster)
  • Rendu GPU sur 4 calques MapLibre (clusters, cluster-count, unclustered, unclustered-icons)
  • Icônes SVG enregistrées dans MapLibre via canvas 2D (map.addImage())
  • Ajout de setFilteredDisplay() — filtrage sans perte du dataset complet
  • Événements POI sur le bus GeoLeaf (geoleaf:poi:click, geoleaf:poi:panel:open/close)
  • Panneau latéral accessible RGAA 4.1 (focus trap, Escape, aria-hidden)
  • Intégration transparente avec le plugin Storage (merge offline POI)
  • Encodage UTF-8 propre — suppression des artefacts d'encodage

v2.0.0-alpha (Décembre 2025)

  • Split module en 7 sous-modules
  • Pattern "Logging over Throwing"
  • API simplifiée (addPoi remplace addPoint et addFromConfigItem)
  • Layouts personnalisés pour le panneau latéral

Dernière mise à jour : Mars 2026 Auteur : Mattieu Pottier — GeoLeaf Platform Version GeoLeaf : 2.0.0

Released under the MIT License.