Vector Tiles avec cache IndexedDB

Retour

Cette démonstration montre l'utilisation du système de cache IndexedDB pour les calques vectoriels tuilés. Le cache permet de :

  • ✅ Réduire drastiquement la bande passante
  • ✅ Améliorer les performances de chargement
  • ✅ Permettre une utilisation hors-ligne après premier chargement
  • ✅ Gérer finement les stratégies de cache par type de ressource

Instructions :

  1. Ouvrez les DevTools (F12) → Application → IndexedDB
  2. Naviguez sur la carte pour déclencher le chargement de tuiles
  3. Observez les databases qui se créent : karteis-mapviewer-tiles-layer-vector-cached
  4. Utilisez les boutons pour gérer le cache
Démo
{
  "jsLib": [
    "https://karteis.sct.saas-gfi.eu/dist/mapviewer/latest/karteis-mapviewer-var.js"
  ]
}
<!-- Ne pas utiliser dans votre code -->
<!-- La déclaration du css n'est utile que pour l'affichage sur le site d'exemples -->
<link rel="stylesheet" href="/mapviewer/mapviewer.css">
<!-- Ne pas utiliser dans votre code -->

<!-- Dans nos exemple on utilise font-awesome pour afficher les icones de boutons -->
<link rel="stylesheet" href="https://cdnjs.cloudflare.com/ajax/libs/font-awesome/6.4.0/css/all.min.css">
<form id="formbackground" style="margin-bottom: 5px;">
  <input name="url" placeholder="URL de demande de token geoservice" autocomplete="current-url"/>
  <input name="username" placeholder="Nom d'utilisateur" autocomplete="current-username"/>
  <input name="password" placeholder="Mot de passe" type="password" autocomplete="current-password"/>
  <button type="submit" title="Fond de plan tuilé protégé"><i class="fas fa-plus"></i></button>
</form>
<div style="margin-bottom: 5px;">
  <button id="btnStats" title="Afficher les statistiques du cache">
    <i class="fas fa-chart-bar"></i> Statistiques du cache
  </button>
  <button id="btnClearTiles" title="Vider uniquement le cache des tuiles">
    <i class="fas fa-eraser"></i> Vider les tuiles
  </button>
  <button id="btnClearAll" title="Vider tout le cache du layer">
    <i class="fas fa-trash"></i> Vider tout le cache
  </button>
  <button id="btnCleanup" title="Nettoyer les caches orphelins">
    <i class="fas fa-broom"></i> Nettoyer orphelins
  </button>
  <button id="btnManageCache" title="Ouvrir la fenêtre de gestion du cache">
    <i class="fas fa-cog"></i> Gérer le cache
  </button>
</div>

<div id="info" style="background: #f0f0f0; padding: 10px; margin-bottom: 10px; font-family: monospace; font-size: 12px; display: none;">
  <!-- Les statistiques s'afficheront ici -->
</div>

<!-- Le div qui sera utilisé pour injecter la carte -->
<div id="kmap" style="height:450px;" />
// Initialisation de la carte avec ses plugins
const kmapvInstance = (new kmapv.Mapviewer({
    mapdiv: document.getElementById('kmap'),
    projection: 'EPSG:3857',
    defaultCenter: [260000, 5950000],
    defaultZoom: 8,
    minZoom: 5,
    maxZoom: 18,
}))
 .use(kmapv.CommonLayer({}))
 .use(kmapv.BackgroundLayer({}))

// Ajout du layermanager
kmapvInstance.horizontalToolbar.addLayerManagerTool({
  reordering: false
})

// Création de groupe
kmapvInstance.commonLayer.addGroup("FondDePlan", {
  title: "Fonds de plan",
  zIndex: 0
})

// 1. Fond OSM de base (sans cache pour comparaison)
kmapvInstance.backgroundLayer.addBackgroundLayer(
  'background-osm',
  'osm',
  {
    idGroup: "FondDePlan",
    title: 'OSM (sans cache)',
    zIndex: 0,
    visible: true,
    baseLayer: true
  }
)

// 2. Vector Tile avec cache configuré (utilise un style OpenMapTiles)

