Martin is a [PostGIS](https://github.com/postgis/postgis) [vector tiles](https://github.com/mapbox/vector-tile-spec) server suitable for large databases. Martin is written in [Rust](https://github.com/rust-lang/rust) using [Actix](https://github.com/actix/actix-web) web framework.
If you are using macOS and [Homebrew](https://brew.sh/) you can install martin using Homebrew tap.
```shell
brew tap urbica/tap
brew install martin
```
You can also use [official Docker image](https://hub.docker.com/r/maplibre/martin)
```shell
docker run -p 3000:3000 -e DATABASE_URL=postgresql://postgres@localhost/db maplibre/martin
```
## Usage
Martin requires a database connection string. It can be passed as a command-line argument or as a `DATABASE_URL` environment variable.
```shell
martin postgresql://postgres@localhost/db
```
Martin provides [TileJSON](https://github.com/mapbox/tilejson-spec) endpoint for each [geospatial-enabled](https://postgis.net/docs/postgis_usage.html#geometry_columns) table in your database.
## API
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.
| `GET` | `/health` | Martin server health check: returns 200 `OK` |
## Using with MapLibre
[MapLibre](https://maplibre.org/projects/maplibre-gl-js/) is an Open-source JavaScript library for showing maps on a website. MapLibre can accept [MVT vector tiles](https://github.com/mapbox/vector-tile-spec) generated by Martin, and applies [a style](https://maplibre.org/maplibre-gl-js-docs/style-spec/) to them to draw a map using Web GL.
You can add a layer to the map and specify Martin [TileJSON](https://github.com/mapbox/tilejson-spec) endpoint as a vector source URL. You should also specify a `source-layer` property. For [Table Sources](#table-sources) it is `{table_name}` by default.
```js
map.addLayer({
id: 'points',
type: 'circle',
source: {
type: 'vector',
url: 'http://localhost:3000/points'
},
'source-layer': 'points',
paint: {
'circle-color': 'red'
},
});
```
```js
map.addSource('rpc', {
type: 'vector',
url: `http://localhost:3000/function_zxy_query`
});
map.addLayer({
id: 'points',
type: 'circle',
source: 'rpc',
'source-layer': 'function_zxy_query',
paint: {
'circle-color': 'blue'
},
});
```
You can also combine multiple sources into one source with [Composite Sources](#composite-sources). Each source in a composite source can be accessed with its `{source_name}` as a `source-layer` property.
```js
map.addSource('points', {
type: 'vector',
url: `http://0.0.0.0:3000/points1,points2`
});
map.addLayer({
id: 'red_points',
type: 'circle',
source: 'points',
'source-layer': 'points1',
paint: {
'circle-color': 'red'
}
});
map.addLayer({
id: 'blue_points',
type: 'circle',
source: 'points',
'source-layer': 'points2',
paint: {
'circle-color': 'blue'
}
});
```
## Using with Leaflet
[Leaflet](https://github.com/Leaflet/Leaflet) is the leading open-source JavaScript library for mobile-friendly interactive maps.
You can add vector tiles using [Leaflet.VectorGrid](https://github.com/Leaflet/Leaflet.VectorGrid) plugin. You must initialize a [VectorGrid.Protobuf](https://leaflet.github.io/Leaflet.VectorGrid/vectorgrid-api-docs.html#vectorgrid-protobuf) with a URL template, just like in L.TileLayers. The difference is that you should define the styling for all the features.
[deck.gl](https://deck.gl/) is a WebGL-powered framework for visual exploratory data analysis of large datasets.
You can add vector tiles using [MVTLayer](https://deck.gl/docs/api-reference/geo-layers/mvt-layer). MVTLayer `data` property defines the remote data for the MVT layer. It can be
-`String`: Either a URL template or a [TileJSON](https://github.com/mapbox/tilejson-spec) 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](https://github.com/mapbox/tilejson-spec/tree/master/2.2.0).
[Mapbox GL JS](https://github.com/mapbox/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](#using-with-maplibre)), so using Martin with Mapbox is similar to MapLibre. Mapbox GL JS can accept [MVT vector tiles](https://github.com/mapbox/vector-tile-spec) generated by Martin, and applies [a style](https://docs.mapbox.com/mapbox-gl-js/style-spec/) 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](#table-sources) it is `{table_name}` by default.
```js
map.addLayer({
id: 'points',
type: 'circle',
source: {
type: 'vector',
url: 'http://localhost:3000/points'
},
'source-layer': 'points',
paint: {
'circle-color': 'red'
}
});
```
## Source List
A list of all available sources is available in a catalogue:
```shell
curl localhost:3000/catalog | jq
```
```yaml
[
{
"id": "function_zxy_query",
"name": "public.function_zxy_query"
},
{
"id": "points1",
"name": "public.points1.geom"
},
// ...
]
```
## Table Sources
Table Source is a database table which can be used to query [vector tiles](https://github.com/mapbox/vector-tile-spec). 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.
### Table Source TileJSON
Table Source [TileJSON](https://github.com/mapbox/tilejson-spec) endpoint is available at `/{table_name}`.
For example, `points` table will be available at `/points`, unless there is another source with the same name, or if the table has multiple geometry columns, in which case it will be available at `/points.1`, `/points.2`, etc.
```shell
curl localhost:3000/points
```
### Table Source Tiles
Table Source tiles endpoint is available at `/{table_name}/{z}/{x}/{y}`
For example, `points` table will be available at `/points/{z}/{x}/{y}`
```shell
curl localhost:3000/points/0/0/0
```
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/points.geom/0/0/0
```
## Composite Sources
Composite Sources allows combining multiple sources into one. Composite Source consists of multiple sources separated by comma `{source1},...,{sourceN}`
Each source in a composite source can be accessed with its `{source_name}` as a `source-layer` property.
### Composite Source TileJSON
Composite Source [TileJSON](https://github.com/mapbox/tilejson-spec) endpoint is available at `/{source1},...,{sourceN}`.
For example, composite source for `points` and `lines` tables will be available at `/points,lines`
```shell
curl localhost:3000/points,lines
```
### Composite Source Tiles
Composite Source tiles endpoint is available at `/{source1},...,{sourceN}/{z}/{x}/{y}`
For example, composite source for `points` and `lines` tables will be available at `/points,lines/{z}/{x}/{y}`
```shell
curl localhost:3000/points,lines/0/0/0
```
## Function Sources
Function Source is a database function which can be used to query [vector tiles](https://github.com/mapbox/vector-tile-spec). When started, martin will look for the functions with a suitable signature. A function that takes `z integer` (or `zoom integer`), `x integer`, `y integer`, and an optional `query json` and returns `bytea`, can be used as a Function Source. Alternatively the function could return a record with a single `bytea` field, or a record with two fields of types `bytea` and `text`, where the `text` field is an etag key (i.e. md5 hash).
For example, if you have a table `table_source` in WGS84 (`4326` SRID), then you can use this function as a Function Source:
```sql, ignore
CREATE OR REPLACE FUNCTION function_zxy_query(z integer, x integer, y integer) RETURNS bytea AS $$
DECLARE
mvt bytea;
BEGIN
SELECT INTO mvt ST_AsMVT(tile, 'function_zxy_query', 4096, 'geom') FROM (
SELECT
ST_AsMVTGeom(ST_Transform(ST_CurveToLine(geom), 3857), ST_TileEnvelope(z, x, y), 4096, 64, true) AS geom
FROM table_source
WHERE geom && ST_Transform(ST_TileEnvelope(z, x, y), 4326)
) as tile WHERE geom IS NOT NULL;
RETURN mvt;
END
$$ LANGUAGE plpgsql IMMUTABLE STRICT PARALLEL SAFE;
```
```sql, ignore
CREATE OR REPLACE FUNCTION function_zxy_query(z integer, x integer, y integer, query_params json) RETURNS bytea AS $$
DECLARE
mvt bytea;
BEGIN
SELECT INTO mvt ST_AsMVT(tile, 'function_zxy_query', 4096, 'geom') FROM (
SELECT
ST_AsMVTGeom(ST_Transform(ST_CurveToLine(geom), 3857), ST_TileEnvelope(z, x, y), 4096, 64, true) AS geom
FROM table_source
WHERE geom && ST_Transform(ST_TileEnvelope(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](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/encodeURIComponent) 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/function_zxy_query/0/0/0
```
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](https://www.postgresql.org/docs/current/functions-json.html):
You can also configure martin using environment variables, but only if the configuration file is not used. See [configuration section](#configuration-file) on how to use environment variables with config files.
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. Config files may contain environment variables, which will be expanded before parsing. For example, to use `MY_DATABASE_URL` in your config file: `connection_string: ${MY_DATABASE_URL}`, or with a default `connection_string: ${MY_DATABASE_URL:-postgresql://postgres@localhost/db}`
```shell
martin --config config.yaml
```
You can find an example of a configuration file [here](https://github.com/maplibre/martin/blob/main/tests/config.yaml).
```yaml
# Connection keep alive timeout [default: 75]
keep_alive: 75
# The socket address to bind [default: 0.0.0.0:3000]
listen_addresses: '0.0.0.0:3000'
# Number of web server workers
worker_processes: 8
# Database configuration. This can also be a list of PG configs.
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.
You can find an example Nginx configuration file [here](https://github.com/maplibre/martin/blob/main/nginx.conf).
### Rewriting URLs
If you are running martin behind Nginx proxy, you may want to rewrite the request URL to properly handle tile URLs in [TileJSON](#table-source-tilejson) [endpoints](#function-source-tilejson).
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.
You can find an example Nginx configuration file [here](https://github.com/maplibre/martin/blob/main/nginx.conf).
## Building from Source
You can clone the repository and build martin using [cargo](https://doc.rust-lang.org/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 postgresql://postgres@localhost/db
```
## Debugging
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 postgresql://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:
An HTML report displaying the results of the benchmark will be generated under `target/criterion/report/index.html`
## Recipes
### Using with DigitalOcean PostgreSQL
You can use martin with [Managed PostgreSQL from DigitalOcean](https://www.digitalocean.com/products/managed-databases-postgresql/) with PostGIS extension
First, you need to download the CA certificate and get your cluster connection string from the [dashboard](https://cloud.digitalocean.com/databases). 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 postgresql://user:password@host:port/db?sslmode=require
```
### Using with Heroku PostgreSQL
You can use martin with [Managed PostgreSQL from Heroku](https://www.heroku.com/postgres) 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)