Webmapping

The Web Constraint

The Web

  • Browsers understand exactly three things: HTML (structure), CSS (style), JavaScript (behaviour)
  • A Shapefile, GeoPackage, or GeoTIFF are not browser native
  • To put a map on the web, geodata must be translated into something JS can work with
  • A mapping library (Leaflet, MapLibre, …) bridges the gap — but the data still needs to arrive in the right format

Two Ways Geodata Enters a Browser

  • Embedded — data is serialised into the HTML/JS directly
    • GeoJSON stored as a JavaScript variable in the HTML file
    • Browser has everything it needs on load
    • Simple, but the entire dataset must download before anything renders
  • Fetched on demand — browser requests tiles as the user pans and zooms
    • Only the tiles visible in the current viewport are loaded
    • Scales to arbitrarily large datasets
    • Requires the data to be pre-tiled

Exercise

tmap (R)

library(tmap)
library(sf)
tmap_mode("view")   # interactive; "plot" for static print output

gpkg <- "data/vector-deepdive/swissboundaries3d_2024-01_2056_5728/swissBOUNDARIES3D_1_5_LV95_LN02.gpkg"

cantons <- st_read(gpkg, "tlm_kantonsgebiet")

tm_shape(cantons) +
  tm_polygons(fill = "kantonsflaeche")
  • Uses leaflet (Leaflet.js) under the hood in interactive mode
  • Save as standalone HTML with tmap_save(m, "map.html")
  • Good for: quick sharing, small datasets (< ~5 MB)

Part 1: Open the Hood

  • Create a tmap map using the cantonal data from webmapping (on Moodle)
  • save it as HTML (tmap_save()) with the name index.html
  • Open index.html in your browser toggle developper tools
  • Open index.html in VSCode

Part 1: Questions

Work through the HTML source and answer:

  1. Where is the canton polygon data? What format is it in?
  2. Where is the basemap? Is it embedded the same way — or something different?
  3. How large is map.html? Now swap cantons for the Swiss municipalities layer (~2300 polygons). What happens to file size?
  4. What would happen if you embedded a 100 MB dataset this way?

The Tile Paradigm

The Billion Dollar Code - The problem

Video shown in class only — watch The Billion Dollar Code (2021) on Netflix

The Billion Dollar Code - The idea

Video shown in class only — watch The Billion Dollar Code (2021) on Netflix

The Billion Dollar Code - The solution

Video shown in class only — watch The Billion Dollar Code (2021) on Netflix

The Tile Grid

  • The world is divided into a pyramid of tiles at discrete zoom levels
  • Each tile is addressed by z / x / y: zoom level, column, row
  • At zoom 0: 1 tile covers the whole world
  • Each zoom level quadruples the number of tiles
Zoom Level Tiles Total
0 1 × 1 1
1 2 × 2 4
2 4 × 4 16
7 128 × 128 16'384
10 1024 × 1024 1'048'576
14 16384 × 16384 268'435'456

Raster Tiles vs. Vector Tiles

Raster tiles Vector tiles
Format PNG / JPEG image Protobuf-encoded geometry (.pbf)
Rendering Server-side Client-side (browser / GPU)
Styling Baked into image Defined in JSON, applied at runtime
File size Larger Smaller
Flexibility Fixed style One dataset, many styles
Typical use Basemaps, aerial imagery Thematic data, interactive layers

Web Mercator (EPSG:3857)

  • Nearly all webmaps use Web Mercator — the projection of the internet
  • Projects the sphere onto a square → convenient for tile grids
  • Distortion: areas near poles appear much larger than they are (Greenland ≈ Africa)
  • Angles are preserved (conformal) — useful for navigation
  • Your data is likely in a different CRS → must be reprojected before serving
  • Tools handle this automatically, but be aware of the distortion

Encoding Vector Geodata for the Web

GeoJSON

  • Human-readable, text-based format — geometry + attributes in one file
  • Natively understood by every webmapping library
  • Easy to embed directly in an HTML page
{
  "type": "FeatureCollection",
  "features": [{
    "type": "Feature",
    "geometry": { "type": "Point", "coordinates": [8.54, 47.37] },
    "properties": { "name": "Zurich" }
  }]
}

Limits of GeoJSON

  • File size: a 50 MB GeoJSON is painful to load — full download before anything renders
  • No streaming: browser must receive the entire file before it can display anything
  • No tiling: all features loaded regardless of what’s visible in the viewport
  • No built-in indexing: spatial queries happen client-side over all features
  • GeoJSON works well for small datasets (< ~5 MB) — for larger data, use tiles