const form = document.getElementById("formbackground");
form.addEventListener("submit", addProtectedBackgrounds);

async function addProtectedBackgrounds(e) {
  // empêche de recharger
  e.stopPropagation();
  e.preventDefault();
  // converti le formulaire en objet {username, password}
  const formData = new FormData(e.target);
  const {url,...auth} = Object.fromEntries([...formData.entries()])
await kmapvInstance.backgroundLayer.addBackgroundLayer(
    'vector-cached', // identifiant
    'vectorTile', // type
    {
      idGroup: 'FondDePlan', // groupe ou ajouter le calque
      title: 'OSM Map Tile', // libellé
      zIndex: 1, // z-order
      visible: false,
      baseLayer: true, // les calques avec baseLayer = true dans un même groupe s'affiche avec un radiobutton (un seul a la fois)
      styleUrl: 'https://services.geosphere.fr/maps/tiles/styles/osm-openmaptiles/style.json',
      identity: {
          id:"geoservice",
          type: 'url',
          method:'get',
          url,
          auth,
          apply: {
            name:'jwt',
          },
          refreshInterval: 10000
        },
        cache: {
          strategy: kmapv.CACHE_STRATEGY_CACHE_FIRST, // stratégie de cache par défaut pour tous les types
        }
      })
}


// kmapvInstance.backgroundLayer.addVectorTile(
//   'vector-cached',
//   {
//     idGroup: "FondDePlan",
//     title: 'OpenMapTiles (avec cache)',
//     styleUrl: 'https://api.maptiler.com/maps/streets/style.json?key=get_your_own_key',
//     visible: true,
//     baseLayer: true,

//     // Configuration du cache
//     cache: {
//       enabled: true, // Active le cache (par défaut)

//       // Valeurs par défaut (si non spécifiées) :
//       // - TTL: 7 jours pour tous les types de ressources
//       // - Stratégie: cache-first pour tous les types
//       // - maxEntries: 1000

//       // Exemple de personnalisation (optionnel) :
//       ttl: {
//         tiles: 14 * 24 * 60 * 60 * 1000,    // 14 jours (au lieu de 7)
//       },

//       strategy: {
//         Style: 'network-first',     // Réseau en priorité pour le style (au lieu de cache-first)
//         Source: 'network-first',    // Réseau en priorité pour la source (au lieu de cache-first)
//       },

//       // Nombre maximum d'entrées dans le cache
//       maxEntries: 2000,
//     }
//   }
// )
const btnStats = document.getElementById('btnStats')
const btnClearTiles = document.getElementById('btnClearTiles')
const btnClearAll = document.getElementById('btnClearAll')
const btnCleanup = document.getElementById('btnCleanup')
const btnManageCache = document.getElementById('btnManageCache')
const infoDiv = document.getElementById('info')
btnStats.addEventListener('click', async () => {
  infoDiv.style.display = 'block'
  infoDiv.innerHTML = '<i>Chargement des statistiques...</i>'

  try {
    const stats = await kmapvInstance.backgroundLayer.getLayerCacheStats('vector-cached')

    if (stats) {
      let html = `<strong>Statistiques du cache - Layer: ${stats.layerId}</strong><br>`
      html += `Total d'entrées: <strong>${stats.total}</strong><br><br>`

      if (stats.total > 0) {
        html += '<strong>Par type de ressource:</strong><br>'
        for (const [type, count] of Object.entries(stats.byType)) {
          html += `  - ${type}: ${count} entrée(s)<br>`
        }

        if (stats.oldestEntry) {
          html += `<br>Plus ancienne: ${new Date(stats.oldestEntry).toLocaleString()}<br>`
          html += `Plus récente: ${new Date(stats.newestEntry).toLocaleString()}<br>`
        }
      } else {
        html += '<em>Cache vide. Naviguez sur la carte pour charger des tuiles.</em>'
      }

      infoDiv.innerHTML = html
    } else {
      infoDiv.innerHTML = '<em>Aucun cache trouvé pour ce layer.</em>'
    }
  } catch (error) {
    infoDiv.innerHTML = `<span style="color:red">Erreur: ${error.message}</span>`
  }
})

