Skip to content

Function Layers

As for pg_tileserv and martin, TiMVT can support Function layer/source.

Functions are database functions which can be used to create vector tiles and must of the form:

CREATE FUNCTION name(
    -- bounding box
    xmin float,
    ymin float,
    xmax float,
    ymax float,
    -- EPSG (SRID) of the bounding box coordinates
    epsg integer,
    -- additional parameters
    query_params json
)
RETURNS bytea
Argument Type Description
xmin float left coordinate
ymin float bottom coordinate
xmax float right coordinate
ymax float top coordinate
epsg float bounding box EPSG (SRID) number
query_params json Additional Query string parameters

Query Parameters

TiMVT will forward all query parameters to the function as a JSON object. It's on the user to properly parse the JSON object in the database function.

url = "https://endpoint/tiles/my_function/1/1/1?value1=2&value2=3"
query_params = '{"value1": "2", "value2": "3"}'

url = "https://endpoint/tiles/my_function/1/1/1?v=2&v=3"
query_params = '{"v": ["2", "3"]}'

Important

Functions are not hard coded into the database but dynamically registered/unregistered by the application on each tile call.

Minimal Application

from timvt.db import close_db_connection, connect_to_db
from timvt.factory import VectorTilerFactory
from timvt.layer import FunctionRegistry
from timvt.layer import Function

from fastapi import FastAPI, Request


# Create FastAPI Application.
app = FastAPI()

# Add Function registery to the application state
app.state.timvt_function_catalog = FunctionRegistry()

# Register Start/Stop application event handler to setup/stop the database connection
# and populate `app.state.table_catalog`
@app.on_event("startup")
async def startup_event():
    """Application startup: register the database connection and create table list."""
    await connect_to_db(app)

@app.on_event("shutdown")
async def shutdown_event():
    """Application shutdown: de-register the database connection."""
    await close_db_connection(app)

# Register Function to the application internal registry
app.state.timvt_function_catalog.register(
    Function.from_file(
        id="squares",  # By default TiMVT will call a function call `squares`
        infile="my_sql_file.sql",  # PATH TO SQL FILE
    )
)

# Register endpoints
mvt_tiler = VectorTilerFactory(
    with_tables_metadata=True,
    with_functions_metadata=True,  # add Functions metadata endpoints (/functions.json, /{function_name}.json)
    with_viewer=True,
)
app.include_router(mvt_tiler.router)

Important

A function Registry object (timvt.layer.FunctionRegistry) should be initialized and stored within the application state. TiMVT assumes app.state.timvt_function_catalog is where the registry is.

Function Options

When registering a Function, the user can set different options:

  • id (required): name of the Layer which will then be used in the endpoint routes.
  • sql (required): SQL code
  • function_name: name of the SQL function within the SQL code. Defaults to id.
  • bounds: Bounding Box for the area of usage (this is for documentation only).
  • minzoom: minimum zoom level (this is for documentation only).
  • maxzoom: maximum zoom level (this is for documentation only).
  • options: List of options available per function (this is for documentation only).
from timvt.layer import Function


# Function with Options
Function(
    id="squares2",
    sql="""
        CREATE FUNCTION squares_but_not_squares(
            xmin float,
            ymin float,
            xmax float,
            ymax float,
            epsg integer,
            query_params json
        )
        RETURNS bytea AS $$
        ...
    """,
    function_name="squares_but_not_squares",  # This allows to call a specific function within the SQL code
    bounds=[0.0, 0.0, 180.0, 90.0],  # overwrite default bounds
    minzoom=9,  # overwrite default minzoom
    maxzoom=24,  # overwrite default maxzoom
    options={  # Provide arguments information for documentation
        {"name": "depth", "default": 2}
    }
)

# Using `from_file` class method
Function.from_file(
    id="squares2",
    infile="directory/my_sql_file.sql",  # PATH TO SQL FILE
    function_name="squares_but_not_squares",  # This allows to call a specific function within the SQL code
    bounds=[0.0, 0.0, 180.0, 90.0],  # overwrite default bounds
    minzoom=9,  # overwrite default minzoom
    maxzoom=24,  # overwrite default maxzoom
    options={  # Provide arguments information for documentation
        {"name": "depth", "default": 2}
    }
)

Function Layer Examples

