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-module | Fichier | Responsabilité |
|---|---|---|
| API publique | poi-api.ts | Assemblage du namespace GeoLeaf.POI — délègue aux sous-modules |
| Core | core.ts | Init, chargement, affichage, CRUD POI |
| Shared | shared.ts | État partagé et constantes (singleton) |
| Normalizers | normalizers.ts | Normalisation, validation et extraction de coordonnées |
| Markers | markers.ts | Création des marqueurs via l'adapter MapLibre |
| Markers Styling | markers-styling.ts | Résolution des couleurs et icônes par catégorie |
| Markers Icon HTML | markers-icon-html.ts | Construction HTML de l'icône de marqueur |
| Markers Sprite | markers-sprite-loader.ts | Injection du sprite SVG du profil dans le DOM |
| Markers Events | markers-events.ts | Événements carte MapLibre (click, popup, panneau latéral) |
| Markers Config | markers-config.ts | Configuration de base des marqueurs |
| Popup | popup.ts | Popups et tooltips via l'adapter MapLibre |
| SidePanel | sidepanel.ts | Panneau latéral de détails POI |
| Renderers | renderers.ts | Agré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/) :
| Fichier | Rôle |
|---|---|
maplibre-poi-renderer.ts | Sources GeoJSON cluster, calques de rendu, événements carte |
maplibre-poi-icons.ts | Enregistrement 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 :
- Un tableau de POI est converti en GeoJSON
FeatureCollectionparpoisToFeatureCollection() - La
FeatureCollectionest injectée dans une source GeoJSON MapLibre (cluster: true, Supercluster) - Quatre calques de rendu sont attachés à cette source :
gl-poi-{id}-clusters— cercles de cluster (typecircle)gl-poi-{id}-cluster-count— labels de comptage (typesymbol)gl-poi-{id}-unclustered— points individuels (typecircle)gl-poi-{id}-unclustered-icons— icônes SVG superposées (typesymbol)
- Les événements
clicksont 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:clicketgeoleaf:poi:panel:open/closesur 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 :
// 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 viaGeoLeaf.Core.getAdapter()et la carte native viaadapter.getNativeMap(). Le paramètremapest accepté pour rétrocompatibilité mais non utilisé directement.
Options de configuration :
| Paramètre | Type | Défaut | Description |
|---|---|---|---|
clustering | boolean | true | Active le clustering Supercluster |
showIconsOnMap | boolean | true | Affiche les icônes SVG sur les marqueurs |
clusterRadius | number | 50 | Rayon de cluster en pixels |
disableClusteringAtZoom | number | 18 | Zoom à partir duquel le clustering est désactivé |
showPopup | boolean | true | Si false, le clic ouvre directement le panneau |
tooltipMode | string | "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
clicksur les calquesunclusteredetclusters - Déclenche
loadAndDisplay()automatiquement
Exemple :
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.
GeoLeaf.POI.loadAndDisplay();Ordre de priorité des sources :
- Profil actif (
GeoLeaf.Config.getActiveProfilePoi()) — si un tableau de POI est défini dans le profil JSON - Cache local (plugin Storage, si disponible) — POI de la file de synchronisation IndexedDB
dataUrl— sipoiConfig.dataUrlest 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.
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ètre | Type | Obligatoire | Description |
|---|---|---|---|
pois | array | Oui | Tableau 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.
const allPois = GeoLeaf.POI.getAllPois();
const restaurants = allPois.filter((p) => p.attributes?.categoryId === "restaurant");
GeoLeaf.POI.setFilteredDisplay(restaurants);Paramètres :
| Paramètre | Type | Obligatoire | Description |
|---|---|---|---|
filteredPois | array | Oui | Sous-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.
// 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ètre | Type | Obligatoire | Description |
|---|---|---|---|
pois | array | Non | Nouveau 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.
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ètre | Type | Obligatoire | Description |
|---|---|---|---|
poi | object | Oui | Objet 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
idsi absent (poi-{label}-{timestamp}-{random}) - Pousse le POI dans
state.allPois - Reconstruit la
FeatureCollectionet met à jour la source MapLibre viaadapter.updateLayerData() - Retourne
nullet log l'erreur si les coordonnées sont invalides (NaN, hors limites)
add(poi)
Alias de addPoi().
GeoLeaf.POI.add(poi);getAllPois()
Retourne tous les POI du registre interne.
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.
const poi = GeoLeaf.POI.getPoiById("restaurant-123");
if (poi) {
GeoLeaf.POI.showPoiDetails(poi);
} else {
console.log("POI introuvable");
}Paramètres :
| Paramètre | Type | Obligatoire | Description |
|---|---|---|---|
id | string | Oui | Identifiant du POI |
Retour : Objet POI ou null si introuvable.
getDisplayedPoisCount()
Retourne le nombre de POI dans le registre interne (state.allPois.length).
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.
// 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ètre | Type | Obligatoire | Description |
|---|---|---|---|
poi | object | Oui | Objet POI à afficher |
customLayout | array | Non | Tableau 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.
GeoLeaf.POI.hideSidePanel();Retour : Aucun.
Comportement :
- Retire la classe
opendu 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).
const layer = GeoLeaf.POI.getLayer();
// Retourne state.poiClusterGroup ou state.poiLayerGroupRetour : 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
{
latlng: [45.76, 4.83]; // REQUIS — [latitude, longitude]
}Formats de coordonnées acceptés
// 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 automatiqueStructure complète normalisée
{
// 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 :
// 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 :
GeoLeaf.POI.addPoi({
latlng: [45.76, 4.83],
title: '<script>alert("XSS")</script>',
});
// Rendu : <script>alert("XSS")</script>Sanitisation des URLs
Les URLs sont validées par _sanitizeUrl() — seuls les protocoles suivants sont acceptés :
// 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:title→label→name→attributes.title→properties.name→"Sans nom"categoryId:attributes.categoryId→categoryId→category→properties.categorywebsite:attributes.link→attributes.website→properties.link→properties.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 :
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 :
| Calque | Type | Filtre MapLibre | Rôle |
|---|---|---|---|
gl-poi-{id}-clusters | circle | ["has", "point_count"] | Cercles de groupe |
gl-poi-{id}-cluster-count | symbol | ["has", "point_count"] | Nombre de points dans le groupe |
gl-poi-{id}-unclustered | circle | ["!", ["has", "point_count"]] | Points individuels |
gl-poi-{id}-unclustered-icons | symbol | ["!", ...] + ["has", "symbolId"] | Icônes SVG superposées |
Configuration
Les paramètres de clustering sont passés à init() :
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 :
{
"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 :
- Le sprite SVG est injecté dans le DOM sous la forme
<svg data-geoleaf-sprite="profile"> - Chaque
<symbol id="...">est rendu sur un canvas 2D (API directe, sans<img>) - L'
ImageDatarésultante est enregistrée dans MapLibre viamap.addImage(symbolId, imageData) - Le calque
unclustered-iconsutilise["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 :
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.
// 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
nullouundefineden 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
// 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 automatiquementAvec GeoLeaf.Filters
Pour le filtrage, utiliser setFilteredDisplay() (préserve le dataset complet) plutôt que reload() :
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
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 automatiquementExemple 2 : ajout de POI depuis une API
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é
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)
// 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
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
Toujours initialiser après
GeoLeaf.Core.init()javascriptawait GeoLeaf.Core.init({ mapId: "map" }); GeoLeaf.POI.init({ clustering: true });Vérifier le retour de
addPoi()javascriptconst poi = GeoLeaf.POI.addPoi(data); if (!poi) { console.error("Échec ajout POI — coordonnées invalides ?"); }Utiliser
setFilteredDisplay()pour le filtrage, pasreload()javascript// Bon : préserve state.allPois GeoLeaf.POI.setFilteredDisplay(filteredSubset); // À éviter pour le filtrage : écrase state.allPois GeoLeaf.POI.reload(filteredSubset);Regrouper les métadonnées dans
attributesjavascript{ latlng: [...], title: "...", attributes: { categoryId: "...", phone: "..." } }
A éviter
Ne pas manipuler l'état interne directement
javascript// Mauvais GeoLeaf._POIShared.state.allPois.push(poi); // Bon GeoLeaf.POI.addPoi(poi);Ne pas supposer que
addPoi()réussit toujoursjavascript// 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);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 (
addPoiremplaceaddPointetaddFromConfigItem) - Layouts personnalisés pour le panneau latéral
Dernière mise à jour : Mars 2026 Auteur : Mattieu Pottier — GeoLeaf Platform Version GeoLeaf : 2.0.0