// Vider uniquement les tuiles
btnClearTiles.addEventListener('click', async () => {
  if (confirm('Voulez-vous vraiment vider le cache des tuiles ?')) {
    infoDiv.style.display = 'block'
    infoDiv.innerHTML = '<i>Nettoyage des tuiles en cours...</i>'

    try {
      await kmapvInstance.backgroundLayer.clearLayerCache('vector-cached', 'Tiles')
      infoDiv.innerHTML = '<span style="color:green">✓ Cache des tuiles vidé avec succès!</span>'
      setTimeout(() => { infoDiv.style.display = 'none' }, 3000)
    } catch (error) {
      infoDiv.innerHTML = `<span style="color:red">Erreur: ${error.message}</span>`
    }
  }
})

// Vider tout le cache du layer
btnClearAll.addEventListener('click', async () => {
  if (confirm('Voulez-vous vraiment vider tout le cache de ce layer ?')) {
    infoDiv.style.display = 'block'
    infoDiv.innerHTML = '<i>Nettoyage complet en cours...</i>'

    try {
      await kmapvInstance.backgroundLayer.clearLayerCache('vector-cached')
      infoDiv.innerHTML = '<span style="color:green">✓ Cache complet vidé avec succès!</span>'
      setTimeout(() => { infoDiv.style.display = 'none' }, 3000)
    } catch (error) {
      infoDiv.innerHTML = `<span style="color:red">Erreur: ${error.message}</span>`
    }
  }
})

// Nettoyer les caches orphelins
btnCleanup.addEventListener('click', async () => {
  infoDiv.style.display = 'block'
  infoDiv.innerHTML = '<i>Recherche des caches orphelins...</i>'

  try {
    const orphans = await kmapvInstance.backgroundLayer.cleanupOrphanCaches()

    if (orphans.length > 0) {
      infoDiv.innerHTML = `<span style="color:green">✓ ${orphans.length} cache(s) orphelin(s) nettoyé(s):<br>${orphans.join(', ')}</span>`
    } else {
      infoDiv.innerHTML = '<span style="color:green">✓ Aucun cache orphelin trouvé.</span>'
    }

    setTimeout(() => { infoDiv.style.display = 'none' }, 5000)
  } catch (error) {
    infoDiv.innerHTML = `<span style="color:red">Erreur: ${error.message}</span>`
  }
})

// Ouvrir la fenêtre de gestion du cache
btnManageCache.addEventListener('click', async () => {
  try {
    await kmapvInstance.backgroundLayer.showCacheManagerDialog('vector-cached')
    // Rafraîchir les stats après fermeture de la dialog
    if (infoDiv.style.display !== 'none') {
      btnStats.click()
    }
  } catch (error) {
    infoDiv.style.display = 'block'
    infoDiv.innerHTML = `<span style="color:red">Erreur: ${error.message}</span>`
  }
})


console.log('💡 Astuce: Ouvrez les DevTools (F12) → Application → IndexedDB pour voir les databases de cache en temps réel (ex: karteis-mapviewer-tiles-layer-vector-cached)')
#formbackground {
  display: flex;
  align-items: center;
  gap: 8px;
  flex-wrap: wrap;
}

#formbackground input {
  padding: 8px 12px;
  border: 1px solid #ccc;
  border-radius: 4px;
  font-size: 12px;
  flex: 1;
  min-width: 150px;
}

#formbackground input:focus {
  outline: none;
  border-color: #0066cc;
  box-shadow: 0 0 0 2px rgba(0, 102, 204, 0.1);
}

button {
  padding: 8px 12px;
  margin-right: 5px;
  border: 1px solid #ccc;
  background: white;
  cursor: pointer;
  border-radius: 4px;
  font-size: 12px;
}

button:hover {
  background: #f0f0f0;
}

button i:not(:last-child) {
  margin-right: 5px;
}

Notes importantes

Préconfigurations disponibles

Le module exporte des constantes pour simplifier la configuration :

