import { createStore, get, set, del, keys } from 'idb-keyval'
import OlExtElement from 'ol-ext/util/element'
import { createButton } from '../control/ControlUtils'
// ============================================================
// Exports de préconfigurations
// ============================================================
/**
* TTL illimité (100 ans) pour tous les types de ressources
* Le cache n'expire pratiquement jamais
* @constant {Object}
* @example
* viewer.addVectorTile('layer', {
* cache: { ttl: CACHE_TTL_UNLIMITED }
* })
*/
export const CACHE_TTL_UNLIMITED = {
tiles: 100 * 365 * 24 * 60 * 60 * 1000,
style: 100 * 365 * 24 * 60 * 60 * 1000,
source: 100 * 365 * 24 * 60 * 60 * 1000,
sprite: 100 * 365 * 24 * 60 * 60 * 1000,
spriteImage: 100 * 365 * 24 * 60 * 60 * 1000,
geoJson: 100 * 365 * 24 * 60 * 60 * 1000,
}
/**
* TTL d'un mois (30 jours) pour tous les types de ressources
* @constant {Object}
* @example
* viewer.addVectorTile('layer', {
* cache: { ttl: CACHE_TTL_ONE_MONTH }
* })
*/
export const CACHE_TTL_ONE_MONTH = {
tiles: 30 * 24 * 60 * 60 * 1000,
style: 30 * 24 * 60 * 60 * 1000,
source: 30 * 24 * 60 * 60 * 1000,
sprite: 30 * 24 * 60 * 60 * 1000,
spriteImage: 30 * 24 * 60 * 60 * 1000,
geoJson: 30 * 24 * 60 * 60 * 1000,
}
/**
* Stratégie cache-first pour tous les types de ressources
* Utilise le cache en priorité, fetch le réseau si absent
* @constant {Object}
* @example
* viewer.addVectorTile('layer', {
* cache: { strategy: CACHE_STRATEGY_CACHE_FIRST }
* })
*/
export const CACHE_STRATEGY_CACHE_FIRST = {
Tiles: 'cache-first',
Style: 'cache-first',
Source: 'cache-first',
Sprite: 'cache-first',
SpriteImage: 'cache-first',
GeoJSON: 'cache-first',
}
/**
* Stratégie network-first pour tous les types de ressources
* Fetch le réseau en priorité, utilise le cache en fallback si échec
* @constant {Object}
* @example
* viewer.addVectorTile('layer', {
* cache: { strategy: CACHE_STRATEGY_NETWORK_FIRST }
* })
*/
export const CACHE_STRATEGY_NETWORK_FIRST = {
Tiles: 'network-first',
Style: 'network-first',
Source: 'network-first',
Sprite: 'network-first',
SpriteImage: 'network-first',
GeoJSON: 'network-first',
}
// ============================================================
// Fonctions utilitaires
// ============================================================
/**
* Formate un nombre d'octets en format lisible
* @param {number} bytes Nombre d'octets
* @returns {string} Taille formatée (ex: "15.2 MB")
*/
function formatBytes (bytes) {
if (bytes === 0) return '0 B'
if (bytes === Infinity || !isFinite(bytes)) return '∞'
const k = 1024
const sizes = ['B', 'KB', 'MB', 'GB', 'TB']
const i = Math.floor(Math.log(bytes) / Math.log(k))
return parseFloat((bytes / Math.pow(k, i)).toFixed(2)) + ' ' + sizes[i]
}
/**
* Map des constantes TTL prédéfinies
* @private
*/
const TTL_PRESETS = {
CACHE_TTL_UNLIMITED,
CACHE_TTL_ONE_MONTH,
}
/**
* Map des constantes Strategy prédéfinies
* @private
*/
const STRATEGY_PRESETS = {
CACHE_STRATEGY_CACHE_FIRST,
CACHE_STRATEGY_NETWORK_FIRST,
}
// ============================================================
// JSDoc Typedefs
// ============================================================
/**
* @typedef {Object} TileCacheTTLOptions
* @property {number} [tiles] Durée de vie des tuiles en ms (défaut: 7 jours)
* @property {number} [style] Durée de vie du style en ms (défaut: 7 jours)
* @property {number} [source] Durée de vie de la source en ms (défaut: 7 jours)
* @property {number} [sprite] Durée de vie du sprite en ms (défaut: 7 jours)
* @property {number} [spriteImage] Durée de vie de l'image sprite en ms (défaut: 7 jours)
* @property {number} [geoJson] Durée de vie du GeoJSON en ms (défaut: 7 jours)
*/
/**
* @typedef {Object} TileCacheStrategyOptions
* @property {('cache-first'|'network-first')} [Tiles] Stratégie pour les tuiles (défaut: 'network-first')
* @property {('cache-first'|'network-first')} [Style] Stratégie pour le style (défaut: 'network-first')
* @property {('cache-first'|'network-first')} [Source] Stratégie pour la source (défaut: 'network-first')
* @property {('cache-first'|'network-first')} [Sprite] Stratégie pour les sprites (défaut: 'network-first')
* @property {('cache-first'|'network-first')} [SpriteImage] Stratégie pour les images sprite (défaut: 'network-first')
* @property {('cache-first'|'network-first')} [GeoJSON] Stratégie pour le GeoJSON (défaut: 'network-first')
*/
/**
* Options de configuration du cache IndexedDB pour les tuiles vectorielles
* @typedef {Object} TileCacheOptions
* @property {TileCacheTTLOptions|string|number} [ttl] Durées de vie par type de ressource (objet, nom de constante, ou nombre de minutes pour tous les types)
* @property {TileCacheStrategyOptions|string} [strategy] Stratégie de cache par type de ressource (objet ou nom de constante : 'CACHE_STRATEGY_CACHE_FIRST' ou 'CACHE_STRATEGY_NETWORK_FIRST')
* @property {number} [maxEntries] Nombre maximum d'entrées en cache (défaut: Infinity = illimité)
* @property {number} [quotaThreshold] Seuil de quota en % déclenchant l'éviction (défaut: 0.9 = 90%)
* @property {number} [quotaEvictionPercent] Pourcentage d'entrées à supprimer lors d'éviction quota (défaut: 0.3 = 30%)
* @property {number} [quotaCheckInterval] Vérifier le quota toutes les N écritures (défaut: 50)
* @property {boolean} [enabled] Active ou désactive le cache (défaut: true)
* @property {string} [dbName] Nom de base de la base de données IndexedDB (défaut: 'karteis-mapviewer-tiles')
*/
/**
* Gestionnaire de cache IndexedDB pour les tuiles vectorielles
* Chaque instance gère le cache d'un layer spécifique via un custom store
*/
export class TileCacheManager {
/**
* @param {string} layerId Identifiant unique du layer
* @param {TileCacheOptions} [options] Options de configuration du cache
*/
constructor (layerId, options = {}) {
this.layerId = layerId
this.enabled = false
if (options.enabled === false) {
return
}
// Créer une database spécifique pour ce layer
// idb-keyval ne supporte pas plusieurs object stores dans une même DB
// Donc on crée une database par layer : '{dbName}-layer-{idLayer}'
const baseDbName = options.dbName || 'karteis-mapviewer-tiles'
this.dbName = `${baseDbName}-layer-${layerId}`
this.store = createStore(this.dbName, 'keyval')
// Configuration des TTL par type de ressource
// Supporte un objet, une string référençant une constante, ou un number (minutes)
let ttlConfig
if (typeof options.ttl === 'string') {
// String : référence à une constante
ttlConfig = TTL_PRESETS[options.ttl] || {}
} else if (typeof options.ttl === 'number') {
// Number : minutes pour tous les types
const ttlMs = options.ttl * 60 * 1000
ttlConfig = {
tiles: ttlMs,
style: ttlMs,
source: ttlMs,
sprite: ttlMs,
spriteImage: ttlMs,
geoJson: ttlMs,
}
} else {
// Objet ou undefined
ttlConfig = options.ttl || {}
}
this.ttl = {
...CACHE_TTL_ONE_MONTH, // Valeurs par défaut
...ttlConfig,
}
this.maxEntries = options.maxEntries ?? Infinity
this.quotaThreshold = options.quotaThreshold ?? 0.9
this.quotaEvictionPercent = options.quotaEvictionPercent ?? 0.3
this.quotaCheckInterval = options.quotaCheckInterval ?? 50
// Configuration de la stratégie par type de ressource
// Supporte un objet ou une string référençant une constante
const strategyConfig = typeof options.strategy === 'string'
? (STRATEGY_PRESETS[options.strategy] || {})
: (options.strategy || {})
this.strategy = {
...CACHE_STRATEGY_NETWORK_FIRST, // Valeurs par défaut
...strategyConfig,
}
// Compteur d'écritures pour optimiser les vérifications de quota
this.writeCount = 0
this.enabled = options.enabled !== false
}
getCacheKey (url, resourceType) {
// Format: "resourceType:url"
return `${resourceType}:${url}`
}
/**
* Récupère une ressource depuis le cache
* @param {string} url URL de la ressource
* @param {string} resourceType Type de ressource
* @returns {Promise<Response|null>} Response depuis le cache ou null
*/
async getFromCache (url, resourceType) {
if (!this.enabled) return null
try {
const key = this.getCacheKey(url, resourceType)
const cached = await get(key, this.store)
if (!cached) return null
// Vérifier l'expiration
const now = Date.now()
const ttl = this.ttl[resourceType] || this.ttl.Tiles
if (now - cached.timestamp > ttl) {
await del(key, this.store)
return null
}
// Reconstruire une Response depuis le cache
return new Response(cached.data, {
status: 200,
statusText: 'OK (from cache)',
headers: new Headers(cached.headers),
})
} catch (error) {
console.warn(`Erreur lecture cache layer ${this.layerId}:`, error)
return null
}
}
/**
* Stocke une ressource dans le cache
* @param {string} url URL de la ressource
* @param {string} resourceType Type de ressource
* @param {Response} response Response à mettre en cache
*/
async putInCache (url, resourceType, response) {
if (!this.enabled) return
try {
const clonedResponse = response.clone()
const data = await clonedResponse.arrayBuffer()
const headers = {}
// Extraire les headers importants
clonedResponse.headers.forEach((value, key) => {
headers[key] = value
})
const cacheEntry = {
url,
resourceType,
data,
headers,
timestamp: Date.now(),
ttl: this.ttl[resourceType],
}
const key = this.getCacheKey(url, resourceType)
await set(key, cacheEntry, this.store)
// Incrémenter le compteur d'écritures
this.writeCount++
// Vérifier la taille du cache (avec optimisation pour le quota)
await this.evictIfNeeded()
} catch (error) {
console.warn(`Erreur mise en cache layer ${this.layerId}:`, error)
}
}
/**
* Fetch une ressource avec gestion du cache selon la stratégie configurée
* @param {string} url URL de la ressource
* @param {string} resourceType Type de ressource ('Tiles', 'Style', etc.)
* @param {Function} createRequest Fonction qui retourne Request ou Promise<Request>
* @returns {Promise<Response>} Response depuis le réseau ou le cache
*/
async fetchWithCache (url, resourceType, createRequest, throwErrorOnKoRequest = false) {
if (!this.enabled) {
// Cache désactivé, fetch direct
const request = await Promise.resolve(createRequest())
return fetch(request)
}
const strategy = this.strategy[resourceType] || 'network-first'
try {
// Stratégie cache-first
if (strategy === 'cache-first') {
const cachedResponse = await this.getFromCache(url, resourceType)
if (cachedResponse) {
return cachedResponse
}
// Pas en cache, fetch depuis le réseau
const request = await Promise.resolve(createRequest())
const response = await fetch(request)
if (response.ok) {
// Mise en cache asynchrone (pas d'attente)
this.putInCache(url, resourceType, response).catch(err => {
console.warn(`Erreur cache asynchrone layer ${this.layerId}:`, err)
})
}
if (throwErrorOnKoRequest && !response.ok) {
throw new Error(`Response status: ${response.status}`)
}
return response
}
// Stratégie network-first (par défaut)
const request = await Promise.resolve(createRequest())
const response = await fetch(request)
if (response.ok) {
// Mise en cache asynchrone
this.putInCache(url, resourceType, response).catch(err => {
console.warn(`Erreur cache asynchrone layer ${this.layerId}:`, err)
})
return response
}
// Erreur réseau ou réponse non-ok, essayer le cache en fallback
const cachedResponse = await this.getFromCache(url, resourceType)
if (cachedResponse) {
console.warn(`Layer ${this.layerId}: utilisation cache fallback pour ${resourceType} (${url})`)
return cachedResponse
}
if (throwErrorOnKoRequest && !response.ok) {
throw new Error(`Response status: ${response.status}`)
}
// Pas de cache disponible, retourner la réponse d'erreur
return response
} catch (error) {
// Exception réseau, essayer le cache en dernier recours
const cachedResponse = await this.getFromCache(url, resourceType)
if (cachedResponse) {
console.warn(`Layer ${this.layerId}: erreur réseau, utilisation cache pour ${resourceType} (${error.message})`)
return cachedResponse
}
// Pas de cache, propager l'erreur
throw error
}
}
/**
* Vérifie le quota de stockage IndexedDB
* @returns {Promise<Object>} Informations sur le quota
*/
async checkStorageQuota () {
try {
// Vérifier si l'API est disponible
if (!navigator.storage || !navigator.storage.estimate) {
return { needsEviction: false, reason: 'API non disponible' }
}
const estimate = await navigator.storage.estimate()
const usage = estimate.usage || 0
const quota = estimate.quota || Infinity
const usagePercent = quota === Infinity ? 0 : usage / quota
if (usagePercent >= this.quotaThreshold) {
return {
needsEviction: true,
reason: 'quota',
usage,
quota,
usagePercent,
}
}
return { needsEviction: false, usage, quota, usagePercent }
} catch (error) {
console.warn(`Erreur vérification quota layer ${this.layerId}:`, error)
return { needsEviction: false, error: error.message }
}
}
/**
* Éviction des entrées les plus anciennes selon maxEntries ou le quota
*/
async evictIfNeeded () {
try {
let evictionReason = null
let evictionPercent = 0.2 // 20% par défaut
let allKeys = []
if (this.maxEntries === Infinity) {
// Mode quota uniquement : vérifier le quota (pas besoin de keys() si pas d'éviction)
if (this.writeCount % this.quotaCheckInterval === 0) {
const quotaCheck = await this.checkStorageQuota()
if (quotaCheck.needsEviction) {
// Maintenant on récupère les clés pour l'éviction
allKeys = await keys(this.store)
evictionReason = 'quota'
evictionPercent = this.quotaEvictionPercent
console.log(
`Layer ${this.layerId}: quota à ${(quotaCheck.usagePercent * 100).toFixed(1)}% ` +
`(${formatBytes(quotaCheck.usage)} / ${formatBytes(quotaCheck.quota)})`
)
}
}
} else {
// Mode maxEntries : récupérer les clés et vérifier maxEntries
allKeys = await keys(this.store)
if (allKeys.length > this.maxEntries) {
evictionReason = 'maxEntries'
evictionPercent = 0.2
} else if (this.writeCount % this.quotaCheckInterval === 0) {
// Vérifier aussi le quota si maxEntries pas dépassé
const quotaCheck = await this.checkStorageQuota()
if (quotaCheck.needsEviction) {
evictionReason = 'quota'
evictionPercent = this.quotaEvictionPercent
console.log(
`Layer ${this.layerId}: quota à ${(quotaCheck.usagePercent * 100).toFixed(1)}% ` +
`(${formatBytes(quotaCheck.usage)} / ${formatBytes(quotaCheck.quota)})`
)
}
}
}
// Pas besoin d'éviction
if (!evictionReason) {
return
}
// Effectuer l'éviction LRU
const entries = await Promise.all(
allKeys.map(async key => ({
key,
entry: await get(key, this.store),
}))
)
// Trier par timestamp (plus ancien en premier)
entries.sort((a, b) => a.entry.timestamp - b.entry.timestamp)
// Supprimer le pourcentage configuré des entrées les plus anciennes
const toDelete = entries.slice(0, Math.floor(entries.length * evictionPercent))
await Promise.all(toDelete.map(({ key }) => del(key, this.store)))
console.log(
`Cache layer ${this.layerId}: ${toDelete.length} entrées supprimées ` +
`(éviction ${evictionReason}, ${(evictionPercent * 100).toFixed(0)}% du cache)`
)
} catch (error) {
console.warn(`Erreur éviction cache layer ${this.layerId}:`, error)
}
}
/**
* Vide le cache pour ce layer
* @param {string} [resourceType] Type de ressource optionnel (vide tout si non spécifié)
*/
async clearCache (resourceType) {
try {
const allKeys = await keys(this.store)
if (resourceType) {
// Supprimer uniquement les clés d'un type spécifique
const keysToDelete = allKeys.filter(k => k.startsWith(`${resourceType}:`))
await Promise.all(keysToDelete.map(key => del(key, this.store)))
console.log(`Cache layer ${this.layerId}: ${keysToDelete.length} entrées ${resourceType} supprimées`)
} else {
// Supprimer toutes les clés du store
await Promise.all(allKeys.map(key => del(key, this.store)))
console.log(`Cache layer ${this.layerId}: cache complet supprimé (${allKeys.length} entrées)`)
}
} catch (error) {
console.warn(`Erreur clear cache layer ${this.layerId}:`, error)
}
}
/**
* Obtient les statistiques du cache pour ce layer
* @returns {Promise<Object>} Statistiques du cache
*/
async getCacheStats () {
try {
const allKeys = await keys(this.store)
const quotaInfo = await this.checkStorageQuota()
const stats = {
layerId: this.layerId,
total: allKeys.length,
maxEntries: this.maxEntries,
byType: {},
oldestEntry: null,
newestEntry: null,
}
// Ajouter les infos de quota si disponibles
if (quotaInfo.usage !== undefined) {
stats.quota = {
usage: quotaInfo.usage,
quota: quotaInfo.quota,
usagePercent: quotaInfo.usagePercent,
available: quotaInfo.quota !== Infinity ? quotaInfo.quota - quotaInfo.usage : Infinity,
usageFormatted: formatBytes(quotaInfo.usage),
quotaFormatted: formatBytes(quotaInfo.quota),
availableFormatted: formatBytes(
quotaInfo.quota !== Infinity ? quotaInfo.quota - quotaInfo.usage : Infinity
),
}
}
// Compter par type
for (const key of allKeys) {
const [type] = key.split(':')
stats.byType[type] = (stats.byType[type] || 0) + 1
}
// Trouver les entrées les plus anciennes/récentes
if (allKeys.length > 0) {
const entries = await Promise.all(
allKeys.slice(0, Math.min(100, allKeys.length)).map(async key => {
const entry = await get(key, this.store)
return { key, timestamp: entry?.timestamp }
})
)
const sorted = entries.filter(e => e.timestamp).sort((a, b) => a.timestamp - b.timestamp)
if (sorted.length > 0) {
stats.oldestEntry = new Date(sorted[0].timestamp).toISOString()
stats.newestEntry = new Date(sorted[sorted.length - 1].timestamp).toISOString()
}
}
return stats
} catch (error) {
console.warn(`Erreur stats cache layer ${this.layerId}:`, error)
return { layerId: this.layerId, error: error.message }
}
}
/**
* Supprime complètement la database d'un layer
* @param {string} layerId Identifiant du layer
* @param {string} [baseDbName] Nom de base de la base de données (défaut: 'karteis-mapviewer-tiles')
*/
static async deleteLayerStore (layerId, baseDbName = 'karteis-mapviewer-tiles') {
try {
const dbName = `${baseDbName}-layer-${layerId}`
return new Promise((resolve, reject) => {
const request = indexedDB.deleteDatabase(dbName)
request.onsuccess = () => {
console.log(`Database layer ${layerId} supprimée`)
resolve()
}
request.onerror = () => {
console.warn(`Erreur suppression database layer ${layerId}:`, request.error)
reject(request.error)
}
request.onblocked = () => {
console.warn(`Suppression database layer ${layerId} bloquée (connexions ouvertes)`)
}
})
} catch (error) {
console.warn(`Erreur suppression database layer ${layerId}:`, error)
}
}
/**
* Liste tous les layers ayant un cache
* @param {string} [baseDbName] Nom de base de la base de données (défaut: 'karteis-mapviewer-tiles')
* @returns {Promise<string[]>} Liste des identifiants de layers en cache
*/
static async listCachedLayers (baseDbName = 'karteis-mapviewer-tiles') {
try {
// indexedDB.databases() retourne toutes les databases
if (!indexedDB.databases) {
console.warn('indexedDB.databases() non disponible dans ce navigateur')
return []
}
const databases = await indexedDB.databases()
const prefix = `${baseDbName}-layer-`
// Extraire les layer IDs depuis les noms de databases
const layerIds = databases
.filter(db => db.name && db.name.startsWith(prefix))
.map(db => db.name.replace(prefix, ''))
return layerIds
} catch (error) {
console.warn('Erreur listage layers en cache:', error)
return []
}
}
/**
* Affiche une interface de gestion du cache
* @param {HTMLElement} target Élément cible où ajouter le panneau
*/
async showCacheManagerDialog (target) {
// Récupérer les statistiques
const stats = await this.getCacheStats()
// Créer le conteneur principal
const dialog = OlExtElement.create('DIV', {
className: 'kmapv-cache-manager-dialog',
style: {
position: 'absolute',
top: '10px',
right: '10px',
backgroundColor: 'white',
borderRadius: '8px',
padding: '15px',
maxWidth: '500px',
maxHeight: '80vh',
overflowY: 'auto',
boxShadow: '0 4px 20px rgba(0, 0, 0, 0.3)',
zIndex: '1000',
},
})
// Titre avec bouton de fermeture
const titleContainer = OlExtElement.create('DIV', {
style: {
display: 'flex',
justifyContent: 'space-between',
alignItems: 'center',
marginBottom: '15px',
borderBottom: '2px solid #e0e0e0',
paddingBottom: '10px',
},
})
const title = OlExtElement.create('H3', {
text: `Gestion du cache - ${stats.layerId}`,
style: {
margin: '0',
color: '#333',
fontSize: '16px',
fontWeight: 'bold',
},
})
const closeButton = OlExtElement.create('i', {
className: 'kmapv-icon kmapv-icon-cancel',
style: {
cursor: 'pointer',
},
on: {
click: () => {
dialog.remove()
},
},
title: 'Fermer',
})
titleContainer.appendChild(title)
titleContainer.appendChild(closeButton)
dialog.appendChild(titleContainer)
// Statistiques globales
const globalStats = OlExtElement.create('DIV', {
style: {
marginBottom: '10px',
padding: '10px',
backgroundColor: '#f5f5f5',
borderRadius: '4px',
},
})
globalStats.innerHTML = `
<strong>Statistiques globales :</strong><br/>
Total d'entrées : <strong>${stats.total}</strong><br/>
Limite : <strong>${stats.maxEntries === Infinity ? 'Illimitée' : stats.maxEntries}</strong>
`
dialog.appendChild(globalStats)
// Statistiques par type
if (Object.keys(stats.byType).length > 0) {
const typeStats = OlExtElement.create('DIV', {
style: {
marginBottom: '10px',
padding: '10px',
backgroundColor: '#f5f5f5',
borderRadius: '4px',
},
})
let typeHtml = '<strong>Par type de ressource :</strong><br/>'
for (const [type, count] of Object.entries(stats.byType)) {
typeHtml += ` - ${type} : <strong>${count}</strong> entrée(s)<br/>`
}
typeStats.innerHTML = typeHtml
dialog.appendChild(typeStats)
}
// Informations de quota
if (stats.quota) {
const quotaStats = OlExtElement.create('DIV', {
style: {
marginBottom: '10px',
padding: '10px',
backgroundColor: '#e8f5e9',
borderRadius: '4px',
},
})
quotaStats.innerHTML = `
<strong>Quota IndexedDB :</strong><br/>
Utilisation : <strong>${stats.quota.usageFormatted}</strong> / ${stats.quota.quotaFormatted}<br/>
Pourcentage : <strong>${(stats.quota.usagePercent * 100).toFixed(1)}%</strong><br/>
Disponible : <strong>${stats.quota.availableFormatted}</strong>
`
dialog.appendChild(quotaStats)
}
// Dates des entrées
if (stats.oldestEntry && stats.newestEntry) {
const dateStats = OlExtElement.create('DIV', {
style: {
marginBottom: '10px',
padding: '10px',
backgroundColor: '#f5f5f5',
borderRadius: '4px',
fontSize: '12px',
},
})
dateStats.innerHTML = `
<strong>Plage temporelle :</strong><br/>
Plus ancienne : ${new Date(stats.oldestEntry).toLocaleString()}<br/>
Plus récente : ${new Date(stats.newestEntry).toLocaleString()}
`
dialog.appendChild(dateStats)
}
// Conteneur des boutons
const buttonsContainer = OlExtElement.create('DIV', {
style: {
display: 'flex',
gap: '10px',
justifyContent: 'flex-end',
marginTop: '15px',
},
})
// Bouton effacer le cache
const clearButton = createButton(
'<i class="kmapv-icon kmapv-icon-clear"></i><span>Effacer le cache</span>',
async () => {
if (confirm(`Êtes-vous sûr de vouloir effacer tout le cache du layer "${stats.layerId}" ?`)) {
await this.clearCache()
dialog.remove()
alert('Cache effacé avec succès !')
}
}
)
clearButton.title = 'Effacer tout le cache de ce layer'
buttonsContainer.appendChild(clearButton)
dialog.appendChild(buttonsContainer)
// Ajouter à l'élément de la carte
target.appendChild(dialog)
}
}