Martin is a PostGIS vector tiles server suitable for large databases. Martin is written in Rust using Actix web framework.
Martin requires PostGIS >= 2.4.0.
You can download martin from Github releases page.
| Platform | Downloads (latest) | |----------|------------------------| | Linux | 64-bit | | macOS | 64-bit | | Windows | 64-bit |
If you are using macOS and Homebrew you can install martin using Homebrew tap.
shell
brew tap urbica/tap
brew install martin
You can also use official Docker image
shell
docker run -p 3000:3000 -e DATABASE_URL=postgres://postgres@localhost/db maplibre/martin
Martin requires a database connection string. It can be passed as a command-line argument or as a DATABASE_URL
environment variable.
shell
martin postgres://postgres@localhost/db
Martin provides TileJSON endpoint for each geospatial-enabled table in your database.
When started, martin will go through all spatial tables and functions with an appropriate signature in the database. These tables and functions will be available as the HTTP endpoints, which you can use to query Mapbox vector tiles.
| Method | URL | Description |
|--------|----------------------------------------------------------------------------------|---------------------------------------------------------|
| GET
| /index.json
| Table Sources List |
| GET
| /{schema_name}.{table_name}.json
| Table Source TileJSON |
| GET
| /{schema_name}.{table_name}/{z}/{x}/{y}.pbf
| Table Source Tiles |
| GET
| /{schema_name1}.{table_name1},...,{schema_nameN}.{table_nameN}.json
| Composite Source TileJSON |
| GET
| /{schema_name1}.{table_name1},...,{schema_nameN}.{table_nameN}/{z}/{x}/{y}.pbf
| Composite Source Tiles |
| GET
| /rpc/index.json
| Function Sources List |
| GET
| /rpc/{schema_name}.{function_name}.json
| Function Source TileJSON |
| GET
| /rpc/{schema_name}.{function_name}/{z}/{x}/{y}.pbf
| Function Source Tiles |
| GET
| /healthz
| Martin server health check: returns 200 OK
|
MapLibre is an Open-source JavaScript library for showing maps on a website. MapLibre can accept MVT vector tiles generated by Martin, and applies a style to them to draw a map using Web GL.
You can add a layer to the map and specify Martin TileJSON endpoint as a vector source URL. You should also specify a source-layer
property. For Table Sources it is {schema_name}.{table_name}
by default.
js
map.addLayer({
id: 'public.points',
type: 'circle',
source: {
type: 'vector',
url: 'http://localhost:3000/public.points.json'
},
'source-layer': 'public.points',
paint: {
'circle-color': 'red'
},
});
js
map.addSource('rpc', {
type: 'vector',
url: `http://localhost:3000/rpc/public.function_source.json`
});
map.addLayer({
id: 'points',
type: 'circle',
source: 'rpc',
'source-layer': 'public.function_source',
paint: {
'circle-color': 'blue'
},
});
You can also combine multiple tables into one source with Composite Sources. Each Table Source in Composite Source can be accessed with its {schema_name}.{table_name}
as a source-layer
property.
``js
map.addSource('points', {
type: 'vector',
url:
http://0.0.0.0:3000/public.points1,public.points2.json`
});
map.addLayer({ id: 'red_points', type: 'circle', source: 'points', 'source-layer': 'public.points1', paint: { 'circle-color': 'red' } });
map.addLayer({ id: 'blue_points', type: 'circle', source: 'points', 'source-layer': 'public.points2', paint: { 'circle-color': 'blue' } }); ```
Leaflet is the leading open-source JavaScript library for mobile-friendly interactive maps.
You can add vector tiles using Leaflet.VectorGrid plugin. You must initialize a VectorGrid.Protobuf with a URL template, just like in L.TileLayers. The difference is that you should define the styling for all the features.
js
L.vectorGrid
.protobuf('http://localhost:3000/public.points/{z}/{x}/{y}.pbf', {
vectorTileLayerStyles: {
'public.points': {
color: 'red',
fill: true
}
}
})
.addTo(map);
deck.gl is a WebGL-powered framework for visual exploratory data analysis of large datasets.
You can add vector tiles using MVTLayer. MVTLayer data
property defines the remote data for the MVT layer. It can be
String
: Either a URL template or a TileJSON URL.Array
: an array of URL templates. It allows to balance the requests across different tile endpoints. For example, if you define an array with 4 urls and 16 tiles need to be loaded, each endpoint is responsible to server 16/4 tiles.JSON
: A valid TileJSON object.```js const pointsLayer = new MVTLayer({ data: 'http://localhost:3000/public.points.json', // 'http://localhost:3000/public.table_source/{z}/{x}/{y}.pbf' pointRadiusUnits: 'pixels', getRadius: 5, getFillColor: [230, 0, 0] });
const deckgl = new DeckGL({ container: 'map', mapStyle: 'https://basemaps.cartocdn.com/gl/dark-matter-gl-style/style.json', initialViewState: { latitude: 0, longitude: 0, zoom: 1 }, layers: [pointsLayer] }); ```
Mapbox GL JS is a JavaScript library for interactive, customizable vector maps on the web. Mapbox GL JS v1.x was open source, and it was forked as MapLibre (see above), so using Martin with Mapbox is similar to MapLibre. Mapbox GL JS can accept MVT vector tiles generated by Martin, and applies a style to them to draw a map using Web GL.
You can add a layer to the map and specify martin TileJSON endpoint as a vector source URL. You should also specify a source-layer
property. For Table Sources it is {schema_name}.{table_name}
by default.
js
map.addLayer({
id: 'public.points',
type: 'circle',
source: {
type: 'vector',
url: 'http://localhost:3000/public.points.json'
},
'source-layer': 'public.points',
paint: {
'circle-color': 'red'
}
});
Table Source is a database table which can be used to query vector tiles. When started, martin will go through all spatial tables in the database and build a list of table sources. A table should have at least one geometry column with non-zero SRID. All other table columns will be represented as properties of a vector tile feature.
Note: In case if there are multiple geometry columns in the table, you can specify the geometry column name in the table source name to access particular geometry in vector tile, e.g. schema_name.table_name.geometry_column
.
Table Sources list endpoint is available at /index.json
shell
curl localhost:3000/index.json
Table Source TileJSON endpoint is available at /{schema_name}.{table_name}.json
.
For example, points
table in public
schema will be available at /public.points.json
shell
curl localhost:3000/public.points.json
In case if you have multiple geometry columns in that table and want to access a particular geometry column in vector tile, you should also specify the geometry column in the table source name
shell
curl localhost:3000/public.points.geom.json
Table Source tiles endpoint is available at /{schema_name}.{table_name}/{z}/{x}/{y}.pbf
For example, points
table in public
schema will be available at /public.points/{z}/{x}/{y}.pbf
shell
curl localhost:3000/public.points/0/0/0.pbf
In case if you have multiple geometry columns in that table and want to access a particular geometry column in vector tile, you should also specify the geometry column in the table source name
shell
curl localhost:3000/public.points.geom/0/0/0.pbf
Composite Sources allows combining multiple Table Sources into one. Composite Source consists of multiple Table Sources separated by comma {schema_name1}.{table_name1},...,{schema_nameN}.{table_nameN}
Each Table Source in Composite Source can be accessed with its {schema_name}.{table_name}
as a source-layer
property.
Composite Source TileJSON endpoint is available at /{schema_name1}.{table_name1},...,{schema_nameN}.{table_nameN}.json
.
For example, composite source for points
and lines
tables in public
schema will be available at /public.points,public.lines.json
shell
curl localhost:3000/public.points,public.lines.json
Composite Source tiles endpoint is available at /{schema_name1}.{table_name1},...,{schema_nameN}.{table_nameN}/{z}/{x}/{y}.pbf
For example, composite source for points
and lines
tables in public
schema will be available at /public.points,public.lines/{z}/{x}/{y}.pbf
shell
curl localhost:3000/public.points,public.lines/0/0/0.pbf
Function Source is a database function which can be used to query vector tiles. When started, martin will look for the functions with a suitable signature. A function that takes z integer
, x integer
, y integer
, and query_params json
and returns bytea
, can be used as a Function Source.
| Argument | Type | Description | | ------------ | ------- | ----------------------- | | z | integer | Tile zoom parameter | | x | integer | Tile x parameter | | y | integer | Tile y parameter | | query_params | json | Query string parameters |
Hint: You may want to use TileBBox function to generate bounding-box geometry of the area covered by a tile.
For example, if you have a table public.table_source
in WGS84 (4326
SRID), then you can use this function as a Function Source:
```sql CREATE OR REPLACE FUNCTION public.functionsource(z integer, x integer, y integer, queryparams json) RETURNS bytea AS $$ DECLARE mvt bytea; BEGIN SELECT INTO mvt STAsMVT(tile, 'public.functionsource', 4096, 'geom') FROM ( SELECT STAsMVTGeom(STTransform(STCurveToLine(geom), 3857), TileBBox(z, x, y, 3857), 4096, 64, true) AS geom FROM public.tablesource WHERE geom && TileBBox(z, x, y, 4326) ) as tile WHERE geom IS NOT NULL;
RETURN mvt; END $$ LANGUAGE plpgsql IMMUTABLE STRICT PARALLEL SAFE; ```
The query_params
argument is a JSON representation of the tile request query params. For example, if user requested a tile with urlencoded params:
shell
curl \
--data-urlencode 'arrayParam=[1, 2, 3]' \
--data-urlencode 'numberParam=42' \
--data-urlencode 'stringParam=value' \
--data-urlencode 'booleanParam=true' \
--data-urlencode 'objectParam={"answer" : 42}' \
--get localhost:3000/rpc/public.function_source/0/0/0.pbf
then query_params
will be parsed as:
json
{
"arrayParam": [1, 2, 3],
"numberParam": 42,
"stringParam": "value",
"booleanParam": true,
"objectParam": { "answer": 42 }
}
You can access this params using json operators:
sql
...WHERE answer = (query_params->'objectParam'->>'answer')::int;
Function Sources list endpoint is available at /rpc/index.json
shell
curl localhost:3000/rpc/index.json
Function Source TileJSON endpoint is available at /rpc/{schema_name}.{function_name}.json
For example, points
function in public
schema will be available at /rpc/public.points.json
shell
curl localhost:3000/rpc/public.points.json
Function Source tiles endpoint is available at /rpc/{schema_name}.{function_name}/{z}/{x}/{y}.pbf
For example, points
function in public
schema will be available at /rpc/public.points/{z}/{x}/{y}.pbf
shell
curl localhost:3000/rpc/public.points/0/0/0.pbf
You can configure martin using command-line interface
```shell Usage: martin [OPTIONS] [CONNECTION]
Arguments: [CONNECTION] Database connection string
Options:
-c, --config
You can also configure martin using environment variables
| Environment variable | Example | Description |
|-------------------------------|------------------------------------|---------------------------------------------|
| DATABASE_URL
| postgres://postgres@localhost/db
| Postgres database connection |
| CA_ROOT_FILE
| ./ca-certificate.crt
| Loads trusted root certificates from a file |
| DEFAULT_SRID
| 4326
| Fallback SRID |
| DANGER_ACCEPT_INVALID_CERTS
| false
| Trust invalid certificates |
If you don't want to expose all of your tables and functions, you can list your sources in a configuration file. To start martin with a configuration file you need to pass a path to a file with a --config
argument.
shell
martin --config config.yaml
You can find an example of a configuration file here.
```yaml
connection_string: 'postgres://postgres@localhost:5432/db'
dangeracceptinvalid_certs: false
default_srid: 4326
keep_alive: 75
listen_addresses: '0.0.0.0:3000'
pool_size: 20
worker_processes: 8
tablesources: public.tablesource: # Table source id (required) id: public.table_source
# Table schema (required)
schema: public
# Table name (required)
table: table_source
# Geometry SRID (required)
srid: 4326
# Geometry column name (required)
geometry_column: geom
# Feature id column name
id_column: ~
# An integer specifying the minimum zoom level
minzoom: 0
# An integer specifying the maximum zoom level. MUST be >= minzoom
maxzoom: 30
# The maximum extent of available map tiles. Bounds MUST define an area
# covered by all zoom levels. The bounds are represented in WGS:84
# latitude and longitude values, in the order left, bottom, right, top.
# Values may be integers or floating point numbers.
bounds: [-180.0, -90.0, 180.0, 90.0]
# Tile extent in tile coordinate space
extent: 4096
# Buffer distance in tile coordinate space to optionally clip geometries
buffer: 64
# Boolean to control if geometries should be clipped or encoded as is
clip_geom: true
# Geometry type
geometry_type: GEOMETRY
# List of columns, that should be encoded as tile properties (required)
properties:
gid: int4
functionsources: public.functionsource: # Function source id (required) id: public.function_source
# Schema name (required)
schema: public
# Function name (required)
function: function_source
# An integer specifying the minimum zoom level
minzoom: 0
# An integer specifying the maximum zoom level. MUST be >= minzoom
maxzoom: 30
# The maximum extent of available map tiles. Bounds MUST define an area
# covered by all zoom levels. The bounds are represented in WGS:84
# latitude and longitude values, in the order left, bottom, right, top.
# Values may be integers or floating point numbers.
bounds: [-180.0, -90.0, 180.0, 90.0]
```
You can use official Docker image maplibre/martin
shell
docker run \
-p 3000:3000 \
-e DATABASE_URL=postgres://postgres@localhost/db \
maplibre/martin
If you are running PostgreSQL instance on localhost
, you have to change network settings to allow the Docker container to access the localhost
network.
For Linux, add the --net=host
flag to access the localhost
PostgreSQL service.
shell
docker run \
--net=host \
-p 3000:3000 \
-e DATABASE_URL=postgres://postgres@localhost/db \
maplibre/martin
For macOS, use host.docker.internal
as hostname to access the localhost
PostgreSQL service.
shell
docker run \
-p 3000:3000 \
-e DATABASE_URL=postgres://postgres@host.docker.internal/db \
maplibre/martin
For Windows, use docker.for.win.localhost
as hostname to access the localhost
PostgreSQL service.
shell
docker run \
-p 3000:3000 \
-e DATABASE_URL=postgres://postgres@docker.for.win.localhost/db \
maplibre/martin
You can use example docker-compose.yml
file as a reference
```yml version: '3'
services: martin: image: maplibre/martin restart: unless-stopped ports: - "3000:3000" environment: - DATABASEURL=postgres://postgres:password@db/db dependson: - db
db: image: postgis/postgis:14-3.1-alpine restart: unless-stopped environment: - POSTGRESDB=db - POSTGRESUSER=postgres - POSTGRESPASSWORD=password volumes: - ./pgdata:/var/lib/postgresql/data ```
First, you need to start db
service
shell
docker-compose up -d db
Then, after db
service is ready to accept connections, you can start martin
shell
docker-compose up -d martin
By default, martin will be available at localhost:3000
You can run martin behind Nginx proxy, so you can cache frequently accessed tiles and reduce unnecessary pressure on the database.
```yml version: '3'
services: nginx: image: nginx:alpine restart: unless-stopped ports: - "80:80" volumes: - ./cache:/var/cache/nginx - ./nginx.conf:/etc/nginx/nginx.conf:ro depends_on: - martin
martin: image: maplibre/martin restart: unless-stopped environment: - DATABASEURL=postgres://postgres:password@db/db dependson: - db
db: image: postgis/postgis:14-3.1-alpine restart: unless-stopped environment: - POSTGRESDB=db - POSTGRESUSER=postgres - POSTGRESPASSWORD=password volumes: - ./pgdata:/var/lib/postgresql/data ```
You can find an example Nginx configuration file here.
If you are running martin behind Nginx proxy, you may want to rewrite the request URL to properly handle tile URLs in TileJSON endpoints.
```nginx
location ~ /tiles/(?
proxy_pass http://martin:3000/$fwd_path$is_args$args;
} ```
You can also use Nginx to cache tiles. In the example, the maximum cache size is set to 10GB, and caching time is set to 1 hour for responses with codes 200, 204, and 302 and 1 minute for responses with code 404.
```nginx http { ... proxycachepath /var/cache/nginx/ levels=1:2 maxsize=10g usetemppath=off keyszone=tiles_cache:10m;
server {
...
location ~ /tiles/(?
proxy_cache tiles_cache;
proxy_cache_lock on;
proxy_cache_revalidate on;
# Set caching time for responses
proxy_cache_valid 200 204 302 1h;
proxy_cache_valid 404 1m;
proxy_cache_use_stale error timeout http_500 http_502 http_503 http_504;
add_header X-Cache-Status $upstream_cache_status;
proxy_pass http://martin:3000/$fwd_path$is_args$args;
}
} } ```
You can find an example Nginx configuration file here.
You can clone the repository and build martin using cargo package manager.
shell
git clone git@github.com:maplibre/martin.git
cd martin
cargo build --release
The binary will be available at ./target/release/martin
.
shell
cd ./target/release/
./martin postgres://postgres@localhost/db
Log levels are controlled on a per-module basis, and by default all logging is disabled except for errors. Logging is controlled via the RUST_LOG
environment variable. The value of this environment variable is a comma-separated list of logging directives.
This will enable debug logging for all modules:
shell
export RUST_LOG=debug
martin postgres://postgres@localhost/db
While this will only enable verbose logging for the actix_web
module and enable debug logging for the martin
and tokio_postgres
modules:
shell
export RUST_LOG=actix_web=info,martin=debug,tokio_postgres=debug
martin postgres://postgres@localhost/db
just
to see available commands:```shell, ignore ❯ git clone git@github.com:maplibre/martin.git ❯ cd martin ❯ just Available recipes: run # Start Martin server and a test database clean # Perform cargo clean to delete all build files clean-test # Delete test output files start-db # Start a test database stop # Stop the test database bench # Run benchmark tests test # Run all tests using a test database test-unit # Run Rust unit tests (cargo test) test-int # Run integration tests bless # Run integration tests and save its output as the new expected output git-push *ARGS # Do a git push, ensuring that it can run tests first. Accepts the same arguments as git push.
```shell
docker-compose up -d db
DATABASE_URL=postgres://postgres@localhost/db cargo run ```
Open tests/debug.html
for debugging. By default, martin will be available at localhost:3000
Make your changes, and check if all the tests are running
shell
DATABASE_URL=postgres://postgres@localhost/db cargo test
You can also run benchmarks with
shell
DATABASE_URL=postgres://postgres@localhost/db cargo bench
An HTML report displaying the results of the benchmark will be generated under target/criterion/report/index.html
You can use martin with Managed PostgreSQL from DigitalOcean with PostGIS extension
First, you need to download the CA certificate and get your cluster connection string from the dashboard. After that, you can use the connection string and the CA certificate to connect to the database
shell
martin --ca-root-file ./ca-certificate.crt postgres://user:password@host:port/db?sslmode=require
You can use martin with Managed PostgreSQL from Heroku with PostGIS extension
shell
heroku pg:psql -a APP_NAME -c 'create extension postgis'
In order to trust the Heroku certificate, you can disable certificate validation with either DANGER_ACCEPT_INVALID_CERTS
environment variable
shell
DATABASE_URL=$(heroku config:get DATABASE_URL -a APP_NAME) DANGER_ACCEPT_INVALID_CERTS=true martin
or --danger-accept-invalid-certs
command-line argument
shell
martin --danger-accept-invalid-certs $(heroku config:get DATABASE_URL -a APP_NAME)