import lunr from "./lib/lunr/lunr.js" import lunrStemmer from "./lib/lunr/lunr.stemmer.support.js" import lunrFr from "./lib/lunr/lunr.fr.js" lunrStemmer(lunr) lunrFr(lunr) const FEATURE_I = Symbol("Feature index") export const FEATURE_ID = Symbol("Feature id") /** * Base de données locale des différents endroits du lieu */ export class PlaceDatabase extends EventTarget { featuresById = {} fullTextIndex = null /** * Résultats d'une recherche dans la base de données * @typedef {Object} SearchResult * @property {number} score Score de match avec la requète (le plus haut est le plus proche) * @property {Object} feature Feature GeoJSON * @property {string} ref Id de la feature GeoJSON */ /** * Rechercher un emplacement * @param {string} query Requète textuelle * @returns {SearchResult[]} Résultat de la recherche ordonnée par score (le plus haut en premier) */ search(query){ if(!this.fullTextIndex){ throw new Error("Full text index not built, please run \"buildIndex()\" function") } let raw_result = this.fullTextIndex.search(`${query}^10 ${query}*^2 *${query}*`) return raw_result.map(it => ({ score: it.score, feature: this.getFeatureById(it.ref), ref: it.ref })) } /** * Obtenir une feature à partir de son identifiant * @param {string} id Identifiant de la feature * @returns {Object|undefined} la feature ou undefined si elle n'a pas été trouvée */ getFeatureById(id){ return this.featuresById[id] } /** * Options de chargement des données GeoJSON * @typedef {Object} AddFeatureOptions * @property {boolean|undefined} batch Activer le mode "batch" qui se content de charger de d'ajouter les données sans reconstruire les index. Il faudra appeler "buildIndex()" pour reconstruire les index. * @property {string|undefined} idPrefix Prefix des identifiants de feature */ /** * Charger et ajouter des données GeoJSON * @param {URL|string} url L'URL d'où doivent être chargées les données * @param {AddFeatureOptions} options Options de chargement des données * @returns {string[]} ids of added features */ async loadGeojson(url, options){ let res = await fetch(url) if(!res.ok) throw new Error(`Fail to load data from ${url}: server responded with status ${res.status} ${res.statusText}`) let content = await res.json() return this.addFeature(content, options) } /** * @event NewFeatureEvent * @type {Event} * @property {"newfeature"} type * @property {string} ref Identifiant de la feature qui vient d'être ajoutée * @property {object} feature Object GeoJSON de la feature */ /** * Ajouter un object GeoJSON a la base de données * * Si vous voulez charger des données depuis une URL, utilisez "loadGeojson" * @see loadGeojson * * @fires NewFeatureEvent Quand une nouvelle feature est ajoutée à la base de données * * @param {Object} geojson Object GeoJSON à ajouter * @param {AddFeatureOptions} options Options d'importation * @returns {string[]} ids of added features */ addFeature(geojson, options){ let idPrefix = options.idPrefix || "" let all_ids = [] if(geojson.type == "Feature"){ let featureId = geojson?.properties?.id || geojson?.properties?.[FEATURE_I] let full_id = idPrefix+"#"+featureId if(this.featuresById[full_id]){ console.warn(`Warning: a feature with ID "${full_id}" already exists in database`) } geojson[FEATURE_ID] = full_id this.featuresById[full_id] = geojson let event = new Event("newfeature") event.ref = full_id event.feature = geojson this.dispatchEvent(event) all_ids.push(full_id) } else if(geojson.type == "FeatureCollection") { if(geojson.name){ idPrefix += (idPrefix != "" ? "-" : "")+geojson.name } if(geojson?.properties?.[FEATURE_I]){ idPrefix += (idPrefix != "" ? "-" : "")+geojson.properties[FEATURE_I] } for(let [i, feature] of Object.entries(geojson.features)){ feature.properties = { ...(feature.properties || {}), [FEATURE_I]: i } let id = this.addFeature(feature, { ...(options || {}), batch: true, idPrefix: idPrefix }) all_ids.push(id) } } else { throw new Error(`Unsupported GsoJSON feature of type "${geojson.type}"`) } if(options.batch !== false){ this.buildIndex() } return all_ids } /** * Construire l'index de recherche * @fires "indexbuild" Quand l'index vient d'être (re)construit */ buildIndex(){ let database = this; this.fullTextIndex = lunr(function(){ this.ref("id") this.field("name") this.field("synonyms") for(let [id, feature] of Object.entries(database.featuresById)){ let synonyms = "" if(feature.properties["n couchage"] > 0){ synonyms += SLEEPING_SYNONYMS.join(" ") } if(feature.properties["toilettes"]){ synonyms += " "+TOILETTES_SYNONYMS.join(" ") } this.add({ id, name: feature.properties.name, synonyms }) } }) this.dispatchEvent(new Event("indexbuild")) } static async createDefault(){ let default_database = new PlaceDatabase() await Promise.all([ default_database.loadGeojson(new URL("../couches/batiments.geojson", import.meta.url), {batch: true}), default_database.loadGeojson(new URL("../couches/piscine.geojson", import.meta.url), {batch: true}), default_database.loadGeojson(new URL("../couches/terrain-de-basket.geojson", import.meta.url), {batch: true}), default_database.loadGeojson(new URL("../couches/pieces-batiments.geojson", import.meta.url), {batch: true}), default_database.loadGeojson(new URL("../couches/camping.geojson", import.meta.url), {batch: true}), default_database.loadGeojson(new URL("../couches/eau-potable.geojson", import.meta.url), {batch: true}), ]) default_database.buildIndex() return default_database } } const SLEEPING_SYNONYMS = [ "dodo", "dortoir", "mimir", "dormir", "lit" ] const TOILETTES_SYNONYMS = [ "caca", "pipi", "cabinets", "water-closet", "latrines", "🚽", "double véssé", "chaise percée", "WC", "waters", "toilettes", "toilette", "chiotte", "chiottes", "le trône", "le trones", "pièce mystère", "backroom", "retailleau" ]