outdooractive/mvt-postgis
Creates Mapbox Vector Tiles (MVT) and MapLibre Tiles (MLT) from PostGIS databases, using configurable Mapnik-style datasource definitions.
Requirements
This package requires Swift 6.3 or higher (at least Xcode 15), and compiles on macOS (\>= macOS 15) as well as Linux.
Installation with Swift Package Manager
dependencies: [
.package(url: "https://github.com/Outdooractive/mvt-postgis", from: "2.0.0"),
],
targets: [
.target(name: "MyTarget", dependencies: [
.product(name: "MVTPostgis", package: "mvt-postgis"),
]),
]This project builds on two other libraries that handle the heavy lifting:
Features
- Source parsing — load datasource configurations from YML (Mapnik), XML (Mapnik/Mapbox Studio), or JSON
- Connection pooling — per-database pools with configurable size, idle connection limits, and timeouts
- Configurable clipping — clip features in PostGIS (
ST_ClipByBox2D), locally before adding to the tile, or at MVT encode time - Configurable simplification — simplify in PostGIS (
ST_Simplify), locally, or with per-zoom meter tolerances - Geometry validation — apply
ST_MakeValidwithdefault,linework, orstructurealgorithm - Feature mapping — transform features after creation (before clipping/simplification) via a callback
- Runtime tracking — per-layer query times, WKB byte counts, feature counts, and invalid feature counts
- Batch query execution — layers are split into batches and queried concurrently with
TaskGroup - Cancellation support — tile creation can be cancelled mid-flight via
Task.cancel() - Per-zoom configuration — clipping, simplification, and validation strategies can vary by zoom level
- Mapnik compatibility — follows the Mapnik PostGIS datasource conventions for layer definitions, extent calculations, and scale denominators
Usage
Configuration
Set up the global configuration before creating any instances:
import MVTPostgis
MVTPostgis.configuration = MVTPostgisConfiguration(
applicationName: "MyTileServer",
connectTimeout: 5.0,
queryTimeout: 10.0,
tileTimeout: 60.0,
poolSize: 10,
maxIdleConnections: 5,
clipping: { zoom, _ in zoom > 10 ? .local : .postgis },
simplification: { zoom, _ in zoom < 12 ? .postgis(preserveCollapsed: true) : .none },
validation: { zoom, _ in zoom > 15 ? .default : .none },
trackRuntimes: true)Initialize from a source file
let mvtPostgis = try MVTPostgis(sourceURL: URL(fileURLWithPath: "datasource.yml"))
// or from XML:
let mvtPostgis = try MVTPostgis(sourceURL: URL(fileURLWithPath: "datasource.xml"))Create a tile
let tile = MapTile(x: 8716, y: 8015, z: 14)
// Get the VectorTile directly for further processing
let (vectorTile, performance) = try await mvtPostgis.vectorTile(forTile: tile)
// Encode to MVT data
let (mvtData, performance) = try await mvtPostgis.data(
forTile: tile,
format: .mvt,
options: VectorTile.ExportOptions(
bufferSize: .pixel(4),
compression: .default))
// Encode to MLT data
let (mltData, performance) = try await mvtPostgis.data(
forTile: tile,
format: .mlt,
options: VectorTile.ExportOptions(
bufferSize: .pixel(4)))YML source format (Mapnik-compatible)
name: my-datasource
minzoom: 0
maxzoom: 20
layers:
- id: roads
datasource:
type: postgis
host: localhost
port: 5432
dbname: gis
user: reader
password: secret
table: (
SELECT osm_id, highway, name, way
FROM planet_osm_roads
WHERE way && !bbox!
) AS roads
geometry_field: way
srid: 3857
extent: -20037508.34,-20037508.34,20037508.34,20037508.34XML source format (Mapnik/Mapbox Studio)
<Layer name="roads" srs="+proj=merc +a=6378137 +b=6378137 +lat_ts=0.0 +lon_0=0.0 +x_0=0.0 +y_0=0.0 +k=1.0 +units=m +nadgrids=@null +wktext +no_defs">
<StyleName>roads</StyleName>
<Datasource>
<Parameter name="type">postgis</Parameter>
<Parameter name="host">localhost</Parameter>
<Parameter name="port">5432</Parameter>
<Parameter name="dbname">gis</Parameter>
<Parameter name="user">reader</Parameter>
<Parameter name="password">secret</Parameter>
<Parameter name="table">(SELECT * FROM roads WHERE way && !bbox!) AS roads</Parameter>
<Parameter name="geometry_field">way</Parameter>
<Parameter name="srid">3857</Parameter>
<Parameter name="extent">-20037508.34,-20037508.34,20037508.34,20037508.34</Parameter>
</Datasource>
</Layer>JSON source format
The JSON format mirrors the PostgisSource → PostgisLayer → PostgisDatasource Codable structure:
{
"name": "Test Source",
"description": "A test source for testing",
"attribution": "Here goes the copyright",
"center": [10.22, 47.56],
"defaultZoom": 10,
"minZoom": 1,
"maxZoom": 16,
"layers": [
{
"id": "First layer",
"description": "Optional layer description",
"fields": {
"type": "Description of the type field",
"geometry": ""
},
"properties": {
"bufferSize": 128
},
"datasource": {
"user": "postgres",
"password": "secret",
"host": "localhost",
"port": 5432,
"databaseName": "gis",
"geometryField": "geometry",
"boundingBox": [10, 47, 11, 48],
"srid": 4326,
"type": "postgis",
"sql": "(SELECT type, geometry FROM some_table WHERE geometry && !bbox!) AS data"
}
}
]
}| Field | Type | Description | |---|---|---| | name | string | Source name | | description | string | Source description | | attribution | string | Copyright attribution | | center | [lon, lat] | Default center coordinate | | defaultZoom | integer | Default zoom level | | minZoom | integer | Minimum zoom | | maxZoom | integer | Maximum zoom | | layers | array | Array of layer definitions | | layers.id | string | Layer name | | layers.description | string? | Optional layer description | | layers.fields | object | Map of field names to descriptions | | layers.properties.bufferSize | integer | Tile buffer in pixels | | layers.datasource.user | string | PostgreSQL user | | layers.datasource.password | string | PostgreSQL password | | layers.datasource.host | string | PostgreSQL hostname | | layers.datasource.port | integer | PostgreSQL port | | layers.datasource.databaseName | string | Database name | | layers.datasource.geometryField | string? | Geometry column name (default: "geometry") | | layers.datasource.boundingBox | [minLon, minLat, maxLon, maxLat]? | Datasource extent | | layers.datasource.srid | integer | SRID (4326 or 3857) | | layers.datasource.type | string | Must be "postgis" | | layers.datasource.sql | string | SQL query with !bbox! placeholder |
The SQL query must contain a !bbox! placeholder — it is replaced with the tile's bounding box at query time.
Shutdown
Always call shutdown() when done to close all database connections:
try await mvtPostgis.shutdown()See also
- API documentation
- gis-tools — geometry types, projections, WKB coding
- mvt-tools — VectorTile model, MVT encode/decode
- Mapnik PostGIS documentation
- Optimize rendering with PostGIS
License
MIT
Package Metadata
Repository: outdooractive/mvt-postgis
Default branch: main
README: README.md