// Import dans votre code
import {
  CACHE_TTL_UNLIMITED,         // Objet TTL 100 ans pour tous les types
  CACHE_TTL_ONE_MONTH,         // Objet TTL 30 jours pour tous les types
  CACHE_STRATEGY_CACHE_FIRST,  // cache-first pour tous
  CACHE_STRATEGY_NETWORK_FIRST // network-first pour tous
} from '@karteis/mapviewer'

// Utilisation
kmapvInstance.backgroundLayer.addVectorTile('my-layer', {
  styleUrl: 'https://...',
  cache: {
    ttl: CACHE_TTL_UNLIMITED,                // Tous les types → 100 ans
    strategy: CACHE_STRATEGY_NETWORK_FIRST,  // Toujours à jour
  },
})

// Ou par string (sans import)
kmapvInstance.backgroundLayer.addVectorTile('my-layer', {
  styleUrl: 'https://...',
  cache: {
    ttl: 'CACHE_TTL_UNLIMITED',                // String
    strategy: 'CACHE_STRATEGY_NETWORK_FIRST',  // String
  },
})

// Ou par nombre de minutes (ultra-simplifié)
kmapvInstance.backgroundLayer.addVectorTile('my-layer', {
  styleUrl: 'https://...',
  cache: {
    ttl: 60,  // 60 minutes pour tous les types
  },
})

Astuce : Les options ttl et strategy acceptent plusieurs formats :

  • ttl : objet, string ('CACHE_TTL_UNLIMITED', 'CACHE_TTL_ONE_MONTH'), ou number (minutes)
  • strategy : objet ou string ('CACHE_STRATEGY_CACHE_FIRST', 'CACHE_STRATEGY_NETWORK_FIRST')

Structure du cache dans IndexedDB

Le cache est organisé comme suit :

  • Une database par layer : {baseDbName}-layer-{idLayer} (ex: karteis-mapviewer-tiles-layer-my-layer)
  • Object Store : keyval (store par défaut d'idb-keyval)
  • Clés : Format {ResourceType}:{url} (ex: Tiles:https://.../{z}/{x}/{y}.pbf)

Types de ressources cachés

  • Tiles : Les tuiles vectorielles (.pbf)
  • Style : Le fichier de style MapBox (.json)
  • Source : Les métadonnées de source
  • Sprite : Les spritesheets de symboles
  • SpriteImage : Les images des sprites
  • GeoJSON : Les données GeoJSON

Stratégies de cache

network-first (défaut pour tous les types)

1. Cherche dans le cache
2. Si trouvé ET non expiré → retourne depuis le cache
3. Sinon → fetch depuis le réseau
4. Met en cache pour la prochaine fois

network-first (optionnel pour données très dynamiques)

1. Fetch depuis le réseau
2. Si succès → met en cache et retourne
3. Si échec → cherche dans le cache comme fallback
4. Si pas en cache → propage l'erreur

Inspection avec DevTools

Pour inspecter le cache :

  1. F12 → Application (Chrome) ou Storage (Firefox)
  2. IndexedDB → karteis-mapviewer-tiles
  3. Cliquer sur layer-vector-cached
  4. Voir toutes les entrées avec leur contenu

Vous pouvez :

  • Voir les URLs mises en cache
  • Inspecter le contenu (ArrayBuffer)
  • Voir les timestamps et TTL
  • Supprimer manuellement des entrées

Désactiver le cache

Pour désactiver le cache sur un layer :

kmapvInstance.backgroundLayer.addVectorTile('my-layer', {
  styleUrl: 'https://...',
  cache: {
    enabled: false, // Cache désactivé
  },
})

Avantages du cache

  • 📉 Réduction de bande passante : 60-90% après premier chargement
  • ⚡ Performances : Chargement instantané depuis IndexedDB (~1ms)
  • 🔌 Mode hors-ligne : Fonctionne sans réseau après mise en cache
  • 💰 Économie de coûts : Moins de requêtes API = moins de frais

Cas d'usage

Application grand public : Les défauts (7 jours, cache-first) sont optimaux. Augmenter TTL à 30 jours si nécessaire Application métier : Override en TTL court (1h) + network-first si données très sensibles PWA/Mobile : Augmenter TTL à 90 jours, pré-chargement des zones

Voir aussi

Dernière mise à jour:
Contributors: David Hequet