Skip to main content
This guide shows how to display GeoPackage data on a Leaflet map — both raster tiles from a tile table and vector features from a feature table.
A fully working demo is available at the GeoPackage Viewer, which is built on top of this library.

Prerequisites

npm install leaflet @ngageoint/geopackage
Include the Leaflet CSS in your HTML:
<link rel="stylesheet" href="https://unpkg.com/leaflet/dist/leaflet.css" />

Add a GeoPackage tile layer

Leaflet’s L.TileLayer fetches tiles by calling a URL template. To serve tiles from a GeoPackage, create a custom tile layer that calls GeoPackageTileRetriever.getTile() for each tile request.
import L from 'leaflet';
import {
  GeoPackageManager,
  GeoPackageTileRetriever,
  setSqljsWasmLocateFile,
} from '@ngageoint/geopackage';

sqljsWasmLocateFile(file => 'public/' + file);

const map = L.map('map').setView([0, 0], 2);

// Open the GeoPackage
const geoPackage = await GeoPackageManager.open(gpkgByteArray);
const tileDao = geoPackage.getTileDao('my_tiles');
const retriever = new GeoPackageTileRetriever(tileDao, 256, 256);

// Custom GridLayer that serves tiles from the GeoPackage
const GeoPackageLayer = L.GridLayer.extend({
  createTile(coords, done) {
    const tile = document.createElement('canvas');
    tile.width = 256;
    tile.height = 256;

    retriever.getTile(coords.x, coords.y, coords.z).then(gpTile => {
      if (!gpTile) {
        done(null, tile); // empty tile
        return;
      }
      gpTile.getGeoPackageImage().then(gpImage => {
        const ctx = tile.getContext('2d');
        ctx.drawImage(gpImage.getImage(), 0, 0);
        done(null, tile);
      });
    }).catch(err => done(err));

    return tile;
  },
});

new GeoPackageLayer().addTo(map);
In the browser the canvas adapter is backed by the native HTML5 Canvas API, so there is no need to call Canvas.disposeImage() or Canvas.disposeCanvas() — only do that in Node.js.

Add features as a GeoJSON layer

Use geoPackage.queryForGeoJSONFeatures() to read all features and pass the resulting FeatureCollection to L.geoJSON().
// Collect all features from the table
const featureResultSet = geoPackage.queryForGeoJSONFeatures('my_features');
const features = [];
try {
  for (const feature of featureResultSet) {
    features.push(feature);
  }
} finally {
  featureResultSet.close();
}

// Add as a Leaflet GeoJSON layer
const featureLayer = L.geoJSON(
  { type: 'FeatureCollection', features },
  {
    style: feature => ({
      color: '#0055ff',
      weight: 2,
      opacity: 0.8,
    }),
    onEachFeature: (feature, layer) => {
      if (feature.properties) {
        layer.bindPopup(
          Object.entries(feature.properties)
            .map(([k, v]) => `<b>${k}</b>: ${v}`)
            .join('<br>'),
        );
      }
    },
  },
).addTo(map);

// Zoom the map to the features
if (featureLayer.getLayers().length) {
  map.fitBounds(featureLayer.getBounds());
}

Spatial filtering before adding to the map

For large feature tables, filter to the current map viewport before loading features to avoid transferring unnecessary data:
import { BoundingBox } from '@ngageoint/geopackage';

map.on('moveend', () => {
  const bounds = map.getBounds();
  const bbox = new BoundingBox(
    bounds.getWest(),
    bounds.getSouth(),
    bounds.getEast(),
    bounds.getNorth(),
  );

  const resultSet = geoPackage.queryForGeoJSONFeatures('my_features', bbox);
  const features = [];
  try {
    for (const feature of resultSet) {
      features.push(feature);
    }
  } finally {
    resultSet.close();
  }

  featureLayer.clearLayers();
  featureLayer.addData({ type: 'FeatureCollection', features });
});

Complete example

<!DOCTYPE html>
<html lang="en">
<head>
  <meta charset="UTF-8">
  <title>GeoPackage + Leaflet</title>
  <link rel="stylesheet" href="https://unpkg.com/leaflet/dist/leaflet.css" />
  <script src="/path/to/geopackage.min.js"></script>
  <style>
    #map { height: 100vh; }
  </style>
</head>
<body>
  <input id="fileInput" type="file" accept=".gpkg" />
  <div id="map"></div>

  <script src="https://unpkg.com/leaflet/dist/leaflet.js"></script>
  <script>
    const {
      GeoPackageManager,
      GeoPackageTileRetriever,
      setSqljsWasmLocateFile,
      BoundingBox,
    } = window.GeoPackage;

    setSqljsWasmLocateFile(file => 'public/' + file);

    const map = L.map('map').setView([20, 0], 2);

    // Add an OpenStreetMap basemap
    L.tileLayer('https://{s}.tile.openstreetmap.org/{z}/{x}/{y}.png', {
      attribution: '© OpenStreetMap contributors',
    }).addTo(map);

    document.getElementById('fileInput').addEventListener('change', function () {
      const reader = new FileReader();
      reader.onload = async function () {
        const array = new Uint8Array(reader.result);
        const geoPackage = await GeoPackageManager.open(array);

        // Add tile layers
        for (const tableName of geoPackage.getTileTables()) {
          const tileDao = geoPackage.getTileDao(tableName);
          const retriever = new GeoPackageTileRetriever(tileDao, 256, 256);

          L.GridLayer.extend({
            createTile(coords, done) {
              const tile = document.createElement('canvas');
              tile.width = tile.height = 256;
              retriever.getTile(coords.x, coords.y, coords.z).then(gpTile => {
                if (!gpTile) return done(null, tile);
                gpTile.getGeoPackageImage().then(img => {
                  tile.getContext('2d').drawImage(img.getImage(), 0, 0);
                  done(null, tile);
                });
              }).catch(err => done(err));
              return tile;
            },
          }).extend({})().addTo(map);
        }

        // Add feature layers as GeoJSON
        for (const tableName of geoPackage.getFeatureTables()) {
          const resultSet = geoPackage.queryForGeoJSONFeatures(tableName);
          const features = [];
          try {
            for (const f of resultSet) features.push(f);
          } finally {
            resultSet.close();
          }
          L.geoJSON({ type: 'FeatureCollection', features }).addTo(map);
        }
      };
      reader.readAsArrayBuffer(this.files[0]);
    });
  </script>
</body>
</html>

Build docs developers (and LLMs) love