Dynamic Geometry Example

Goal: Sub-divide input BBOX in smaller squares.

CREATE OR REPLACE FUNCTION squares(
    -- mandatory parameters
    xmin float,
    ymin float,
    xmax float,
    ymax float,
    epsg integer,
    -- additional parameters
    query_params json
)
RETURNS bytea AS $$
DECLARE
    result bytea;
    sq_width float;
    bbox_xmin float;
    bbox_ymin float;
    bounds geometry;
    depth integer;
BEGIN
    -- Find the bbox bounds
    bounds := ST_MakeEnvelope(xmin, ymin, xmax, ymax, epsg);

    -- Find the bottom corner of the bounds
    bbox_xmin := ST_XMin(bounds);
    bbox_ymin := ST_YMin(bounds);

    -- Get Depth from the query_params object
    depth := coalesce((query_params ->> 'depth')::int, 2);

    -- We want bbox divided up into depth*depth squares per bbox,
    -- so what is the width of a square?
    sq_width := (ST_XMax(bounds) - ST_XMin(bounds)) / depth;

    WITH mvtgeom AS (
        SELECT
            -- Fill in the bbox with all the squares
            ST_AsMVTGeom(
                ST_SetSRID(
                    ST_MakeEnvelope(
                        bbox_xmin + sq_width * (a - 1),
                        bbox_ymin + sq_width * (b - 1),
                        bbox_xmin + sq_width * a,
                        bbox_ymin + sq_width * b
                    ),
                    epsg
                ),
                bounds
            )

        -- Drive the square generator with a two-dimensional
        -- generate_series setup
        FROM generate_series(1, depth) a, generate_series(1, depth) b
    )
    SELECT ST_AsMVT(mvtgeom.*, 'default')

    -- Put the query result into the result variale.
    INTO result FROM mvtgeom;

    -- Return the answer
    RETURN result;
END;
$$
LANGUAGE 'plpgsql'
IMMUTABLE -- Same inputs always give same outputs
STRICT -- Null input gets null output
PARALLEL SAFE;

Extending the Function layer

As mentioned early, Function takes bounding box and EPSG number as input to support multiple TileMatrixSet. If you only want to support one pre-defined TMS (e.g WebMercator) you could have functions taking X,Y,Z inputs:

Example of XYZ function:

CREATE OR REPLACE FUNCTION xyz(
    z integer,
    x integer,
    y integer,
    query_params json
)
RETURNS bytea
AS $$
DECLARE
    table_name text;
    result bytea;
BEGIN
    table_name := query_params ->> 'table';

    WITH
    bounds AS (
      SELECT ST_TileEnvelope(z, x, y) AS geom
    ),
    mvtgeom AS (
      SELECT ST_AsMVTGeom(ST_Transform(t.geom, 3857), bounds.geom) AS geom, t.name
      FROM table_name t, bounds
      WHERE ST_Intersects(t.geom, ST_Transform(bounds.geom, 4326))
    )
    SELECT ST_AsMVT(mvtgeom, table_name)
    INTO result
    FROM mvtgeom;

    RETURN result;
END;
$$
LANGUAGE 'plpgsql'
STABLE
PARALLEL SAFE;

In order to support those function, you'll need to extend the Funcion class:

# custom.py
from typing import Any
import morecantile
from buildpg import asyncpg

from timvt import layer

class Function(layer.Function):
    "Custom Function Layer: SQL function takes xyz input."""

    async def get_tile(
        self,
        pool: asyncpg.BuildPgPool,
        tile: morecantile.Tile,
        tms: morecantile.TileMatrixSet,  # tms won't be used here
        **kwargs: Any,
    ):
        """Custom Get Tile method."""

        async with pool.acquire() as conn:
            transaction = conn.transaction()
            await transaction.start()
            await conn.execute(self.sql)

            sql_query = clauses.Select(
                Func(
                    self.function_name,
                    ":x",
                    ":y",
                    ":z",
                    ":query_params",
                ),
            )
            q, p = render(
                str(sql_query),
                x=tile.x,
                y=tile.y,
                z=tile.z,
                query_params=json.dumps(kwargs),
            )

            # execute the query
            content = await conn.fetchval(q, *p)

            # rollback
            await transaction.rollback()

        return content