Skip to contents

freestiler creates PMTiles vector tilesets from R. Give it an sf object, a file on disk, or a DuckDB SQL query, and it writes a single .pmtiles file you can serve from anywhere. The tiling engine is written in Rust and runs in-process, so there’s nothing else to install.

About this fork

This is an R-only fork of walkerke/freestiler. It tracks upstream’s R package and API, with one deliberate difference: native builds always statically compile both Rust backends into the package:

  • Rust DuckDB (duckdb-rs, bundled) — powers freestile_query() and the engine = "duckdb" file reader, enabling out-of-R-memory, file-to-file and SQL-to-tiles workflows.
  • GeoParquet (native Rust reader) — powers direct .parquet tiling via freestile_file().

Upstream ships these off by default on Windows; here they are always on, including on Windows (built with the Rtools45 GNU/UCRT toolchain). Because DuckDB is statically linked, the result is a single self-contained DLL with no external runtime dependencies — once installed, library(freestiler) gives you the full feature set with no PATH or runtime setup. Python packaging is intentionally not part of this fork.

Installation

This fork is Windows-focused and always compiles the Rust GeoParquet and DuckDB backends in. Install it from GitHub with pak:

# install.packages("pak")
pak::pak("jimbrig/freestiler")

From a local clone of this repo:

Once an r-universe binary has built, you can install it without a Rust toolchain:

install.packages(
  "freestiler",
  repos = c("https://jimbrig.r-universe.dev", "https://cloud.r-project.org")
)

The upstream package (Rust DuckDB off by default on Windows) is on CRAN and walkerke.r-universe.dev.

Verifying the Rust backends

library(freestiler)
freestiler:::.has_rust_duckdb()
freestiler:::.has_rust_geoparquet()

Building from source (maintainers)

install.R and build.R are convenience scripts for source builds. They locate Rtools via pkgbuild, isolate the build from any personal ~/.R/Makevars, and verify both Rust backends are compiled in:

  • source("install.R") — install into your normal library
  • source("build.R") — install into an isolated build/library/ for testing

tools/feature-smoke.R runs the same feature checks used in CI.

Quick start

The main function is freestile(). Let’s tile the North Carolina counties dataset that ships with sf:

library(sf)
library(freestiler)

nc <- st_read(system.file("shape/nc.shp", package = "sf"))

freestile(nc, "nc_counties.pmtiles", layer_name = "counties")

That’s useful for checking your installation, but the same API handles much bigger data. Here we tile all 242,000 US block groups from tigris:

library(tigris)
options(tigris_use_cache = TRUE)

bgs <- block_groups(cb = TRUE)

freestile(
  bgs,
  "us_bgs.pmtiles",
  layer_name = "bgs",
  min_zoom = 4,
  max_zoom = 12
)

Viewing tiles

The quickest way to view a tileset is view_tiles(), which starts a local server and opens an interactive map:

view_tiles("us_bgs.pmtiles")

For more control, use serve_tiles() to start a local server and build your map with mapgl:

library(mapgl)

serve_tiles("us_bgs.pmtiles")

maplibre(hash = TRUE) |>
  add_pmtiles_source(
    id = "bgs-src",
    url = "http://localhost:8080/us_bgs.pmtiles",
    promote_id = "GEOID"
  ) |>
  add_fill_layer(
    id = "bgs-fill",
    source = "bgs-src",
    source_layer = "bgs",
    fill_color = "navy",
    fill_opacity = 0.5,
    hover_options = list(
      fill_color = "#ffffcc",
      fill_opacity = 0.9
    )
  )

The built-in server handles CORS and range requests automatically. For tilesets larger than ~1 GB, use an external server like npx http-server /path --cors -c-1 for better performance. See the Mapping with mapgl article for a full walkthrough.

DuckDB queries

If your data lives in DuckDB, freestile_query() lets you filter, join, and transform with SQL before tiling:

freestile_query(
  query = "SELECT * FROM read_parquet('blocks.parquet') WHERE state = 'NC'",
  output = "nc_blocks.pmtiles",
  layer_name = "blocks"
)

For very large point datasets, the streaming pipeline avoids loading the full result into memory. On a recent run, freestile_query() streamed 146 million US job points from DuckDB into a 2.3 GB PMTiles archive in about 12 minutes:

freestile_query(
  query = "SELECT naics, state, ST_Point(lon, lat) AS geometry FROM jobs_dots",
  output = "us_jobs_dots.pmtiles",
  db_path = db_path,
  layer_name = "jobs",
  tile_format = "mvt",
  min_zoom = 4,
  max_zoom = 14,
  base_zoom = 14,
  drop_rate = 2.5,
  source_crs = "EPSG:4326",
  streaming = "always",
  overwrite = TRUE
)

Direct file input

You can tile spatial files without loading them into R first:

# GeoParquet
freestile_file("census_blocks.parquet", "blocks.pmtiles")

# GeoPackage, Shapefile, or other formats via DuckDB
freestile_file("counties.gpkg", "counties.pmtiles", engine = "duckdb")

For GeoParquet, the direct file path is powered by the Rust geoparquet backend, which this fork always compiles in. freestile_file() reads WKB-based GeoParquet directly without materializing the data in the R session first.

Dynamic hexagonal binning

For dense point datasets, freestile_h3() aggregates points into H3 hexagons at zoom-appropriate resolutions: low zooms show coarse hexes summarizing whole regions, intermediate zooms show progressively finer hexes, and base_zoom and above reveal the underlying points. Aggregation rules are user-defined SQL (COUNT(*), SUM(pop), AVG(value), …).

freestile_h3(
  pts,
  "wind.pmtiles",
  agg = c(n = "COUNT(*)", total_mw = "SUM(capacity_mw)"),
  min_zoom = 2, max_zoom = 12, base_zoom = 10
)

view_h3_tiles("wind.pmtiles", agg_column = "n")

Pass fade = TRUE to cross-fade between resolutions instead of swapping cleanly. Requires DuckDB and its H3 community extension; see the Hexagonal binning with H3 article.

Multi-layer tilesets

pts <- st_centroid(nc)

freestile(
  list(
    counties = freestile_layer(nc, min_zoom = 0, max_zoom = 10),
    centroids = freestile_layer(pts, min_zoom = 6, max_zoom = 14)
  ),
  "nc_layers.pmtiles"
)

Tile formats

freestiler defaults to Mapbox Vector Tiles (MVT), the widely-supported protobuf format that works with both MapLibre GL JS and Mapbox GL JS. The experimental MapLibre Tiles (MLT) format is also available via tile_format = "mlt" and can produce smaller files for polygon and line data.

Learn more