Short answer

Turning a static geological map into a web map is a four-stage pipeline: georeference the source (if it is a scan), vectorize the geological units into attributed polygons and the contacts into lines, build vector tiles (typically MVT via tippecanoe), and style and serve them with MapLibre GL so each unit is clickable, queryable, and re-symbolizable. The hard part is not the rendering — it is preserving the geological semantics: unit codes, contact certainty, stratigraphic order, and the legend, so the interactive map says exactly what the printed sheet said and no more.

Decide what kind of source you have

The pipeline forks immediately on the nature of the input:

  • A scanned paper sheet (raster, no attributes). You have pixels, not geology. It must be georeferenced and then either left as a raster tileset (fast, but "a picture with zoom") or vectorized (slow, but interactive). Vectorizing a scan is largely manual or semi-automated digitizing — there is no reliable button that turns coloured polygons into correct geological units.
  • A GIS dataset (Shapefile/GeoPackage with unit attributes). This is the good case: the geology is already polygons with codes. You can go almost straight to tiling.

Be honest about which you have, because it sets the effort by an order of magnitude.

Stage 1 — Georeference a scan

If the input is an image, place it in real-world coordinates using known control points (graticule intersections, surveyed corners). In QGIS the Georeferencer tool writes a world file or a GeoTIFF; on the command line:

gdal_translate -of GTiff -gcp 100 200 7.50 46.20 \
  -gcp 4000 200 7.80 46.20 -gcp 100 3000 7.50 46.00 \
  sheet_scan.png sheet_gcp.tif
gdalwarp -t_srs EPSG:4326 -r bilinear -tps \
  sheet_gcp.tif sheet_geo.tif

-tps uses a thin-plate-spline transform, which absorbs the paper distortion and scanner skew that a simple affine transform cannot. Record the RMS error of the control points; for a 1:50,000 sheet an RMS of a few tens of metres is acceptable, but anything approaching the cartographic line width should be flagged.

Stage 2 — Vectorize into attributed geology

For the GIS case, confirm the schema carries what a geologist needs: a unit code (e.g. lithostratigraphic abbreviation), a description, age/period, and ideally a confidence or contact type field. Validate and fix geometry first — slivers and self-intersections from old digitizing break tiling:

ogrinfo -sql "SELECT COUNT(*) FROM units WHERE ST_IsValid(geom)=0" geology.gpkg
ogr2ogr -f GPKG geology_fixed.gpkg geology.gpkg \
  -makevalid -nlt PROMOTE_TO_MULTI

PROMOTE_TO_MULTI avoids the common failure where one unit is split into both Polygon and MultiPolygon features. Keep contacts (geological boundaries) as a separate line layer so you can symbolize certain, inferred, and concealed contacts differently — that distinction is core geological information, not decoration.

Stage 3 — Reproject and build vector tiles

Web maps render in EPSG:3857 (Web Mercator). Reproject, then cut tiles. tippecanoe is the standard tool for turning GeoJSON/GeoPackage into MBTiles of Mapbox Vector Tiles:

ogr2ogr -t_srs EPSG:3857 -f GeoJSON units.geojson geology_fixed.gpkg units
tippecanoe -o geology.mbtiles -l units \
  -Z6 -z16 --drop-densest-as-needed \
  --coalesce-densely-coded-features \
  units.geojson

-Z6 -z16 sets the min/max zoom — geology rarely needs detail below the source scale, so do not over-tile. --drop-densest-as-needed protects tile size in busy areas. Crucially, keep the unit code attribute in the tiles; without it the browser cannot colour or label units.

For genuinely fixed cartographic sheets (a published map you must reproduce exactly), skip vectorizing and tile the georeferenced raster instead:

gdal2tiles.py --zoom=8-15 --xyz sheet_geo.tif tiles/

Stage 4 — Style and serve with MapLibre

In MapLibre GL JS the style is JSON. Drive the fill colour from the unit code with a match expression so the map and legend share one source of truth:

{
  "id": "units-fill",
  "type": "fill",
  "source": "geology",
  "source-layer": "units",
  "paint": {
    "fill-color": [
      "match", ["get", "unit_code"],
      "Qal", "#fdf6b5",
      "Jm",  "#7fc97f",
      "Tr",  "#beaed4",
      "#cccccc"
    ],
    "fill-opacity": 0.8
  }
}

Symbolize the contact layer with a line-dasharray for inferred contacts. Wire a click handler that reads the feature properties and shows unit code, description, age, and confidence in a popup. deck.gl is worth reaching for only when you genuinely need GPU-scale rendering (millions of points, animated layers) or 3D draping over terrain; for a polygonal geological map MapLibre alone is lighter and easier to keep cartographically correct.

Preserve the legend and the uncertainty

A geological legend is not a colour key — it encodes stratigraphic order and unit identity. Build one lookup (unit_code → {colour, label, age, order}) and generate both the MapLibre match expression and the HTML legend from it. That guarantees the rendered map and the legend can never disagree, the single most common embarrassment in geology web maps.

Equally important: carry mapping certainty through to the browser. A printed geological map uses dashed lines for inferred contacts and queried symbols for uncertain units. Drop those and an interactive map quietly upgrades interpretation to fact. Keep a contact_type field, render it, and put source, mapped_scale, and survey_date in both the popup and a persistent metadata panel so a user can never read a unit boundary without knowing it came from a 1:50,000 reconnaissance survey from decades ago.

Common pitfalls and why they happen

  • Blurry or shifting geology. Georeferencing used too few control points or an affine transform on a distorted scan. Add control points and use -tps.
  • Legend colours that do not match the map. Symbology and legend were defined separately and drifted. Generate both from one lookup.
  • Huge, slow tiles. Over-tiling beyond the source scale, or vectorizing every pixel of a busy lithology. Cap max zoom near the source scale and use tippecanoe's density controls.
  • Polygons with holes filled in or seams between units. Invalid geometry survived into tiling. Run -makevalid and check for gaps and overlaps before cutting tiles.
  • Inferred contacts shown as solid lines. The certainty attribute was dropped. Preserve and symbolize contact_type.

Validation

Compare the web map against the source sheet at matching scales — unit boundaries should fall on the same ground features. Click a representative sample of polygons and confirm the popup shows the correct unit and age. Check that the legend lists every unit present in the tiles (ogrinfo -sql "SELECT DISTINCT unit_code FROM units") with no orphan or missing entries. Inspect tile sizes (tippecanoe reports them) and confirm the busiest tiles stay within a sane budget. Finally, verify that source, scale, and date are visible without interaction.

Bathyl perspective

An interactive geological map earns trust by saying no more than its source. We treat the legend and the certainty attributes as first-class data, drive symbology from a single lookup, and keep scale and date always on screen — so the map gains interactivity without gaining false precision.

Related reading

Sources