Skip to content

GeoLeaf.Filters — Documentation du module Filters

Product Version : GeoLeaf Platform V2

Version : 2.0.0

Fichier : src/modules/geoleaf.filters.tssrc/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 :

typescript
GeoLeaf.Filters.filterPoiList(basePois: any[], filterState: FilterState): any[]

Paramètres :

ParamètreTypeObligatoireDescription
basePoisany[]OuiTableau des POIs à filtrer
filterStateobjectOuiObjet décrivant les critères de filtrage

Structure filterState :

javascript
{
    // 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 filterState ne 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 :

javascript
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 :

typescript
GeoLeaf.Filters.filterRouteList(baseRoutes: any[], filterState: FilterState): any[]

Paramètres :

ParamètreTypeObligatoireDescription
baseRoutesany[]OuiTableau des Routes à filtrer
filterStateobjectOuiObjet décrivant les critères de filtrage

Retour : any[] — Tableau des Routes filtrées.

Exemple :

javascript
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.

javascript
const categories = GeoLeaf.Filters.getUniqueCategories(allPois);
// ['cafe', 'hiking', 'restaurant', ...]

getUniqueSubCategories(items)

Retourne la liste triée des IDs de sous-catégories uniques.

javascript
const subCategories = GeoLeaf.Filters.getUniqueSubCategories(allPois);

getUniqueTags(items)

Retourne la liste triée de tous les tags uniques présents dans les items.

javascript
const tags = GeoLeaf.Filters.getUniqueTags(allPois);
// ['accessible', 'family-friendly', 'scenic', 'wifi', ...]

countByCategory(items)

Retourne un dictionnaire { categoryId: count } pour les items fournis.

javascript
const counts = GeoLeaf.Filters.countByCategory(allPois);
// { restaurant: 12, cafe: 5, hotel: 8 }

countBySubCategory(items)

Retourne un dictionnaire { subCategoryId: count } pour les items fournis.

javascript
const counts = GeoLeaf.Filters.countBySubCategory(allPois);

getRatingStats(items)

Calcule des statistiques de notes pour un tableau d'items.

javascript
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é :

javascript
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é.

javascript
// 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) :

javascript
// 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.

javascript
filterState = {
    proximity: {
        active: true,
        center: { lat: 45.5017, lng: -73.5673 },
        radius: 2000, // 2 km
    },
    dataTypes: { poi: true, routes: false },
};

Note minimum

javascript
// 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 :

javascript
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

javascript
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

javascript
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

javascript
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

javascript
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

javascript
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

javascript
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

  1. Initialiser filterState avec dataTypes — requis pour le filtre de type :

    javascript
    const filterState = { dataTypes: { poi: true, routes: false } };
  2. Utiliser hasSearchText, hasMinRating, hasTags — ces flags activent explicitement les critères :

    javascript
    filterState = {
        searchText: "pizza",
        hasSearchText: true, // sans ce flag, searchText est ignoré
        dataTypes: { poi: true, routes: false },
    };
  3. Utiliser debounce pour recherche textuelle :

    javascript
    searchInput.addEventListener(
        "input",
        GeoLeaf.Utils.debounce(() => applyFilters(), 300)
    );
  4. Vérifier les résultats avant reload :

    javascript
    const filtered = GeoLeaf.Filters.filterPoiList(allPois, filterState);
    if (filtered.length === 0) {
        showNoResultsMessage();
    } else {
        GeoLeaf.POI.reload(filtered);
    }

DON'T

  1. Ne pas modifier allPois directement :

    javascript
    // Mauvais
    allPois = allPois.filter((poi) => poi.categoryId === "restaurant");
    
    // Bon
    const filtered = GeoLeaf.Filters.filterPoiList(allPois, {
        categoriesTree: ["restaurant"],
        dataTypes: { poi: true, routes: false },
    });
  2. 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

  1. Early return — chaque critère peut court-circuiter le filtre individuellement
  2. Flags booléenshasCats, hasTags, hasSearchText, etc. évitent les vérifications inutiles
  3. Résolution de coordonnées multi-sourceslatlng, lat/lng, coordinates, geometry.coordinates
  4. Formule Haversine intégrée — fallback si GeoLeaf.Utils.getDistance n'est pas disponible

Benchmarks indicatifs

Opération100 POIs1 000 POIs10 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, getSearchFieldsFromProfile

Tests

bash
# Lancer les tests Filters
npm test -- filters

Les 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

Released under the MIT License.