import lunr from "./lib/lunr/lunr.js" import lunrStemmer from "./lib/lunr/lunr.stemmer.support.js" import lunrFr from "./lib/lunr/lunr.fr.js" import { MapSymbol } from "./symbols.js" lunrStemmer(lunr) lunrFr(lunr) const FEATURE_INDEX = 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 { /** @type {Object} */ featuresById = {} /** @type {MapFeature[]} */ featuresShownOnEmptyMap = [] 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 feature = geojson; if(!(feature instanceof MapFeature)){ feature = new MapFeature(geojson) } if(!feature.id){ let full_id = idPrefix+"#"+(geojson?.properties?.id || geojson?.properties?.[FEATURE_INDEX]) feature.id = full_id } if(this.featuresById[feature.id]){ console.warn(`Warning: a feature with ID "${feature.id}" already exists in database`) } if(!feature.geometry){ console.warn(`Geometry ${feature.id} does not have a geometry, it will not be included`) return } this.featuresById[feature.id] = feature let event = new Event("newfeature") event.ref = feature.id event.feature = feature this.dispatchEvent(event) all_ids.push(feature.id) } else if(geojson.type == "FeatureCollection") { if(geojson.name){ idPrefix += (idPrefix != "" ? "-" : "")+geojson.name } if(geojson?.properties?.[FEATURE_INDEX]){ idPrefix += (idPrefix != "" ? "-" : "")+geojson.properties[FEATURE_INDEX] } for(let [i, feature] of Object.entries(geojson.features)){ feature.properties = { ...(feature.properties || {}), [FEATURE_INDEX]: 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.featuresShownOnEmptyMap = [] let showOnEmptyMap = this.featuresShownOnEmptyMap this.fullTextIndex = lunr(function(){ this.ref("id") this.field("name") this.field("synonyms") for(let [id, feature] of Object.entries(database.featuresById)){ let sym = feature.mapSymbol; let synonyms = [] if(sym.genericName){ synonyms.push(sym.genericName) } if(sym.indexSynonyms){ synonyms.push(...sym.indexSynonyms) } if(feature.properties["show-on-empty-map"]){ showOnEmptyMap.push(feature) } this.add({ id, name: feature.properties.name || sym.genericName, synonyms }) } }) this.dispatchEvent(new Event("indexbuild")) } static async createDefault(){ let default_database = new PlaceDatabase() await Promise.all([ default_database.loadGeojson(new URL("../couches/aire-de-jeux.geojson", import.meta.url), {batch: true}), default_database.loadGeojson(new URL("../couches/autogestion.geojson", import.meta.url), {batch: true}), default_database.loadGeojson(new URL("../couches/bac-a-sable.geojson", import.meta.url), {batch: true}), default_database.loadGeojson(new URL("../couches/batiments.geojson", import.meta.url), {batch: true}), default_database.loadGeojson(new URL("../couches/braseros.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/dons.geojson", import.meta.url), {batch: true}), default_database.loadGeojson(new URL("../couches/eau-potable.geojson", import.meta.url), {batch: true}), default_database.loadGeojson(new URL("../couches/espace-camp.geojson", import.meta.url), {batch: true}), default_database.loadGeojson(new URL("../couches/espacemiam.geojson", import.meta.url), {batch: true}), default_database.loadGeojson(new URL("../couches/marabouts.geojson", import.meta.url), {batch: true}), default_database.loadGeojson(new URL("../couches/medic.geojson", import.meta.url), {batch: true}), default_database.loadGeojson(new URL("../couches/parkings.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/piscine.geojson", import.meta.url), {batch: true}), default_database.loadGeojson(new URL("../couches/poubelles.geojson", import.meta.url), {batch: true}), default_database.loadGeojson(new URL("../couches/scenes.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/wifi.geojson", import.meta.url), {batch: true}), default_database.loadGeojson(new URL("../couches/zones-interdite.geojson", import.meta.url), {batch: true}), ]) default_database.buildIndex() return default_database } } const POINT_FEATURE = Symbol("Point feature") export class MapFeature { constructor(geojson){ Object.assign(this, geojson) } get id(){ return this[FEATURE_ID] } set id(value){ this[FEATURE_ID] = value } get mapSymbol(){ return MapSymbol.fromFeature(this) } asPoint(){ if(!this[POINT_FEATURE]){ if(this.geometry.type != "Point"){ let point_feature = turf.centerOfMass(this) point_feature.properties = this.properties point_feature = new MapSubFeature(point_feature, this) this[POINT_FEATURE] = point_feature } else { this[POINT_FEATURE] = this } } return this[POINT_FEATURE] } } const PARENT_FEATURE = Symbol("Parent feature") export class MapSubFeature extends MapFeature { constructor(geojson, parent){ super(geojson) this[PARENT_FEATURE] = parent } get parentFeature(){ return this[PARENT_FEATURE] } }