Mapbox Vector Tiles (MVT)

  • Binary format (protobuf) — much more compact than GeoJSON
  • Geometry encoded in tile-local integer coordinates (0–4096), not lat/lon
  • Features are simplified at low zoom levels — less data transferred when zoomed out
  • Spec is open — implemented by MapLibre, OpenLayers, deck.gl
  • File extension: .pbf or .mvt
  • MVT is the format of individual tiles; PMTiles packages them all into one file
  • GDAL can also write MVT (ogr2ogr -f MVT) — but outputs a directory tree, not a single archive

PMTiles

  • A single-file archive containing all tiles of a dataset
  • Tiles are addressed by z/x/y
  • Clients fetch only the tiles they need using HTTP range requests — no tile server needed
  • Host on any object storage: S3, Cloudflare R2, GitHub Pages
  • One file, no server process, scales to any traffic
  • Generate with tippecanoe

flowchart LR
  A[Geopackage] -->|gdal| B[GeoJSON]
  B -->|tippecanoe| C[.pmtiles]

Exercise Part 2 (optional)

Convert the municipalities GeoJSON to PMTiles with tippecanoe:

tippecanoe -o municipalities.pmtiles \
           -zg \
           --drop-densest-as-needed \
           municipalities.geojson

Then:

  1. Create a minimal index.html that loads the .pmtiles with MapLibre GL JS
  2. Push both files to a GitHub repository with Pages enabled
  3. Share your public URL

Publishing with minimal Infrastructure

Two Paths to a Public Map

Embedded GeoJSON PMTiles
Data size < ~5 MB Any size
Output Standalone HTML .pmtiles + HTML
Hosting Anywhere (Drive, email, Pages) Object storage or GitHub Pages
Workflow tmaptmap_save() tippecanoe → upload
Requires a server? No No

Both paths produce a static, serverless public map.

GitHub Pages — Free Hosting

A GitHub repository with Pages enabled serves files as a static website:

your-repo/
├── index.html        ← MapLibre map that loads the PMTiles
└── data.pmtiles      ← your tile archive
  • Public URL, zero cost, works immediately after git push
  • Works well for files up to ~100 MB
  • Larger files → use S3 or Cloudflare R2

Object Storage (S3, R2)

When serving PMTiles from S3 or Cloudflare R2, you must configure CORS:

[{
  "AllowedOrigins": ["*"],
  "AllowedMethods": ["GET", "HEAD"],
  "AllowedHeaders": ["Range"],
  "ExposeHeaders": ["Content-Range", "Content-Length"]
}]
  • Without this, the browser blocks range requests to your bucket
  • Range is the header that enables partial tile fetching — it must be explicitly allowed
  • This is the most common reason PMTiles “doesn’t work” in production

Basemap Providers

Your PMTiles has your thematic data — you also need a basemap:

Provider Free tier API key
Protomaps Unlimited (CDN) No
MapTiler 100k requests/month Yes
Stadia Maps Unlimited for non-commercial Yes
OpenStreetMap (raster) Rate-limited No

A Minimal MapLibre Map

<!DOCTYPE html>
<html>
<head>
  <link href="https://unpkg.com/maplibre-gl/dist/maplibre-gl.css" rel="stylesheet"/>
  <script src="https://unpkg.com/maplibre-gl/dist/maplibre-gl.js"></script>
  <style> #map { height: 100vh; } </style>
</head>
<body>
  <div id="map"></div>
  <script>
    const map = new maplibregl.Map({
      container: 'map',
      style: 'https://demotiles.maplibre.org/style.json',  // <1>
      center: [8.54, 47.37],
      zoom: 8
    });
  </script>
</body>
</html>
  1. The style URL is a JSON document defining all layers and sources — this is where you add your PMTiles layer

Choosing Your Stack

Decision Guide

  • Quick exploration / publication maptmap
  • Small data + sharingtmaptmap_save() → standalone HTML → any host
  • Large data + public map → tippecanoe → PMTiles → GitHub Pages / S3
  • Custom styled vector map → PMTiles + MapLibre GL JS + Protomaps basemap
  • Enterprise / government → AGOL or GeoServer (out of today’s scope)

The low-infrastructure path for most projects:

flowchart LR
  A[Geopackage] -->|gdal| B[GeoJSON]
  B -->|tippecanoe| C[.pmtiles]
  C -->|git push| D[GitHub Pages]
  D --> E([public URL: no server · no subscription])