GeoLeaf.Filters — Documentation du module Filters
Product Version : GeoLeaf Platform V2
Version : 2.0.0
Fichier : src/modules/geoleaf.filters.ts → src/modules/built-in/filters/index.ts
Dernière mise à jour : Mars 2026
Vue d'ensemble
Le module GeoLeaf.Filters gère le filtrage avancé des POI et des Routes selon de multiples critères.
Il a été créé lors d'une refactorisation pour découpler la logique de filtrage de l'UI et des modules POI/Route. Il centralise tous les algorithmes de filtrage dans un module réutilisable.
Critères de filtrage supportés
POIs
- Catégories (
categoriesTree) — filtrage par ID de catégorie(s) - Sous-catégories (
subCategoriesTree) — filtrage par ID de sous-catégorie(s) - Tags (
selectedTags) — filtrage par tags (union) - Recherche (
searchText) — recherche textuelle dans champs configurables - Note (
minRating) — filtrage par note minimum - Proximité (
proximity) — filtrage par distance depuis un point - Type de données (
dataTypes) — filtrage POI vs routes
Routes
- Catégories (
categoriesTree) - Tags (
selectedTags) - Recherche (
searchText) - Note (
minRating) - Proximité (
proximity)
API publique
filterPoiList(basePois, filterState)
Filtre un tableau de POIs selon un état de filtres actifs.
Signature :
GeoLeaf.Filters.filterPoiList(basePois: any[], filterState: FilterState): any[]Paramètres :
| Paramètre | Type | Obligatoire | Description |
|---|---|---|---|
basePois | any[] | Oui | Tableau des POIs à filtrer |
filterState | object | Oui | Objet décrivant les critères de filtrage |
Structure filterState :
{
// Catégorisation (tableau d'IDs sélectionnés)
categoriesTree: ['restaurant', 'cafe'],
subCategoriesTree: ['french', 'italian'],
// Tags (union : au moins un tag doit correspondre)
selectedTags: ['wifi', 'terrasse'],
hasTags: true,
// Recherche textuelle
searchText: 'pizza',
hasSearchText: true,
searchFields: ['label', 'attributes.shortDescription'], // optionnel
// Note minimum
minRating: 4.0,
hasMinRating: true,
// Proximité géographique
proximity: {
active: true,
center: { lat: 45.5017, lng: -73.5673 },
radius: 5000 // en mètres
},
// Filtre de type de données
dataTypes: { poi: true, routes: false }
}Retour : any[] — Tableau des POIs filtrés.
Comportement :
- Tous les critères sont appliqués en ET logique (intersection)
- Si
filterStatene définit aucun critère actif, retourne tous les POIs - La recherche textuelle est insensible à la casse
- Les tags fonctionnent en union (au moins un tag du filtre présent dans le POI)
Exemple :
const allPois = GeoLeaf.POI.getAllPois();
// Filtrage par catégorie
const restaurants = GeoLeaf.Filters.filterPoiList(allPois, {
categoriesTree: ["restaurant"],
dataTypes: { poi: true, routes: false },
});
// Filtrage multi-critères
const premiumRestaurants = GeoLeaf.Filters.filterPoiList(allPois, {
categoriesTree: ["restaurant"],
selectedTags: ["terrasse", "wifi"],
hasTags: true,
minRating: 4.5,
hasMinRating: true,
searchText: "italien",
hasSearchText: true,
dataTypes: { poi: true, routes: false },
});
// Filtrage par proximité
const nearbyPois = GeoLeaf.Filters.filterPoiList(allPois, {
proximity: {
active: true,
center: { lat: 45.5017, lng: -73.5673 },
radius: 2000,
},
dataTypes: { poi: true, routes: false },
});filterRouteList(baseRoutes, filterState)
Filtre un tableau de Routes selon un état de filtres actifs.
Signature :
GeoLeaf.Filters.filterRouteList(baseRoutes: any[], filterState: FilterState): any[]Paramètres :
| Paramètre | Type | Obligatoire | Description |
|---|---|---|---|
baseRoutes | any[] | Oui | Tableau des Routes à filtrer |
filterState | object | Oui | Objet décrivant les critères de filtrage |
Retour : any[] — Tableau des Routes filtrées.
Exemple :
const allRoutes = GeoLeaf.Route.getAllRoutes();
// Routes de randonnée faciles
const easyHikes = GeoLeaf.Filters.filterRouteList(allRoutes, {
categoriesTree: ["hiking"],
dataTypes: { poi: false, routes: true },
});getUniqueCategories(items)
Retourne la liste triée des IDs de catégories uniques présentes dans un tableau de POIs/routes.
const categories = GeoLeaf.Filters.getUniqueCategories(allPois);
// ['cafe', 'hiking', 'restaurant', ...]getUniqueSubCategories(items)
Retourne la liste triée des IDs de sous-catégories uniques.
const subCategories = GeoLeaf.Filters.getUniqueSubCategories(allPois);getUniqueTags(items)
Retourne la liste triée de tous les tags uniques présents dans les items.
const tags = GeoLeaf.Filters.getUniqueTags(allPois);
// ['accessible', 'family-friendly', 'scenic', 'wifi', ...]countByCategory(items)
Retourne un dictionnaire { categoryId: count } pour les items fournis.
const counts = GeoLeaf.Filters.countByCategory(allPois);
// { restaurant: 12, cafe: 5, hotel: 8 }countBySubCategory(items)
Retourne un dictionnaire { subCategoryId: count } pour les items fournis.
const counts = GeoLeaf.Filters.countBySubCategory(allPois);getRatingStats(items)
Calcule des statistiques de notes pour un tableau d'items.
const stats = GeoLeaf.Filters.getRatingStats(allPois);
// {
// min: 1.5,
// max: 5.0,
// avg: 3.8,
// count: 42,
// withRating: 35,
// withoutRating: 7
// }Configuration des champs de recherche
Le module Filters utilise une hiérarchie de fallbacks pour déterminer les champs de recherche :
1. Priorité : Champs searchFields dans filterState
Si filterState.searchFields est fourni et non vide, il est utilisé en priorité :
const filtered = GeoLeaf.Filters.filterPoiList(allPois, {
searchText: "pizza",
hasSearchText: true,
searchFields: ["label", "attributes.shortDescription"],
dataTypes: { poi: true, routes: false },
});2. Fallback : getSearchFieldsFromProfile()
Si searchFields n'est pas fourni, les champs sont extraits du profil actif (champs marqués "search": true dans les layouts, ou searchFields dans le filtre search).
3. Fallback final : Champs par défaut
Si aucune configuration, utilise : ['title', 'label', 'name']
Algorithmes de filtrage
Catégories et Sous-catégories
Le filtre par sous-catégories est prioritaire sur les catégories. Si subCategoriesTree est défini et non vide, il est appliqué ; sinon categoriesTree est utilisé.
// Si subCategoriesTree est non vide, seul le filtre sous-catégorie s'applique
filterState = {
subCategoriesTree: ["french"],
categoriesTree: ["restaurant"], // ignoré si subCategoriesTree est actif
dataTypes: { poi: true, routes: false },
};Tags (Union)
Au moins un tag du filtre doit être présent dans le POI (logique OU) :
// Le POI est inclus s'il a 'wifi' OU 'terrasse'
filterState = {
selectedTags: ["wifi", "terrasse"],
hasTags: true,
dataTypes: { poi: true, routes: false },
};Proximité géographique
Utilise la formule de Haversine (ou GeoLeaf.Utils.getDistance si disponible) pour calculer la distance en mètres. Pour les POIs, la distance est calculée depuis latlng, lat/lng, ou geometry.coordinates.
filterState = {
proximity: {
active: true,
center: { lat: 45.5017, lng: -73.5673 },
radius: 2000, // 2 km
},
dataTypes: { poi: true, routes: false },
};Note minimum
// Note moyenne >= minRating (extrait depuis attributes.rating, reviews.rating, ou reviews[].rating)
filterState = {
minRating: 4.0,
hasMinRating: true,
dataTypes: { poi: true, routes: false },
};Intégration avec l'UI
Construction du filterState depuis DOM
Exemple typique depuis les contrôles UI :
function buildFilterStateFromUI() {
const filterState = {
dataTypes: { poi: true, routes: false },
categoriesTree: [],
subCategoriesTree: [],
selectedTags: [],
hasTags: false,
searchText: "",
hasSearchText: false,
minRating: 0,
hasMinRating: false,
proximity: { active: false },
};
// Catégories (checkboxes)
document.querySelectorAll(".gl-filter-category:checked").forEach((cb) => {
filterState.categoriesTree.push(cb.value);
});
// Tags (checkboxes)
document.querySelectorAll(".gl-filter-tag:checked").forEach((cb) => {
filterState.selectedTags.push(cb.value);
});
filterState.hasTags = filterState.selectedTags.length > 0;
// Recherche (input text)
const searchInput = document.getElementById("search-input");
if (searchInput && searchInput.value.trim()) {
filterState.searchText = searchInput.value.trim().toLowerCase();
filterState.hasSearchText = true;
}
// Note minimum (select)
const ratingSelect = document.getElementById("rating-filter");
if (ratingSelect && ratingSelect.value) {
filterState.minRating = parseFloat(ratingSelect.value);
filterState.hasMinRating = true;
}
return filterState;
}
// Appliquer les filtres
function applyFilters() {
const filterState = buildFilterStateFromUI();
const allPois = GeoLeaf.POI.getAllPois();
const filtered = GeoLeaf.Filters.filterPoiList(allPois, filterState);
GeoLeaf.POI.reload(filtered);
document.getElementById("results-count").textContent = `${filtered.length} résultat(s)`;
}
// Écouter les changements
document.querySelectorAll(".gl-filter-category, .gl-filter-tag").forEach((cb) => {
cb.addEventListener("change", applyFilters);
});
document
.getElementById("search-input")
.addEventListener("input", GeoLeaf.Utils.debounce(applyFilters, 300));Réinitialisation des filtres
function resetFilters() {
document.querySelectorAll(".gl-filter-category, .gl-filter-tag").forEach((cb) => {
cb.checked = false;
});
document.getElementById("search-input").value = "";
document.getElementById("rating-filter").value = "";
const allPois = GeoLeaf.POI.getAllPois();
GeoLeaf.POI.reload(allPois);
}Exemples d'usage
Exemple 1 : Filtrage POI basique
const allPois = GeoLeaf.POI.getAllPois();
const restaurantsWithWifi = GeoLeaf.Filters.filterPoiList(allPois, {
categoriesTree: ["restaurant"],
selectedTags: ["wifi"],
hasTags: true,
dataTypes: { poi: true, routes: false },
});
console.log(`${restaurantsWithWifi.length} restaurants avec wifi`);
GeoLeaf.POI.reload(restaurantsWithWifi);Exemple 2 : Recherche textuelle
const allPois = GeoLeaf.POI.getAllPois();
const pizzaPlaces = GeoLeaf.Filters.filterPoiList(allPois, {
searchText: "pizza",
hasSearchText: true,
dataTypes: { poi: true, routes: false },
});Exemple 3 : POIs à proximité avec géolocalisation
navigator.geolocation.getCurrentPosition((position) => {
const allPois = GeoLeaf.POI.getAllPois();
const nearbyPois = GeoLeaf.Filters.filterPoiList(allPois, {
proximity: {
active: true,
center: {
lat: position.coords.latitude,
lng: position.coords.longitude,
},
radius: 2000,
},
dataTypes: { poi: true, routes: false },
});
GeoLeaf.POI.reload(nearbyPois);
});Exemple 4 : Statistiques avant filtrage
const allPois = GeoLeaf.POI.getAllPois();
// Analyser les données disponibles
const categories = GeoLeaf.Filters.getUniqueCategories(allPois);
const tags = GeoLeaf.Filters.getUniqueTags(allPois);
const counts = GeoLeaf.Filters.countByCategory(allPois);
const ratingStats = GeoLeaf.Filters.getRatingStats(allPois);
console.log("Catégories disponibles:", categories);
console.log("Note moyenne:", ratingStats.avg.toFixed(1));Exemple 5 : Filtre dynamique avec debounce
const searchInput = document.getElementById("search-input");
let allPois = GeoLeaf.POI.getAllPois();
const performSearch = GeoLeaf.Utils.debounce((searchText) => {
const filtered = GeoLeaf.Filters.filterPoiList(allPois, {
searchText: searchText.toLowerCase(),
hasSearchText: searchText.length > 0,
dataTypes: { poi: true, routes: false },
});
GeoLeaf.POI.reload(filtered);
updateResultsCount(filtered.length);
}, 300);
searchInput.addEventListener("input", (e) => {
performSearch(e.target.value);
});Bonnes pratiques
DO
Initialiser
filterStateavecdataTypes— requis pour le filtre de type :javascriptconst filterState = { dataTypes: { poi: true, routes: false } };Utiliser
hasSearchText,hasMinRating,hasTags— ces flags activent explicitement les critères :javascriptfilterState = { searchText: "pizza", hasSearchText: true, // sans ce flag, searchText est ignoré dataTypes: { poi: true, routes: false }, };Utiliser debounce pour recherche textuelle :
javascriptsearchInput.addEventListener( "input", GeoLeaf.Utils.debounce(() => applyFilters(), 300) );Vérifier les résultats avant reload :
javascriptconst filtered = GeoLeaf.Filters.filterPoiList(allPois, filterState); if (filtered.length === 0) { showNoResultsMessage(); } else { GeoLeaf.POI.reload(filtered); }
DON'T
Ne pas modifier
allPoisdirectement :javascript// Mauvais allPois = allPois.filter((poi) => poi.categoryId === "restaurant"); // Bon const filtered = GeoLeaf.Filters.filterPoiList(allPois, { categoriesTree: ["restaurant"], dataTypes: { poi: true, routes: false }, });Ne pas appliquer les filtres à chaque keystroke sans debounce :
javascript// Mauvais (lag) searchInput.addEventListener("input", applyFilters); // Bon searchInput.addEventListener("input", GeoLeaf.Utils.debounce(applyFilters, 300));
Performance
Optimisations implémentées
- Early return — chaque critère peut court-circuiter le filtre individuellement
- Flags booléens —
hasCats,hasTags,hasSearchText, etc. évitent les vérifications inutiles - Résolution de coordonnées multi-sources —
latlng,lat/lng,coordinates,geometry.coordinates - Formule Haversine intégrée — fallback si
GeoLeaf.Utils.getDistancen'est pas disponible
Benchmarks indicatifs
| Opération | 100 POIs | 1 000 POIs | 10 000 POIs |
|---|---|---|---|
| Catégorie simple | < 1 ms | < 5 ms | ~30 ms |
| Recherche textuelle | < 2 ms | ~10 ms | ~80 ms |
| Proximité | < 3 ms | ~15 ms | ~120 ms |
| Multi-critères (5) | < 5 ms | ~20 ms | ~150 ms |
Architecture interne
built-in/filters/
├── index.ts ← Aggregate export : Filters namespace
├── poi-filter.ts ← filterPoiList + helpers internes
├── route-filter.ts ← filterRouteList + helpers internes
├── filter-stats.ts ← getUniqueCategories, countByCategory, getRatingStats, …
└── utils.ts ← getNestedValue, getSearchFieldsFromProfileTests
# Lancer les tests Filters
npm test -- filtersLes tests couvrent :
- Filtrage par catégorie, sous-catégorie, tags
- Recherche textuelle (insensible à la casse, multi-champs)
- Filtrage par note (reviews[], reviews.rating, attributes.rating)
- Filtrage par proximité (Haversine)
- Statistiques (getUniqueCategories, getRatingStats, …)
- Cas limites (tableau vide, filterState vide, POI sans coordonnées)
Références
- Code source :
src/modules/built-in/filters/ - Façade publique :
src/modules/geoleaf.filters.ts - Module POI :
docs/poi/GeoLeaf_POI_README.md - Module Route :
docs/route/GeoLeaf_Route_README.md - Configuration profils :
docs/config/GeoLeaf_Config_README.md
Changelog
v2.0.0 (mars 2026)
- Migration vers MapLibre GL JS ^5.0.0 (suppression des références legacy cartographiques)
- Refactorisation : extraction dans
built-in/filters/(poi-filter, route-filter, filter-stats, utils) - Ajout des fonctions statistiques :
getUniqueCategories,getUniqueSubCategories,getUniqueTags,countByCategory,countBySubCategory,getRatingStats filterState: nouveau format avec flags explicites (hasTags,hasSearchText,hasMinRating)- Tags : passage de logique ET (intersection) à logique OU (union)
- Proximité : nouveau format
{ active, center: { lat, lng }, radius }(remplace{ lat, lng, radius })
v2.0.0-alpha (décembre 2025)
- Création du module Filters (extraction depuis UI)
- Support multi-critères pour POIs et Routes
- Filtrage proximité avec formule Haversine
- Configuration dynamique des champs de recherche
Dernière mise à jour : Mars 2026
Auteur : Équipe GeoLeaf
Version GeoLeaf : 2.0.0
